usdata
All examples

Notebook · saved results

Was 2024 warmer or wetter than normal?

Compare twelve months at Oklahoma City's airport with 1991–2020 station normals, including temperature-unit checks and monthly anomaly plots.

Download notebookDownload manifestView source

These are saved results, not live data. Execution times, source URLs, and checksums are recorded below. Set up and run examples.

Run this example · instructions and limitations

Open the executed notebook to compare twelve months of 2024 observations at Will Rogers World Airport against the 1991-2020 normals for the same station, plot both anomaly series, and verify the manifest's locked inputs. The calculated summary counts warmer-than-normal months and compares total precipitation with the sum of the monthly normals. It describes this station and year, not a climate trend or statistical significance. See examples setup to run it.

Requires usdata v0.11 or newer for noaa:climate-normals. The retained manifest holds both sources: noaa:gsom with the 2024 calendar year, and noaa:climate-normals with period: monthly and no dates, because normals are 30-year averages rather than observations. The anomaly is the observed value minus its normal.

The two DATE columns carry different labels: GSOM uses YYYY-MM and monthly normals use MM, so the notebook reads both as text and relabels the normals onto the observed months. NCEI's units=metric converts MLY-PRCP-NORMAL but not MLY-TAVG-NORMAL, which arrives in degrees Fahrenheit; the notebook detects that and converts it rather than subtracting mixed units.

Both sources are query-shaped URLs that the agencies revise, so a lockfile can go stale. See --update in the manifest reference for accepting new bytes for named entries, and the NOAA normals notes for variable codes and units. Keep the manifest, lockfile, and cached bytes together.

Compare 2024 monthly observations at Will Rogers World Airport in Oklahoma City against the 1991-2020 normals for the same station. NOAA Global Summary of the Month (GSOM) supplies the observations, noaa:climate-normals supplies the 30-year averages, and the anomaly is the observed value minus its normal. This example requires usdata v0.11 or newer.

Saved outputs are a recorded run, with execution times and source provenance below. Restart the kernel and run all cells to run it yourself. See examples setup for the optional notebook environment.

Code · cell 1
from datetime import UTC, datetime
from pathlib import Path

import pandas as pd
from IPython.display import Markdown, display

import usdata
from usdata.pull import pull, verify

manifest = Path("examples/climate-anomalies/dataset.yaml")
if not manifest.is_file():
    manifest = Path("dataset.yaml")

pd.set_option("display.max_rows", 12)
pd.set_option("display.max_columns", 8)
print(f"Executed (UTC): {datetime.now(UTC).isoformat(timespec='seconds')}")
print(f"usdata {usdata.__version__}; pandas {pd.__version__}")
Executed (UTC): 2026-09-12T05:44:27+00:00
usdata 0.12.0; pandas 3.0.5

1. Declare both inputs

One manifest declares the two sources the comparison needs. The GSOM source selects the twelve 2024 UTC calendar months at one station. The normals source carries no dates, because normals are 30-year averages rather than observations; period: monthly selects the monthly dataset and the whole year is requested. units: metric asks both services for millimeters and degrees Celsius.

Code · cell 2
print(manifest.read_text())
name: okc-climate-anomalies
sources:
  - dataset: noaa:gsom
    start: 2024-01-01
    end: 2024-12-31
    variables: [TAVG, PRCP]
    params:
      stations: USW00013967
      units: metric
  - dataset: noaa:climate-normals
    variables: [MLY-TAVG-NORMAL, MLY-PRCP-NORMAL]
    params:
      period: monthly
      stations: USW00013967
      units: metric

2. Pull and open both sources

The first pull writes one lockfile, covering both assets, next to the manifest. Index the fetched items by dataset_id rather than by position. The two DATE columns are different labels: GSOM uses YYYY-MM and monthly normals use MM, so both are read as text with an explicit dtype.

NCEI's units=metric converts MLY-PRCP-NORMAL but not MLY-TAVG-NORMAL, which comes back in degrees Fahrenheit, keeping the leading spaces the service puts on standard-unit values. That is not the only data type units=metric gets wrong; the normals guide lists what was probed, including the derived codes it mis-converts. Read MLY-TAVG-NORMAL as text so the mismatch stays visible instead of quietly subtracting Fahrenheit from Celsius below.

Code · cell 3
result = pull(manifest)
items = {item.asset.dataset_id: item for item in result.fetched}
assert len(items) == len(result.fetched) == 2, "expected exactly one asset per source"
observed = items["noaa:gsom"].open(dtype={"DATE": "string"}).set_index("DATE")
normals = (
    items["noaa:climate-normals"]
    .open(dtype={"DATE": "string", "MLY-TAVG-NORMAL": "string"})
    .set_index("DATE")
)
print(f"Observed months: {len(observed)}; monthly normals rows: {len(normals)}")
display(observed[["STATION", "PRCP", "TAVG"]].head(3))
display(normals[["MLY-PRCP-NORMAL", "MLY-TAVG-NORMAL"]].head(3))
Observed months: 12; monthly normals rows: 12
STATION PRCP TAVG
DATE
2024-01 USW00013967 45.7 1.1
2024-02 USW00013967 31.7 9.7
2024-03 USW00013967 42.7 12.7
MLY-PRCP-NORMAL MLY-TAVG-NORMAL
DATE
01 33.5 38.2
02 36.1 42.3
03 64.8 51.2
Code · cell 4
for item in result.fetched:
    print(f"{item.asset.dataset_id}")
    print(f"Retrieved (UTC): {item.provenance.retrieved_at.isoformat()}")
    print(f"Bytes: {item.provenance.size}; cache hit: {item.from_cache}")
    print(f"Checksum: {item.provenance.checksum}")
    display(Markdown(f"[Source request](<{item.provenance.source_url}>)"))
noaa:gsom
Retrieved (UTC): 2026-09-12T05:44:28.305010+00:00
Bytes: 892; cache hit: False
Checksum: sha256:9d2617cd3f94200de386b0698c0e01afea7d84466e578c3da05b176b615c7a46

Source request

noaa:climate-normals
Retrieved (UTC): 2026-09-12T05:44:28.553808+00:00
Bytes: 918; cache hit: False
Checksum: sha256:81fe2b4e9d7368e8e9910b18ea9f69db3b4727df236a62080fcc824fe0858fb4

Source request

3. Align the months and subtract

Normals are labelled by two-digit month, so select them in the observed order and relabel them onto the observed YYYY-MM index before subtracting; a missing month raises rather than aligning to nothing. A positive temperature anomaly means the month ran warmer than its 1991-2020 average, and a positive precipitation anomaly means it was wetter.

One station's year against one 30-year baseline is not a trend, and a different baseline period would move every number. Precipitation anomalies are totals, so a single wet month dominates its sign. Missing measurements stay missing.

Code · cell 5
aligned = normals.loc[observed.index.str.slice(5, 7)].set_axis(observed.index)
fahrenheit = aligned["MLY-TAVG-NORMAL"]
if not fahrenheit.str.startswith(" ").all():
    raise ValueError("MLY-TAVG-NORMAL lost its standard-unit marker; recheck units first")
tavg_normal = (fahrenheit.astype("float64") - 32) * 5 / 9
if (observed["TAVG"] - tavg_normal).abs().max() > 10:
    raise ValueError("converted MLY-TAVG-NORMAL is implausible against observed TAVG")
prcp_normal = aligned["MLY-PRCP-NORMAL"]
frame = pd.DataFrame(
    {
        "TAVG": observed["TAVG"],
        "TAVG normal": tavg_normal,
        "TAVG anomaly": observed["TAVG"] - tavg_normal,
        "PRCP": observed["PRCP"],
        "PRCP normal": prcp_normal,
        "PRCP anomaly": observed["PRCP"] - prcp_normal,
    }
).round(1)
assert frame.notna().all().all(), "every selected month needs an observation and a normal"
display(frame)
TAVG TAVG normal TAVG anomaly PRCP PRCP normal PRCP anomaly
DATE
2024-01 1.1 3.4 -2.3 45.7 33.5 12.2
2024-02 9.7 5.7 4.0 31.7 36.1 -4.4
2024-03 12.7 10.7 2.0 42.7 64.8 -22.1
2024-04 17.3 15.2 2.1 84.7 91.4 -6.7
2024-05 21.7 20.1 1.6 91.8 134.9 -43.1
2024-06 26.7 24.9 1.8 95.2 114.0 -18.8
2024-07 27.4 27.6 -0.2 80.8 91.2 -10.4
2024-08 28.2 27.1 1.1 171.5 91.4 80.1
2024-09 23.8 22.6 1.2 54.1 94.5 -40.4
2024-10 20.3 16.2 4.1 8.6 84.3 -75.7
2024-11 11.9 9.6 2.3 196.3 42.7 153.6
2024-12 7.4 4.4 3.0 39.3 45.5 -6.2
Code · cell 6
%matplotlib inline
import matplotlib.pyplot as plt

months = frame.index.str.slice(5, 7)
fig, axes = plt.subplots(2, 1, figsize=(7, 4.6), layout="constrained", sharex=True)
for ax, column, title, unit, (above, below) in zip(
    axes,
    ["TAVG anomaly", "PRCP anomaly"],
    ["Mean temperature minus normal", "Total precipitation minus normal"],
    ["°C", "mm"],
    [("#b45631", "#2563a6"), ("#2563a6", "#b45631")],
    strict=True,
):
    values = frame[column]
    ax.bar(
        months,
        values,
        width=0.6,
        color=[above if value >= 0 else below for value in values],
    )
    ax.axhline(0, color="#444444", linewidth=0.8)
    ax.set(title=title, ylabel=unit)
    ax.margins(y=0.25)
axes[-1].set_xlabel("2024 month")
fig.suptitle("Will Rogers World Airport · 2024 anomalies against 1991-2020 normals")
plt.show()
Saved plot from Was 2024 warmer or wetter than normal?
Saved notebook output; see the surrounding analysis for units and interpretation.

What does the comparison show?

Count warmer-than-normal months and compare the twelve-month precipitation total with the sum of the monthly normals. The summary uses the unrounded values; displayed tables and plots round to one decimal place. “Normal” here means the 1991–2020 average at this station, not a threshold for statistical significance. This single year cannot establish a climate trend.

Code · cell 7
temperature_difference = observed["TAVG"] - tavg_normal
precipitation_difference = observed["PRCP"].sum() - prcp_normal.sum()
warmer_months = int((temperature_difference > 0).sum())
wetness = (
    "wetter"
    if precipitation_difference > 0
    else "drier"
    if precipitation_difference < 0
    else "equal to normal"
)
print(f"{warmer_months} of 12 months had a higher mean temperature than normal.")
print(f"2024 precipitation: {observed['PRCP'].sum():.1f} mm; normal: {prcp_normal.sum():.1f} mm.")
print(f"The year was {wetness}: {precipitation_difference:+.1f} mm relative to normal.")
print("Scope: Will Rogers World Airport, 2024, compared with 1991-2020 monthly normals.")
10 of 12 months had a higher mean temperature than normal.
2024 precipitation: 942.4 mm; normal: 924.3 mm.
The year was wetter: +18.1 mm relative to normal.
Scope: Will Rogers World Airport, 2024, compared with 1991-2020 monthly normals.

4. Verify, and handle a stale lockfile

Verification is offline: it checks the manifest and cached bytes against the lockfile. A repeat pull then restores both pinned assets from that lockfile and the cache.

Neither source is a stable filename. Both are query-shaped URLs that the agencies revise: NCEI revises GSOM as late station reports arrive, and the 1991-2020 normals files are corrected occasionally even though their period is fixed. The same URL can return different bytes months later. Restoration then exits 4, reports every asset whose bytes changed, and rewrites nothing. Accept the new bytes for the named entries with usdata pull dataset.yaml --update noaa:gsom, or with pull(manifest, update=["noaa:gsom"]) in Python; see --update in the manifest reference. This notebook does not simulate that drift.

Keep the manifest, the lockfile, and the cached bytes together in your own analysis repository. A checksum detects a revision but cannot recreate bytes upstream no longer serves. The SDK checkout ignores generated example lockfiles.

Code · cell 8
drift = verify(manifest)
assert not drift, drift
again = pull(manifest)
print(f"Verification passed: {not drift}")
print(f"Restored from lockfile: {again.from_lockfile}")
print(f"All assets came from cache: {all(value.from_cache for value in again.fetched)}")
Verification passed: True
Restored from lockfile: True
All assets came from cache: True