06 · From cold observations to event evidence¶
Context¶
A threshold is useful when its meaning is explicit. Here, severe cold means an observed PRISM daily minimum below −10 °C.
Question¶
Where did severe cold persist for at least two days, and how synchronized was its occurrence with the center of the study region?
Analysis story¶
We move from temperature to states, from states to events, and from states to a synchrony map. Each transition is one named verb.
Data used in this lesson¶
Every value comes from the PRISM Group at Oregon State University's AN91d daily 4 km climate product. This repository carries a small Boulder-region extract for 1–30 January 2024 so the lesson runs offline without replacing observations with generated values. The data validation page records source URLs, terms, checksums, bounds, units, and acceptance tests.
# Record the exact code imported by this notebook kernel.
import cubedynamics as cd
print(cd.version_info())
CubeDynamics 0.1.0rc3 Artifact: development checkout Code: git:a852137944cabf238e9321f2eecf09e483c9b9f5 Imported from: /home/runner/work/cubedynamics/cubedynamics/src/cubedynamics Distribution: /opt/hostedtoolcache/Python/3.11.16/x64/lib/python3.11/site-packages
Prepare · Keep the threshold next to its units¶
from pathlib import Path
import xarray as xr
# Find the repository from either a root-level documentation build or a kernel
# started beside this notebook, then open the checksum-controlled PRISM extract.
data_path = next(
candidate / "tests" / "fixtures" / "real_data" / "prism_boulder_january_2024.nc"
for candidate in (Path.cwd(), *Path.cwd().parents)
if (candidate / "tests" / "fixtures" / "real_data" / "prism_boulder_january_2024.nc").exists()
)
prism = xr.open_dataset(data_path, engine="scipy").load()
# These assertions are part of the teaching contract: official source,
# canonical cube dimensions, complete daily time, and declared Celsius units.
assert prism.attrs["source"] == "PRISM Group, Oregon State University"
assert prism.attrs["is_synthetic"] == 0
assert prism.sizes == {"time": 30, "y": 24, "x": 24}
assert prism["tmax"].attrs["units"] == "degC"
cube = prism["tmin"]
assert cube.attrs["units"] == "degC"
Pipes · Keep state, event, and synchrony questions separate¶
from cubedynamics import pipe, verbs as v
severe_cold = (
pipe(cube)
| v.threshold_state(threshold=-10.0, direction="below", name="severe_cold")
).unwrap()
events = (pipe(severe_cold) | v.detect_events(min_duration=2)).unwrap()
# One row is one persistent event at one PRISM cell, not one regional cold wave.
assert events.event_scope == "local_cell"
regional_episodes = (
pipe(events)
| v.consolidate_events(spatial_relation="neighbors", max_gap="1D")
).unwrap()
event_summary = (
pipe(regional_episodes)
| v.event_metrics(period="all", metrics=("event_count", "mean_duration", "max_duration"))
).unwrap()
synchrony = (
pipe(severe_cold)
| v.occurrence_synchrony(spatial_mode="reference", reference="center")
).unwrap()
assert len(events.catalog) > 0
assert regional_episodes.event_scope == "regional_episode"
Figure · Follow the evidence from values to events¶
import matplotlib.pyplot as plt
event_count = events.dataset["event_active"].sum("time")
reference_sync = synchrony["occurrence_synchrony"].isel(time_window_end=0)
fig, axes = plt.subplots(2, 2, figsize=(11, 8), constrained_layout=True)
cube.isel(time=15).plot(ax=axes[0, 0], cmap="coolwarm", cbar_kwargs={"label": "°C"})
axes[0, 0].set_title("Observed minimum temperature · 16 January")
severe_cold["state"].isel(time=15).plot(ax=axes[0, 1], cmap="Blues", add_colorbar=False)
axes[0, 1].set_title("Below −10 °C state")
event_count.plot(ax=axes[1, 0], cmap="magma", cbar_kwargs={"label": "Event days"})
axes[1, 0].set_title("Days retained in ≥2-day events")
reference_sync.plot(ax=axes[1, 1], cmap="viridis", vmin=0, vmax=1)
axes[1, 1].set_title("Occurrence synchrony with center cell")
plt.show()
What the figure tells us¶
The threshold isolates the observed mid-January outbreak. events.catalog
counts local cell instances; it is not a count of independent regional cold
waves. consolidate_events makes the temporal-gap and spatial-neighbor choices
explicit before producing regional episodes. Duration filtering distinguishes
persistence, while synchrony shows where local timing matched the region's
center.
Try the next variation¶
Change only the threshold to −15 °C and compare retained events.
Data used¶
| Field | Frozen analysis input |
|---|---|
| Provider | PRISM Group, Oregon State University |
| Product | AN91d daily 4 km time series |
| Dates | 2024-01-01 to 2024-01-30 |
| Fixture | tests/fixtures/real_data/prism_boulder_january_2024.nc |
| Provenance record | tests/fixtures/real_data/prism_boulder_january_2024.provenance.json |
The PRISM source reference describes current catalog support; the fixture record above identifies the observations used here. Data validation documents checksums and acceptance checks. The analytical baseline and thresholds belong to this story, not the provider.
Reproduce¶
Clone the repository, then run these commands from its root:
python -m pip install -e ".[vignettes]"
python scripts/run_vignettes.py docs/vignettes/states_and_events.ipynb
No network is needed after installation. Open the downloaded notebook in
Jupyter and run all cells to see the same figures. The website executes these
cells during its strict build. Environment setup
and the vignette contract explain the workflow.
The first code cell prints cd.version_info() so a rendered result can be tied
to a package path and, for development checkouts, a Git commit.
See also¶
temperature · precipitation · consolidate_events · detect_events · event_metrics · occurrence_synchrony · threshold_state