This lesson teaches you how weather data actually arrives, gets parsed, and lands in Bronze. By the end, you'll understand the file formats (GRIB2, NetCDF), the tooling that reads them, how to schedule incremental ingestion on Databricks, and the key architectural decisions at this layer.
Numerical Weather Prediction (NWP) models produce forecast data on fixed schedules. The three main delivery mechanisms:
| Source | Format | Delivery | Frequency | Typical Latency |
|---|---|---|---|---|
| ECMWF IFS | GRIB2 | MARS API / CDS API / S3 dissemination | 4× daily (00, 06, 12, 18 UTC) | ~4–6 h after init time |
| GFS (NOAA) | GRIB2 | HTTPS / AWS Open Data (S3) | 4× daily | ~3.5 h after init time |
| ICON (DWD) | GRIB2 | DWD Open Data (HTTPS) | 4× daily | ~2–4 h after init time |
| ERA5 Reanalysis | NetCDF / GRIB2 | CDS API (Copernicus) | Monthly (5-day delay) | Days to weeks |
| Commercial providers | NetCDF / JSON / Parquet | REST API / SFTP / Cloud push | Varies | Minutes to hours |
GRIB2 (GRIdded Binary, edition 2) is the standard format for meteorological data. It's compact but not self-describing — you need external tables to decode message meanings.[1]
# The toolchain for GRIB2 → tabular data
import xarray as xr
import cfgrib
# Open GRIB2 file with cfgrib engine
ds = xr.open_dataset(
"ecmwf_forecast_20260717_00z.grib2",
engine="cfgrib",
backend_kwargs={
"filter_by_keys": {
"shortName": ["10u", "10v", "2t", "ssrd"], # wind u/v, temp, irradiance
"typeOfLevel": "heightAboveGround"
}
}
)
# Result: xarray Dataset with dimensions (time, latitude, longitude)
# Variables: u10, v10, t2m, ssrd — each is a 3D array
print(ds)
# <xarray.Dataset>
# Dimensions: (time: 81, latitude: 721, longitude: 1440)
# Coordinates:
# * time (time) datetime64[ns]
# * latitude (latitude) float64
# * longitude (longitude) float64
# Data variables:
# u10 (time, latitude, longitude) float32
# v10 (time, latitude, longitude) float32
# t2m (time, latitude, longitude) float32
# ssrd (time, latitude, longitude) float32
📦 Library Stack
cfgrib decodes GRIB2 messages. xarray provides the labelled multi-dimensional array API. eccodes (ECMWF's C library) is the underlying decoder. On Databricks, install via cluster init script or %pip install cfgrib eccodes xarray.
The bridge between meteorology (xarray) and Databricks (Spark) is the key engineering challenge. Here's the pattern:
import xarray as xr
import pandas as pd
from pyspark.sql import SparkSession
from pyspark.sql.types import *
spark = SparkSession.builder.getOrCreate()
def grib_to_bronze_df(grib_path: str, init_time: str, source_model: str):
"""Parse a GRIB2 file and return a Spark DataFrame for Bronze."""
# 1. Open with xarray/cfgrib
ds = xr.open_dataset(grib_path, engine="cfgrib")
# 2. Convert to pandas (flatten grid → rows)
# Each row = one grid point × one time step
df_pd = ds.to_dataframe().reset_index()
# 3. Rename to domain terms
df_pd = df_pd.rename(columns={
"latitude": "grid_lat",
"longitude": "grid_lon",
"time": "valid_time", # forecast valid time
"u10": "wind_u_ms",
"v10": "wind_v_ms",
"t2m": "temperature_k",
"ssrd": "irradiance_jm2",
})
# 4. Add metadata columns
df_pd["init_time"] = pd.Timestamp(init_time)
df_pd["source_model"] = source_model
df_pd["ingested_at"] = pd.Timestamp.utcnow()
# 5. Convert to Spark DataFrame
return spark.createDataFrame(df_pd)
# Usage in a Databricks notebook or job
bronze_df = grib_to_bronze_df(
"/mnt/raw/ecmwf/20260717_00z.grib2",
init_time="2026-07-17T00:00:00Z",
source_model="ecmwf_ifs"
)
# Write to Bronze — append-only, partitioned by date for manageability
bronze_df.write.format("delta") \
.mode("append") \
.partitionBy("init_time") \
.saveAsTable("bronze.weather_raw")
Bronze is raw and immutable. It stores grid_lat / grid_lon — not asset_id. The geo-spatial join to assets happens in the Bronze → Silver transformation (covered in Lesson 2). This means if you add new assets later, you can reprocess Bronze without re-downloading.
-- Bronze layer: raw weather grid data as-is from source
CREATE TABLE bronze.weather_raw (
grid_lat DOUBLE NOT NULL,
grid_lon DOUBLE NOT NULL,
valid_time TIMESTAMP NOT NULL, -- what time the forecast is FOR
init_time TIMESTAMP NOT NULL, -- when the model run started
wind_u_ms DOUBLE, -- eastward wind component
wind_v_ms DOUBLE, -- northward wind component
temperature_k DOUBLE, -- 2m temperature in Kelvin
irradiance_jm2 DOUBLE, -- surface solar radiation
pressure_pa DOUBLE,
source_model STRING NOT NULL, -- "ecmwf_ifs", "gfs", "icon"
ingested_at TIMESTAMP NOT NULL -- pipeline processing timestamp
)
USING DELTA
PARTITIONED BY (init_time)
TBLPROPERTIES (
'delta.autoOptimize.optimizeWrite' = 'true',
'delta.autoOptimize.autoCompact' = 'true',
'delta.logRetentionDuration' = '30 days'
);
Each model run (init_time) produces one batch of data. Partitioning by init_time means: (1) appending a new forecast run creates one new partition — no rewrites, (2) reprocessing a specific run is a targeted read, (3) retention policies can drop old init_times cleanly.
Weather ingestion runs on a fixed schedule tied to model availability. Here's how it maps to Databricks Workflows:
Network failures, API timeouts, and duplicate triggers happen. Your ingestion must be idempotent: running the same job twice for the same init_time must not create duplicate data.
# Pattern: MERGE to enforce idempotency at Bronze
# If init_time + grid_lat + grid_lon + valid_time already exists, skip it
bronze_df.createOrReplaceTempView("new_weather")
spark.sql("""
MERGE INTO bronze.weather_raw AS target
USING new_weather AS source
ON target.init_time = source.init_time
AND target.grid_lat = source.grid_lat
AND target.grid_lon = source.grid_lon
AND target.valid_time = source.valid_time
AND target.source_model = source.source_model
WHEN NOT MATCHED THEN INSERT *
""")
💡 Alternative: Append + Dedup at Silver
Some teams prefer simpler Bronze (blind append) and handle deduplication at the Silver layer. Trade-off: Bronze grows faster but Silver logic is more complex. For weather data with clear natural keys (init_time, grid point, valid_time), MERGE at Bronze is clean and prevents waste early.
How big is weather data? Let's estimate for a typical ECMWF forecast:
| Dimension | Value | Notes |
|---|---|---|
| Grid points (global 0.25°) | ~1 million | 721 lat × 1440 lon |
| Variables | 5–15 | wind, temp, irradiance, pressure… |
| Time steps per run | ~81 | Hourly out to 3–5 days |
| Rows per run (flattened) | ~400M–1.2B | grid × time × variable |
| GRIB2 file size | ~2–10 GB | Compressed binary |
| Delta table size (one run) | ~15–50 GB | Parquet, snappy compressed |
If your wind parks are in the North Sea, you only need a geographic subset. Filtering by bounding box during download or parse reduces data by 95%+. Most APIs support spatial subsetting at request time (ECMWF's MARS/CDS, GFS via grib_filter). Always filter as early as possible.
# Subset to relevant area during parse (e.g., North Sea + Northern Europe)
ds = xr.open_dataset("forecast.grib2", engine="cfgrib")
ds_subset = ds.sel(
latitude=slice(62, 48), # North Sea region
longitude=slice(-5, 15)
)
# Reduces ~1M grid points to ~5,000 — manageable on a single node
Your team likely consumes multiple NWP sources (ECMWF for accuracy, GFS as backup, ICON for Central European detail). The source_model column in Bronze handles this naturally:
-- Query: compare forecasts from different models for same valid_time
SELECT
source_model,
AVG(wind_u_ms) as mean_wind_u,
AVG(temperature_k) as mean_temp
FROM bronze.weather_raw
WHERE init_time = '2026-07-17T00:00:00Z'
AND valid_time = '2026-07-18T12:00:00Z'
AND grid_lat BETWEEN 54.0 AND 56.0
AND grid_lon BETWEEN 5.0 AND 8.0
GROUP BY source_model
At the Silver layer, you choose which model feeds the production forecast (usually the most recent available, or an ensemble blend). This decision is a Silver-layer concern — Bronze stores all models equally.
| Aspect | Recommendation | Why |
|---|---|---|
| Cluster type | Job cluster (ephemeral) | Starts for the job, terminates after. No idle cost. |
| Node count | Single node (for subset ingest) | cfgrib/xarray run in the driver. Parallelism comes from multiple tasks, not multiple nodes. |
| Instance type | Memory-optimised (e.g., Standard_E4s_v3) | xarray loads full grid into memory for subsetting. |
| Libraries | Init script: install eccodes, cfgrib, xarray | Not in default Databricks Runtime. Pin versions for reproducibility. |
| Parallelism | Multiple Workflow tasks (one per model source) | ECMWF, GFS, ICON run as parallel tasks in the same Workflow. |
Weather ingestion has strict timeliness requirements — stale forecasts degrade your power predictions. Build these checks into your pipeline:
MAX(init_time) in Bronze is ≤ 6 hours old. Alert if breached.mergeSchema) to handle gracefully.# Example: freshness assertion in a Databricks notebook task
from datetime import datetime, timedelta, timezone
latest = spark.sql("""
SELECT MAX(init_time) as latest_init FROM bronze.weather_raw
WHERE source_model = 'ecmwf_ifs'
""").collect()[0]["latest_init"]
threshold = datetime.now(timezone.utc) - timedelta(hours=8)
assert latest >= threshold, f"STALE DATA: latest init_time is {latest}, expected >= {threshold}"
A GRIB2 file from ECMWF contains forecasts initialised at 00:00 UTC with valid times at +1h, +2h, … +72h. Which column captures "when the model run started"?
Why is the Bronze weather table partitioned by init_time rather than by grid coordinates?
Your ECMWF ingestion job runs at 05:00 UTC but the model run isn't available yet. What should Task 1 (Check) do?
Your wind parks are all in the North Sea (lat 52–58°, lon 2–9°). The ECMWF global grid has ~1 million points. What's the most impactful optimisation?
📖 Recommended Reading
Databricks Workflows documentation — covers job creation, task dependencies, scheduling (cron), retries, and cluster policies. Read the "Create and manage jobs" and "Configure compute for jobs" sections. Then look at ECMWF's data access guide for understanding how weather data is actually retrieved.
You now understand how weather data enters Databricks. The next lesson will cover the Bronze → Silver transformation in depth — the geo-spatial join, wind speed calculation from u/v components, unit conversions, and how to handle multiple forecast versions (init times) for the same valid time.
Questions about GRIB2 parsing, scheduling, or cluster sizing? Ask your teacher — I can dive deeper into cfgrib configuration, multi-model ensemble strategies, or cost estimation for your fleet size.
Rechtliches: Impressum