diff --git a/places_insights/notebooks/custom_location_scores/README.md b/places_insights/notebooks/custom_location_scores/README.md new file mode 100644 index 0000000..e69de29 diff --git a/places_insights/notebooks/custom_location_scores/places_insights_custom_location_scores.ipynb b/places_insights/notebooks/custom_location_scores/places_insights_custom_location_scores.ipynb new file mode 100644 index 0000000..0b203cd --- /dev/null +++ b/places_insights/notebooks/custom_location_scores/places_insights_custom_location_scores.ipynb @@ -0,0 +1,571 @@ +{ + "nbformat": 4, + "nbformat_minor": 0, + "metadata": { + "colab": { + "provenance": [], + "private_outputs": true + }, + "kernelspec": { + "name": "python3", + "display_name": "Python 3" + }, + "language_info": { + "name": "python" + } + }, + "cells": [ + { + "cell_type": "code", + "source": [ + "# Copyright 2026 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." + ], + "metadata": { + "id": "hWTdLZpCewIw" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "source": [ + "# AI-Powered Custom Location Scoring with Places Insights & Gemini\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + "
\n", + " \n", + " \"Google
Open in Colab\n", + "
\n", + "
\n", + " \n", + " \"Google
Open in Colab Enterprise\n", + "
\n", + "
\n", + " \n", + " \"BigQuery
Open in BigQuery Studio\n", + "
\n", + "
\n", + " \n", + " \"GitHub
View on GitHub\n", + "
\n", + "
" + ], + "metadata": { + "id": "uQAT7_P4fqT3" + } + }, + { + "cell_type": "markdown", + "source": [ + "### Overall Goal\n", + "\n", + "This notebook demonstrates a powerful workflow for generating **Custom Location Scores** (\"suitability scores\") for real estate or travel. Instead of relying solely on raw counts of nearby amenities, we combine **quantitative data** (Google Maps Platform Public Datasets) with **qualitative data** (listing descriptions) and use Generative AI to calculate a nuanced score for a specific user persona (e.g., a \"Young Family\").\n", + "\n", + "The result is an **interactive map with color-coded markers**, where the AI not only rates a location (0-10) but provides the **reasoning** behind the score directly in the map popup.\n", + "\n", + "### Key Technologies Used\n", + "\n", + "* **[Places Insights](https://developers.google.com/maps/documentation/placesinsights):** A Google Maps Platform dataset available in BigQuery that allows for advanced statistical analysis. It provides aggregated counts and density information for Places-of-Interest (POI).\n", + "* **[BigQuery](https://cloud.google.com/bigquery):** The serverless data warehouse used to store the listings and execute the analysis.\n", + "* **[BigQuery AI.GENERATE](https://docs.cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-ai-generate):** A SQL function that allows us to call Generative AI models (like Gemini) directly within a query to generate scores and text reasoning.\n", + "* **[Vertex AI (Gemini)](https://cloud.google.com/vertex-ai/):** We use the `gemini-flash-latest` model to analyze the combined data.\n", + "* **[Google Maps 2D Tiles](https://developers.google.com/maps/documentation/tile/2d-tiles-overview):** To use Google Maps as the high-resolution basemap for visualization.\n", + "* **Python Libraries:**\n", + " * **[Folium](https://python-visualization.github.io/folium/latest/)** for creating the final interactive, layered map.\n", + " * **[Pandas](https://pandas.pydata.org/)** for handling the synthetic listing data.\n", + "\n", + "See [Google Maps Platform Pricing](https://mapsplatform.google.com/intl/en_uk/pricing/) and [BigQuery Pricing](https://cloud.google.com/bigquery/pricing) for costs associated with running this notebook.\n", + "\n", + "**Note:** The `AI.GENERATE` function passes input to a Gemini model and incurs charges in Vertex AI each time it is called. For details on managing these costs, see the **[Best Practices](https://docs.cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-ai-generate#best_practices)** section of the documentation.\n", + "\n", + "### The Step-by-Step Workflow\n", + "\n", + "1. **Generate Target Candidates:** We begin by creating a dataset of hypothetical apartment listings (including descriptions like \"Quiet street\" vs. \"Busy intersection\"). **To store this data, the notebook automatically creates a dedicated BigQuery dataset in your project.**\n", + "\n", + "2. **The AI-SQL Scoring Engine:** We execute a single, powerful SQL query in BigQuery that:\n", + " * Performs a spatial join to count specific amenities (Parks, Schools, Museums) within 800m of each listing.\n", + " * Constructs a natural language prompt combining these counts with the listing description.\n", + " * Calls **Gemini** via `AI.GENERATE` to calculate a \"Family-Friendliness Score\" and explain *why* that score was given.\n", + "\n", + "3. **Visual Analysis:** In the final step, we render the scored locations on a Google Map. We use **color-coded pins** (Green=High Score, Red=Low Score) to instantly identify the best candidates. Clicking a marker reveals a popup with the AI's detailed reasoning.\n", + "\n", + "4. **Resource Cleanup:** A final, interactive cell allows you to delete the temporary BigQuery dataset and all its contents, ensuring you leave your project clean.\n", + "\n", + "### **How to Use This Notebook**\n", + "\n", + "1. **Set Up Secrets:** Before you begin, you must configure two secrets in the Colab \"Secrets\" tab (the **key icon** on the left menu):\n", + " * `GCP_PROJECT_ID`: Your Google Cloud Project ID.\n", + " * `GMP_API_KEY`: Your Google Maps Platform API key.\n", + "\n", + "2. **Enable APIs:** Ensure the following APIs are enabled in your Google Cloud Console for the project:\n", + " * **Maps Tiles API** (for the visualization).\n", + " * **Vertex AI API** (for the AI scoring).\n", + "\n", + "3. **Subscribe to Places Insights (US Data):**\n", + " This notebook analyzes locations in New York, so you must have access to the United States dataset in BigQuery.\n", + " * **Sign Up:** If you haven't already, [sign up for access](https://developers.google.com/maps/documentation/placesinsights/cloud-setup) to Places Insights.\n", + " * **Choose Your Dataset:**\n", + " * **Full Data Option:** Subscribe to the full **\"Places Insights - US\"** listing. Ensure your linked dataset is named `places_insights___us`.\n", + " * **Sample Data Option:** You may also use the **[US Sample Data](https://developers.google.com/maps/documentation/placesinsights/cloud-setup#sample_data)** (\"Places Insights - US - sample\"), as it covers New York City.\n", + " * **Important:** If you use the **Sample Data** (or name your dataset differently), you **must update** the `PLACES_TABLE` variable in **Phase 3** of this notebook to match your linked dataset name (e.g., `places_insights___us___sample`).\n", + "\n", + "4. **Run the Cells:** Once the secrets and data subscriptions are ready, simply run the cells in order." + ], + "metadata": { + "id": "8jjHCex58gYW" + } + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "J7ij3r2truTB" + }, + "outputs": [], + "source": [ + "# @title Phase 1: Authentication & Setup\n", + "# @markdown This cell authenticates you to Google Cloud, retrieves API keys, and **creates a new BigQuery dataset** (`places_insights_custom_score_demo`) in your project to store the demo data.\n", + "\n", + "import sys\n", + "from google.colab import userdata, auth\n", + "from google.cloud import bigquery\n", + "import google.auth\n", + "import pandas as pd\n", + "\n", + "# 1. Retrieve Secrets\n", + "try:\n", + " GCP_PROJECT_ID = userdata.get('GCP_PROJECT_ID')\n", + " GMP_API_KEY = userdata.get('GMP_API_KEY')\n", + " print(f\"βœ… Secrets retrieved for project: {GCP_PROJECT_ID}\")\n", + "except userdata.SecretNotFoundError as e:\n", + " raise ValueError(\"Missing Secrets! Please add 'GCP_PROJECT' and 'GMP_API_KEY' to Colab Secrets.\") from e\n", + "\n", + "# 2. Authenticate User\n", + "auth.authenticate_user(project_id=GCP_PROJECT_ID)\n", + "print(\"βœ… User Authenticated.\")\n", + "\n", + "# 3. Initialize BigQuery Client\n", + "client = bigquery.Client(project=GCP_PROJECT_ID)\n", + "REGION = \"US\" # Places Insights is in the US multi-region\n", + "DATASET_ID = f\"{GCP_PROJECT_ID}.places_insights_custom_score_demo\"\n", + "\n", + "# Create a working dataset to store our staging table\n", + "ds = bigquery.Dataset(DATASET_ID)\n", + "ds.location = REGION\n", + "client.create_dataset(ds, exists_ok=True)\n", + "print(f\"βœ… Working dataset ready: {DATASET_ID}\")" + ] + }, + { + "cell_type": "code", + "source": [ + "# @title Phase 2: Create Target Listings Data\n", + "# @markdown Generates a hypothetical portfolio of **candidate properties** to simulate a real-world scenario.\n", + "# @markdown\n", + "# @markdown Each listing combines **geospatial coordinates** (for spatial analysis) with **text descriptions** (for AI sentiment analysis), staging the data in **BigQuery** for the scoring engine.\n", + "\n", + "# 1. Define Apartment Listings (Quantitative + Qualitative)\n", + "data = [\n", + " {\n", + " \"id\": 1,\n", + " \"name\": \"The Midtown Lofts\",\n", + " \"lat\": 40.7511, \"lng\": -73.9846, # Times Square area\n", + " \"description\": \"Modern loft in the heart of the city. High energy, constant noise, and nightlife right downstairs. Great for singles, difficult for quiet sleepers.\"\n", + " },\n", + " {\n", + " \"id\": 2,\n", + " \"name\": \"Suburban Garden Suites\",\n", + " \"lat\": 40.6586, \"lng\": -73.9764, # Near Prospect Park\n", + " \"description\": \"Quiet, tree-lined street near a major park. Large backyard, very low traffic. Perfect for pets and kids.\"\n", + " },\n", + " {\n", + " \"id\": 3,\n", + " \"name\": \"The Transit Hub\",\n", + " \"lat\": 40.6892, \"lng\": -73.9772, # Near Atlantic Terminal\n", + " \"description\": \"Unbeatable commute. Located directly above a major subway hub. Extremely busy intersection, no green space immediately adjacent.\"\n", + " },\n", + " {\n", + " \"id\": 4,\n", + " \"name\": \"Riverside Retreat\",\n", + " \"lat\": 40.8000, \"lng\": -73.9700, # Upper West Side / Riverside\n", + " \"description\": \"Scenic views of the river. historic building, slower pace of life but plenty of cultural amenities and museums nearby.\"\n", + " },\n", + " {\n", + " \"id\": 5,\n", + " \"name\": \"Industrial Fixer-Upper\",\n", + " \"lat\": 40.7050, \"lng\": -73.9200, # Bushwick area\n", + " \"description\": \"Converted warehouse space. Very gritty, few amenities nearby, but huge open floor plan. Developing neighborhood.\"\n", + " }\n", + "]\n", + "\n", + "df_listings = pd.DataFrame(data)\n", + "\n", + "# 2. Upload to BigQuery\n", + "table_id = f\"{DATASET_ID}.apartment_listings\"\n", + "\n", + "job_config = bigquery.LoadJobConfig(\n", + " # specific schema to ensure Lat/Lng are floats\n", + " schema=[\n", + " bigquery.SchemaField(\"id\", \"INTEGER\"),\n", + " bigquery.SchemaField(\"name\", \"STRING\"),\n", + " bigquery.SchemaField(\"lat\", \"FLOAT\"),\n", + " bigquery.SchemaField(\"lng\", \"FLOAT\"),\n", + " bigquery.SchemaField(\"description\", \"STRING\"),\n", + " ],\n", + " write_disposition=\"WRITE_TRUNCATE\", # Overwrite if exists\n", + ")\n", + "\n", + "job = client.load_table_from_dataframe(df_listings, table_id, job_config=job_config)\n", + "job.result() # Wait for job to complete\n", + "\n", + "print(f\"βœ… Data uploaded to `{table_id}`\")\n", + "print(f\" Rows inserted: {job.output_rows}\")\n", + "df_listings.head()" + ], + "metadata": { + "id": "jBg8EY7dsEUi" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "code", + "source": [ + "# @title Phase 3: Custom Location Scoring Engine\n", + "# @markdown Executes the core **AI-SQL Scoring Engine** by fusing **Places Insights** data with **BigQuery `AI.GENERATE`**.\n", + "# @markdown\n", + "# @markdown We first perform a spatial join to quantify nearby amenities (parks, schools, etc.), then pass these metrics, along with listing descriptions, directly into **Gemini** to calculate a nuanced Family-Friendliness Score and explanation.\n", + "\n", + "from google.cloud import bigquery\n", + "import pandas as pd\n", + "\n", + "# 1. Define Resources\n", + "PLACES_TABLE = f\"{GCP_PROJECT_ID}.places_insights___us.places\"\n", + "LISTINGS_TABLE = f\"{DATASET_ID}.apartment_listings\"\n", + "\n", + "# 2. Construct the Global Endpoint URL\n", + "# This explicit URL ensures we target the global publisher model directly\n", + "MODEL_NAME = \"gemini-flash-latest\"\n", + "ENDPOINT_URL = f\"https://aiplatform.googleapis.com/v1/projects/{GCP_PROJECT_ID}/locations/global/publishers/google/models/{MODEL_NAME}\"\n", + "\n", + "sql_query = f\"\"\"\n", + "WITH insight_counts AS (\n", + " -- Mandatory Privacy Clause\n", + " SELECT WITH AGGREGATION_THRESHOLD\n", + " listings.id,\n", + " listings.name,\n", + " listings.lat,\n", + " listings.lng,\n", + " listings.description,\n", + " -- Quantitative Metrics (COALESCE handles privacy NULLs later)\n", + " COUNTIF(places.primary_type = 'park') AS park_count,\n", + " COUNTIF(places.primary_type = 'museum') AS museum_count,\n", + " COUNTIF(places.primary_type = 'restaurant' AND places.good_for_children = TRUE) AS family_restaurant_count,\n", + " COUNTIF(places.primary_type IN ('primary_school', 'secondary_school')) AS school_count\n", + " FROM\n", + " `{LISTINGS_TABLE}` AS listings\n", + " LEFT JOIN\n", + " `{PLACES_TABLE}` AS places\n", + " ON ST_DWITHIN(ST_GEOGPOINT(listings.lng, listings.lat), places.point, 800)\n", + " GROUP BY\n", + " listings.id, listings.name, listings.lat, listings.lng, listings.description\n", + "),\n", + "\n", + "prepared_prompts AS (\n", + " SELECT\n", + " *,\n", + " -- Prompt Engineering with Null Handling\n", + " FORMAT('''\n", + " You are an expert real estate analyst. Generate a 'Family-Friendliness Score' (0-10) for this location.\n", + "\n", + " Target User: Young family with a toddler, looking for safety, green space, and convenience.\n", + "\n", + " Location Data:\n", + " - Description: %s\n", + " - Parks nearby (800m): %d\n", + " - Schools (Primary/Secondary) nearby: %d\n", + " - Museums nearby: %d\n", + " - Family-friendly restaurants: %d\n", + "\n", + " Scoring Rules:\n", + " - High importance: High park count and presence of schools.\n", + " - Negative modifiers: Descriptions mentioning \"noise\", \"nightlife\", or \"industrial\".\n", + " - Positive modifiers: Descriptions mentioning \"quiet\", \"backyard\", or \"community\".\n", + "\n", + " Output only the score and a 1-sentence reasoning.\n", + " ''',\n", + " description,\n", + " COALESCE(park_count, 0),\n", + " COALESCE(school_count, 0),\n", + " COALESCE(museum_count, 0),\n", + " COALESCE(family_restaurant_count, 0)\n", + " ) AS prompt_text\n", + " FROM insight_counts\n", + ")\n", + "\n", + "-- Final Step: Call AI.GENERATE with Explicit Endpoint\n", + "SELECT\n", + " id,\n", + " name,\n", + " lat,\n", + " lng,\n", + " generated_output.family_friendliness_score AS score,\n", + " generated_output.reasoning\n", + "FROM (\n", + " SELECT\n", + " *,\n", + " AI.GENERATE(\n", + " prompt_text,\n", + " endpoint => '{ENDPOINT_URL}',\n", + " output_schema => 'family_friendliness_score FLOAT64, reasoning STRING'\n", + " ) AS generated_output\n", + " FROM prepared_prompts\n", + ")\n", + "ORDER BY score DESC;\n", + "\"\"\"\n", + "\n", + "print(f\"🧠 Running AI Scoring Query...\")\n", + "print(f\" Target Model: {MODEL_NAME}\")\n", + "\n", + "try:\n", + " query_job = client.query(sql_query)\n", + " df_scored = query_job.to_dataframe()\n", + " print(\"βœ… Scoring Complete!\")\n", + "\n", + " # Display results\n", + " pd.set_option('display.max_colwidth', None)\n", + " display(df_scored[['name', 'score', 'reasoning']])\n", + "\n", + "except Exception as e:\n", + " print(f\"❌ Query Failed. Error: {e}\")" + ], + "metadata": { + "id": "Wirb8GlpuXMX" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "code", + "source": [ + "# @title Phase 4a: Maps Backend Initialization (Session, Copyright & Assets)\n", + "# @markdown This cell manages the Google Maps 2D Tiles API handshake. It performs the following steps:\n", + "# @markdown 1. **Session Creation:** Authenticates and requests a \"Roadmap\" session for the target region.\n", + "# @markdown 2. **Attribution Fetching:** Queries the API for the specific copyright text required for the configured viewport.\n", + "# @markdown 3. **Asset Preparation:** Generates the compliant HTML for the Google Maps logo overlay.\n", + "\n", + "import requests\n", + "\n", + "# 1. Validation & Data Bounds\n", + "if 'df_scored' not in locals() or df_scored.empty:\n", + " raise ValueError(\"No data found! Please run Phase 3 successfully first.\")\n", + "if not GMP_API_KEY:\n", + " raise ValueError(\"GMP_API_KEY is missing. Please set it in Phase 1.\")\n", + "\n", + "# Calculate center and bounds dynamically from the dataframe\n", + "center_lat = df_scored['lat'].mean()\n", + "center_lng = df_scored['lng'].mean()\n", + "min_lat, max_lat = df_scored['lat'].min(), df_scored['lat'].max()\n", + "min_lng, max_lng = df_scored['lng'].min(), df_scored['lng'].max()\n", + "ZOOM_LEVEL = 12\n", + "\n", + "# 2. Create Google Maps Session\n", + "print(\"πŸ—ΊοΈ Initializing Google Maps Session...\")\n", + "session_url = f\"https://tile.googleapis.com/v1/createSession?key={GMP_API_KEY}\"\n", + "headers = {\"Content-Type\": \"application/json\"}\n", + "payload = {\n", + " \"mapType\": \"roadmap\",\n", + " \"language\": \"en-US\",\n", + " \"region\": \"US\"\n", + "}\n", + "\n", + "try:\n", + " response = requests.post(session_url, json=payload, headers=headers)\n", + " response.raise_for_status()\n", + " session_token = response.json().get(\"session\")\n", + " print(f\"βœ… Session Token acquired: {session_token[:10]}...\")\n", + "except Exception as e:\n", + " raise RuntimeError(f\"Failed to initialize Google Maps session: {e}\")\n", + "\n", + "# 3. Fetch Dynamic Attribution\n", + "# We use the bounds of our data to request the specific copyright for this area\n", + "print(\"Fetching dynamic attribution...\")\n", + "viewport_url = (\n", + " f\"https://tile.googleapis.com/tile/v1/viewport?key={GMP_API_KEY}\"\n", + " f\"&session={session_token}\"\n", + " f\"&zoom={ZOOM_LEVEL}\"\n", + " f\"&north={max_lat}&south={min_lat}\"\n", + " f\"&west={min_lng}&east={max_lng}\"\n", + ")\n", + "\n", + "try:\n", + " vp_response = requests.get(viewport_url)\n", + " vp_response.raise_for_status()\n", + " google_attribution = vp_response.json().get('copyright', 'Map data Β© Google')\n", + " print(f\"βœ… Attribution received: {google_attribution}\")\n", + "except Exception as e:\n", + " print(f\"⚠️ Warning: Could not fetch attribution ({e}). Defaulting.\")\n", + " google_attribution = \"Map data Β© Google\"\n", + "\n", + "# 4. Construct Logo HTML\n", + "# Standard Google on-map logo asset\n", + "logo_url = \"https://maps.gstatic.com/mapfiles/api-3/images/google_white3.png\"\n", + "\n", + "logo_html = f\"\"\"\n", + "
\n", + " \"Google\n", + "
\n", + "\"\"\"\n", + "print(\"βœ… Logo HTML prepared.\")" + ], + "metadata": { + "cellView": "form", + "id": "EWAWR0K6SJRR" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "code", + "source": [ + "# @title Phase 4b: Map Visualization\n", + "# @markdown Visualizes the **custom location scores** generated by combining **Places Insights** data with **BigQuery `AI.GENERATE`**.\n", + "# @markdown\n", + "# @markdown This interactive map plots the apartment listings, allowing you to instantly see which locations best fit the target persona and explore the reasoning.\n", + "\n", + "import folium\n", + "\n", + "# 1. Construct Tile URL\n", + "# Note: {{z}}, {{x}}, {{y}} are kept as literals for Folium/Leaflet to substitute.\n", + "tiles_url = f\"https://tile.googleapis.com/v1/2dtiles/{{z}}/{{x}}/{{y}}?session={session_token}&key={GMP_API_KEY}\"\n", + "\n", + "# 2. Initialize Map\n", + "m = folium.Map(\n", + " location=[center_lat, center_lng],\n", + " zoom_start=ZOOM_LEVEL,\n", + " tiles=tiles_url,\n", + " attr=google_attribution, # Uses the dynamic copyright fetched in Phase 4a\n", + " name=\"Google Maps\",\n", + " control_scale=True,\n", + " prefer_canvas=True\n", + ")\n", + "\n", + "# 3. Create Custom Legend HTML\n", + "# Positioned at Bottom-Right to avoid the Google Logo at Bottom-Left.\n", + "legend_html = '''\n", + "
\n", + " AI Score Legend
\n", + "     High (8-10)
\n", + "     Medium (5-7)
\n", + "     Low (0-4)
\n", + "
\n", + " '''\n", + "\n", + "# 4. Inject Overlays (Logo + Legend)\n", + "# We add the logo to the left, and the legend to the right.\n", + "m.get_root().html.add_child(folium.Element(logo_html)) # Logo (from Phase 4a)\n", + "m.get_root().html.add_child(folium.Element(legend_html)) # Legend\n", + "\n", + "# 5. Add Scored Markers\n", + "print(\"πŸ“ Plotting AI-scored locations...\")\n", + "\n", + "for _, row in df_scored.iterrows():\n", + " # Dynamic Color Logic based on AI Score\n", + " score = row['score']\n", + " if score >= 8.0:\n", + " icon_color = 'green'\n", + " icon_type = 'star'\n", + " elif score >= 5.0:\n", + " icon_color = 'orange'\n", + " icon_type = 'info-sign'\n", + " else:\n", + " icon_color = 'red'\n", + " icon_type = 'exclamation-sign'\n", + "\n", + " # Popup Content (HTML)\n", + " popup_html = f\"\"\"\n", + "
\n", + "

{row['name']}

\n", + "

Score: {score}/10

\n", + "

\"{row['reasoning']}\"

\n", + "
\n", + " \"\"\"\n", + "\n", + " folium.Marker(\n", + " location=[row['lat'], row['lng']],\n", + " popup=folium.Popup(popup_html, max_width=300),\n", + " tooltip=f\"{row['name']} ({score})\",\n", + " icon=folium.Icon(color=icon_color, icon=icon_type)\n", + " ).add_to(m)\n", + "\n", + "# 6. Display\n", + "display(m)" + ], + "metadata": { + "cellView": "form", + "id": "HeS4nK6WTmLf" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "code", + "source": [ + "# @title Phase 5: Clean Up Resources\n", + "# @markdown This cell deletes the demo dataset and all tables within it.\n", + "# @markdown\n", + "# @markdown **You will be prompted to confirm before deletion.**\n", + "\n", + "from google.cloud.exceptions import NotFound\n", + "\n", + "# Validation\n", + "print(f\"⚠️ WARNING: You are about to DELETE the dataset: `{DATASET_ID}`\")\n", + "print(f\" Project: `{GCP_PROJECT_ID}`\")\n", + "print(\" This action cannot be undone.\")\n", + "\n", + "# Interactive Input\n", + "confirmation = input(\"Type 'yes' to proceed with deletion: \").strip().lower()\n", + "\n", + "if confirmation == 'yes':\n", + " print(f\"\\nπŸ—‘οΈ Deleting dataset: {DATASET_ID}...\")\n", + " try:\n", + " # delete_contents=True removes tables inside the dataset\n", + " client.delete_dataset(DATASET_ID, delete_contents=True, not_found_ok=True)\n", + " print(f\"βœ… Successfully deleted dataset '{DATASET_ID}' and all contents.\")\n", + " except Exception as e:\n", + " print(f\"❌ Error deleting dataset: {e}\")\n", + "else:\n", + " print(f\"\\nπŸ›‘ Operation cancelled. Dataset `{DATASET_ID}` was NOT deleted.\")" + ], + "metadata": { + "id": "4MBgXd_p0aR1" + }, + "execution_count": null, + "outputs": [] + } + ] +} \ No newline at end of file