GCP · BigQuery ML · Vertex AI · Production

Weather Whisperer v5
ML Nowcasting for Ride Operations

A production, serverless GCP pipeline that ingests real-time METAR observations from 17 Florida stations, GOES-18 satellite lightning, upper-air radiosonde soundings, and NWS forecasts/alerts into BigQuery — then runs 6 purpose-trained ML models plus Gemini 1.5 Flash to produce 2-hour, venue-specific nowcasts for Central Florida theme parks. Published as an adaptive JSON API (5–15 min, 1 min during active storms) and consumed cross-cloud by an AWS Bedrock agent calling Azure Function tools.

17
METAR Stations
8
Curated Tables/Views
6
Production ML Models
8
Cloud Run Jobs
1–20min
Adaptive Refresh
$6–220
Monthly Cost Range

7-Layer GCP Stack

4 external weather/lightning sources → 3 parallel Cloud Run ingestion jobs → BigQuery raw + curated views → spatial ML nowcasting + Gemini narratives → adaptive JSON publisher → GCS public bucket → cross-cloud consumers (GCP-native + an AWS Bedrock agent via Azure). Each layer is independently scalable and separately schedulable via Cloud Scheduler.

Data Sources
aviationweather.gov (METAR, 17 stations) GOES-18 GLM (lightning) University of Wyoming (Radiosonde) api.weather.gov (NWS forecast + alerts) 4 external sources
Ingestion
Cloud Run Jobs weather-updater-v6 weather-lightning-ingestor weather-updater-atmos Cloud Scheduler
Intelligence / ML
BigQuery park_data_warehouse dataset weather-ml-predictor (6 models, ST_DISTANCE spatial) weather-insights-generator (Gemini 1.5 Flash) weather-outage-tracker
Training / Publishing
weather-ml-trainer (on-demand, 44K+ obs) weather-json-publisher adaptive fast-poll scheduler 5–15 min (1 min in storms)
Storage / CDN
GCS public bucket — park-whisperer-curated-json CORS-enabled nowcast/*.json · venue_impact/*.json insights/*.json · stations/latest/*.json
Consumption
ride_whisperer (wait-time ML) 6 live Azure Function endpoints AWS Bedrock Claude agent (tool calls) Ops dashboards / live map
IaC / Ops
Modular Terraform 2 service accounts (least-privilege) Key Vault-secured cross-cloud reader SA validate / deploy / health_check / rollback scripts

From Raw Observation to Venue Nowcast

How METAR bytes, satellite lightning, and soundings become a 2-hour, venue-specific risk narrative.

1

Multi-Source Ingestion — 3 Parallel Cloud Run Jobs

weather-updater-v6 (every 10 min): fetches surface observations from aviationweather.gov for 17 Florida stations — temperature, dewpoint, pressure, wind, precipitation, visibility, present-wx codes.

weather-lightning-ingestor (every 10 min): pulls GOES-18 GLM flashes within 20 mi of the parks, 30-min lookback, MERGE-deduped into tbl_lightning_whisperer.

weather-updater-atmos (every 12 hr): fetches University of Wyoming radiosonde upper-air soundings (Station 72210, Tampa Bay) — CAPE, CIN, Lifted Index, K-Index, Total Totals, Showalter, precipitable water.

Cloud Run Jobs Cloud Scheduler Exponential backoff
2

BigQuery Raw Tables + Enhanced Curated Views

Raw observations land in partitioned/clustered tables. v_weather_current_enhanced adds enhanced thunderstorm detection (TS/VCTS/LTG pattern matching) with a clear-sky suppressor that prevents false HIGH alerts when METAR shows no activity. v_weather_forecast_enhanced gates CAPE/LI contribution so it only applies when precipitation or thunder is actually forecast.

DAY partitioning station_id clustering v_weather_current_enhanced v_weather_forecast_enhanced
3

Spatial ML Nowcasting — weather-ml-predictor (every 20 min)

6 scikit-learn models — retrained on-demand from 44K+ historical observations — score thunderstorm, precipitation, high-wind, fog, and venue-impact probability over a 2-hour horizon. BigQuery ST_DISTANCE detects nearby (100 mi) and upwind (150 mi) convective cells and boosts risk scores when they're moving toward a park.

6 ML models ST_DISTANCE spatial join 2-hr horizon
4

Narrative Generation + Outage Tracking

weather-insights-generator calls Vertex AI Gemini 1.5 Flash to turn structured risk scores into human-readable operational narratives every 20 min, feeding vw_storm_state_current — a single-row, 5-state storm severity engine (CLEAR → SEVERE) fusing lightning + NWS alerts. weather-outage-tracker flags weather-attributed ride closures against ride_weather_sensitivity thresholds every 5 min.

Gemini 1.5 Flash vw_storm_state_current tbl_weather_ride_outage_log
5

Adaptive Publisher → GCS → Cross-Cloud Consumers

weather-json-publisher serializes nowcasts, venue impact, insights, and station data to a public GCS bucket every 5–15 min — 1 min during active storms, via a self-toggled adaptive scheduler. Consumed directly by ride_whisperer and ops dashboards, and cross-cloud by an AWS Bedrock Claude agent calling Azure Function tools that read these same BigQuery views.

GCS public bucket Adaptive 1–15 min cadence Azure Function tools AWS Bedrock

6 Production ML Models + Gemini Narratives

weather-ml-predictor runs 6 purpose-trained scikit-learn models — retrained on-demand by weather-ml-trainer from 44K+ historical observations — to produce 2-hour, venue-specific probability nowcasts. weather-insights-generator then calls Vertex AI Gemini 1.5 Flash to turn each structured risk score into a human-readable operational narrative.

⛈️

thunderstorm_nowcast

Thunderstorm probability, next 2 hrs, boosted by spatial storm-tracking (BigQuery ST_DISTANCE).

ROC AUC 0.750 · 72% acc.
🌧️

precipitation_prediction

Precipitation probability, next 2 hrs, from current METAR + atmospheric instability indices.

ROC AUC 0.990 · 99% acc.
💨

high_wind_prediction

25+ kt gust probability, next 2 hrs, from surface wind trend and pressure change.

ROC AUC 1.000 · 100% acc.
🌫️

fog_prediction

Fog probability, next 2 hrs, from dewpoint depression and visibility trend.

ROC AUC 0.900 · 92% acc.
🎢

venue_impact_prediction

Per-venue operational impact, joining live conditions against ride_weather_sensitivity.

ROC AUC 0.847 · 81% acc.
🚨

high_impact_venue_prediction

High-impact venue closures — the highest-severity tier of the venue impact model.

ROC AUC 0.950 · 95% acc.

Model Performance

ModelROC AUCAccuracyPredicts
thunderstorm_nowcast0.75072%Thunderstorm probability, next 2 hrs
precipitation_prediction0.99099%Precipitation probability, next 2 hrs
high_wind_prediction1.000100%25+ kt gust probability, next 2 hrs
fog_prediction0.90092%Fog probability, next 2 hrs
venue_impact_prediction0.84781%Per-venue operational impact
high_impact_venue_prediction0.95095%High-impact venue closures

The near-perfect wind/precipitation scores likely reflect strong signal correlation in METAR features (e.g. reported gusts directly implying "high wind") rather than a uniformly hard prediction problem — validated against a held-out temporal split, not a random split.

8 Tables + Views — Partitioned + Clustered

All raw tables use DAY partitioning on the time field and clustering on station_id — queries with time and station filters never scan more than the relevant partition slices.

metar_observations — 26 fields, partitioned by observation_time+
FieldTypeDescription
station_idSTRINGICAO station identifier (one of 17 Florida stations)
observation_timeTIMESTAMPUTC observation time — partition key
temp_f / dewpoint_fFLOAT64Temperature and dewpoint (°F)
relative_humidityFLOAT64Relative humidity (%)
pressure_hpaFLOAT64Station pressure (hPa)
wind_speed_kt / wind_gust_ktFLOAT64Wind speed and gust (knots)
precip_rolling_3hr / 6hrFLOAT64Rolling precipitation totals (derived)
pressure_change_1hrFLOAT641-hour pressure trend (derived)
is_thunderstorm / is_fog / is_mistBOOLCondition flags from METAR present weather
adverse_heavy_rain / thunderstorm / fog / high_windBOOLAdverse event flags for quick filtering
wyoming_atmospheric_profiles — 22 instability fields, partitioned by sounding_timestamp+
FieldTypeDescription
cape_j_kgFLOAT64Convective Available Potential Energy (J/kg) — thunderstorm potential
cin_j_kgFLOAT64Convective Inhibition (J/kg) — cap strength
lifted_indexFLOAT64Lifted Index — negative = unstable, <-6 = severe
k_indexFLOAT64K-Index — thunderstorm probability (>40 = high)
total_totals_indexFLOAT64Total Totals — >55 indicates severe potential
showalter_indexFLOAT64Showalter Index — additional stability discriminator
pwat_inchesFLOAT64Precipitable water — flood potential
lcl / lfc / el pressureFLOAT64Lifted condensation level, free convection level, equilibrium level (hPa)
thickness_1000_500_mFLOAT641000-500mb thickness — rain vs snow discriminator
weather_forecasts — 12 fields, partitioned by forecast_timestamp+
FieldTypeDescription
forecast_timestampTIMESTAMPForecast valid time (UTC) — partition key
temperature_f / apparent_temperature_fFLOAT64NWS temperature and feels-like (°F)
precipitation_intensity_mm_hrFLOAT64Forecast precip rate (mm/hr)
cloud_cover_percentFLOAT64Cloud cover (%)
weather_condition_categorySTRINGNWS categorical condition string
active_alert_flagBOOLNWS active alert present for the forecast window — feeds vw_storm_state_current
tbl_lightning_whisperer — GOES-18 GLM flashes, MERGE-deduped+
FieldTypeDescription
strike_idSTRINGUnique flash identifier
timestampTIMESTAMPFlash detection time (UTC)
lat / lonFLOAT64Flash location
energy_joulesFLOAT64Optical energy of the flash
nearest_park / distance_milesSTRING / FLOAT64Assigned at ingest — no runtime geo math. Filtered to within 20 mi of a park.
vw_storm_state_current + ride_weather_sensitivity — the go/no-go signal+

vw_storm_state_current is a single-row, 5-state storm severity engine (CLEAR → WATCH → APPROACH → ACTIVE → SEVERE) fusing lightning proximity and NWS alerts — the primary go/no-go signal for outdoor ride operations. ride_weather_sensitivity stores historical storm/wind closure sensitivity percentages per individual ride, joined against live conditions by weather-ml-predictor and weather-outage-tracker. v_rides_currently_down joins rides currently down during operating hours with live weather context, and tbl_weather_ride_outage_log records weather-attributed closures written by the outage tracker.

8 Tables/Views → 4 GCS JSON Paths

Each curated view or model output is a stable, pre-joined query over the raw tables. The publisher serializes each to a structured JSON file at a predictable GCS path — web apps and cross-cloud agent tools fetch by path/query, no API layer needed.

v_weather_current_enhanced

2-hr rolling window with enhanced thunderstorm detection (TS/VCTS/LTG patterns) and risk scoring; clear-sky suppressor prevents false HIGH alerts.

→ stations/latest/*.json

v_weather_forecast_enhanced

48-hr NWS forecast with CAPE/LI contribution gated to only apply when precipitation or thunder is actually forecast.

→ stations/latest/*.json

v_weather_intelligence

Unified current + forecast view: overall_risk_score, risk_level, primary_concern, ride_impact_status, next-6-hour array.

→ insights/*.json

vw_storm_state_current

Single-row, 5-state storm severity engine — the primary go/no-go signal for outdoor ride ops.

→ insights/*.json

ride_weather_sensitivity

Historical storm/wind closure sensitivity percentages, per individual ride.

→ venue_impact/*.json

v_rides_currently_down

Rides currently down during operating hours joined with live weather context.

→ venue_impact/*.json

tbl_lightning_whisperer + weather-ml-predictor output

Deduplicated GLM flashes and 6-model probability nowcasts (thunderstorm, precip, wind, fog, venue impact) over a 2-hr horizon.

→ nowcast/*.json

tbl_weather_ride_outage_log

Weather-attributed ride closures written by weather-outage-tracker.

→ venue_impact/*.json

8 Independently Scheduled Jobs

All compute runs as Cloud Run Jobs — no always-on instances, billed per execution. Cloud Scheduler drives the cadence; each job is independently deployable via Terraform.

Every 10 minutes

weather-updater-v6

Pulls and parses METAR observations for 17 Florida stations into BigQuery, deriving rolling precip, pressure change, and adverse condition booleans.

Cloud Run→ BigQuery
Every 10 minutes

weather-lightning-ingestor

Ingests GOES-18 GLM lightning flashes within 20 mi of the parks (30-min lookback); MERGE-dedupes into tbl_lightning_whisperer.

Cloud Run→ BigQuery
Every 12 hours

weather-updater-atmos

Ingests Tampa Bay radiosonde upper-air soundings (Station 72210) — CAPE, CIN, Lifted Index, K-Index, and more.

Cloud Run→ BigQuery
Every 20 minutes

weather-ml-predictor

Spatial storm-tracked nowcast + venue impact scoring using BigQuery ST_DISTANCE across all 6 production ML models.

Cloud RunBigQuery ML
Every 20 minutes

weather-insights-generator

Calls Vertex AI Gemini 1.5 Flash to generate atmospheric risk narratives and operational recommendations.

Cloud RunVertex AI
Every 5 minutes

weather-outage-tracker

Flags weather-attributed ride closures against ride_weather_sensitivity risk thresholds.

Cloud Run→ BigQuery
On-demand

weather-ml-trainer

Trains/retrains all 6 scikit-learn models from 44K+ historical observations; writes versioned pickled models to GCS.

Cloud Run→ GCS
5–15 min (1–5 min in storms)

weather-json-publisher

Publishes the curated JSON API; manages the storm-adaptive fast-poll scheduler, escalating cadence automatically.

Cloud RunBigQuery →GCS

Modular Terraform · Scripted Deploy Lifecycle

Every resource (BigQuery, GCS, Cloud Run Jobs, Cloud Scheduler, service accounts, IAM, Artifact Registry) is defined in Terraform with remote state in a versioned GCS backend. Deploy/validate/rollback/health-check scripts make the lifecycle repeatable, not a manual runbook.

Terraform

Modular IaC

bigquery.tf, cloud_run_jobs.tf, cloud_scheduler.tf, service_accounts.tf, storage.tf, with remote state in a versioned GCS backend. Image field intentionally ignore_changes so app deploys and infra deploys don't fight each other.

Service Accounts

2 Least-Privilege SAs

weather-ingestion-sa: BigQuery dataEditor + jobUser.
weather-publisher-sa: BigQuery dataViewer/dataEditor + GCS storage.objectAdmin (single bucket) + aiplatform.user (Gemini path only).

Cross-Cloud

azure-bigquery-reader SA

Read-only (dataViewer + jobUser) GCP service account whose key is stored in Azure Key Vault and fetched at runtime by the Azure Function tools — never checked in, never given write access.

Build

Artifact Registry

Each job (ml, publisher, insights, ingestors) has its own Dockerfile + build.sh pushing to a private Artifact Registry repo.

Scripts

Validate / Deploy / Health-check / Rollback

A scripts/ folder ships pre-deploy validation, full apply sequence, post-deploy health checks (jobs, tables, models, scheduler state), and rollback — scripted and repeatable, not a manual runbook.

Cost

$6–220/month

~$6/mo steady-state core pipeline at documented usage. The original full agentic stack (7 jobs, ~22,500 executions/mo) was ~$100–220/mo; a minimal hybrid tool-calling redesign cut that to ~$22–52/mo with no capability loss.

Operational SLOs

Key metrics, targets, and alert thresholds. Daily health check runs check_job_status.sh and queries BigQuery for data freshness.

MetricTargetAlert ThresholdSeverity
Job success rate> 99%< 95%P1
METAR data freshness< 15 min> 30 minP1
Job execution duration< 2 min> 5 minP2
BigQuery storage< 10 GB> 50 GBP3
GCS object count~200< 50 or > 1000P2
Monthly cost (core pipeline)< $50> $250P2
Model ROC AUC decay< 5% drop> 10% dropP2
HTTP 429 rate (external APIs)< 1/hour> 10/hourP2 — exponential backoff active

AWS Bedrock → Azure Function → BigQuery

A working precursor to today's standardized agent-interoperability stacks, and a proven cost re-architecture story.

Agent

AWS Bedrock Claude Agent

Calls weather "tools" implemented as Azure Function endpoints (/weather/current, /atmospheric, /trends, /summary) via an OpenAPI action-group schema — the agent decides when it needs current conditions, atmospheric indices, trends, or a region summary.

Tool Backend

Azure Function + Key Vault

Authenticates to BigQuery using the read-only azure-bigquery-reader GCP service account, whose key lives in Azure Key Vault and is fetched at runtime — never checked in, never given write access.

Standard

Precursor to MCP/A2A

Functionally the same shape as an MCP tool server or an OIDC-secured agent gateway, just implemented before those standards existed in this codebase — a natural modernization path (swap the static key for short-lived OIDC federation, move onto a standardized MCP tool server).

Cost Re-Architecture

$100–220/mo → $22–52/mo

The original full agentic stack (7 Cloud Run jobs, ~22,500 executions/month, heavy BigQuery ML queries) was estimated at ~$100–220/month. A "minimal hybrid" redesign — one ingestion job plus tool-calling on read (~4,320 executions/month) — cut that to ~$22–52/month with no loss of capability, by moving compute from "always scheduled" to "on-demand at agent query time."

Key Engineering Choices

Why 6 focused scikit-learn models instead of one multi-output model or BigQuery ML?+

Thunderstorm, precipitation, high-wind, fog, venue-impact, and high-impact-venue prediction have different feature relevance and different cost-of-error profiles. Keeping them as 6 separately trained scikit-learn models (retrained on-demand by weather-ml-trainer from 44K+ observations, pickled and versioned in GCS) lets weather-ml-predictor report a distinct confidence per hazard rather than one blended score that obscures which risk is actually driving a recommendation. BigQuery remains the single source of truth for features and labels — the models just live outside it, where model choice and explainability are easier to control.

Why radiosonde data (upper-air soundings) in addition to surface METAR?+

CAPE, CIN, and Lifted Index from upper-air soundings are leading indicators of convective development — they signal thunderstorm potential 3–6 hours before surface conditions deteriorate. METAR alone would only react to thunderstorms after they're already overhead. The atmospheric component of the ride impact score gives operations staff advance warning: a high CAPE + low CIN sounding at 12Z means the afternoon convective risk is elevated even if current surface conditions look fine. The University of Wyoming archives soundings at 00Z and 12Z — the 12-hour ingestion schedule matches this availability window exactly.

Why publish JSON to GCS instead of an API endpoint?+

The consuming application is hosted on Azure, and now also an AWS Bedrock agent. A GCP API Gateway in front of BigQuery would add ~$50–100/month in API call costs and a round-trip to GCP on every request. Instead, the publisher job runs every 5–15 minutes (1 minute during storms) and writes pre-computed JSON to a CORS-enabled public GCS bucket. Azure web apps fetch the JSON directly — no GCP API calls at read time. GCS standard storage for ~200 objects costs under $1/month. This architecture decouples the compute (GCP) from the consumption (Azure, AWS) with no persistent connection required.

Why partition + cluster BigQuery tables? What does it save?+

METAR observations accumulate at ~144 records/day per station (every 10 min). At 17 monitored stations, that's ~2,450 rows/day, ~894,000 rows/year — small in absolute terms, but the curated views are queried on every predictor/publisher run (every 10–20 min). Without partitioning, BigQuery would scan the full table on every query. DAY partitioning on observation_time with clustering on station_id means each view only scans the relevant partition and skips other stations via clustering — reducing per-query bytes scanned from GB to KB. BigQuery charges $5/TB scanned; partition + clustering drops the query cost to near zero.

Why Cloud Run Jobs instead of Cloud Functions or always-on services?+

Cloud Run Jobs are the right primitive for batch/scheduled workloads that run for seconds to minutes and then exit. Each ingestion job runs for <2 minutes every 15–60 minutes — an always-on container or Cloud Function with a long timeout would be idle >95% of the time. Cloud Run Jobs bill per CPU-second of execution, not per allocation time. Cloud Functions have a 9-minute max timeout — the atmospheric ingestion job parses complex sounding data and could approach this. Cloud Run Jobs have a 1-hour timeout with no per-invocation startup overhead once the container is cached. Cloud Scheduler triggers each job on its own schedule independently.

Digital Twin Premium ↗ EARE on AWS ↗ All Projects ↗