05 · One cube, six analytical views¶
Context¶
Verbs should be small enough to understand independently and consistent enough to combine. Every panel answers a real question about the same observations.
Question¶
How do summaries, departures, standardization, clipping, and reshaping reveal different aspects of January minimum temperature?
Analysis story¶
Each result begins from the same PRISM cube, so the verb remains the focus.
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.
Prepare · Use the reviewed minimum-temperature cube¶
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"]
cube
<xarray.DataArray 'tmin' (time: 30, y: 24, x: 24)> Size: 69kB
array([[[-12.188, -13.476, -13.069, ..., -9.204, -9.268, -9.498],
[-13.195, -14.14 , -13.613, ..., -9.415, -9.505, -9.626],
[-14.016, -13.577, -10.611, ..., -8.292, -8.478, -8.844],
...,
[-10.418, -9.994, -10.626, ..., -4.607, -4.986, -4.598],
[-10.427, -9.88 , -12.208, ..., -3.931, -4.526, -4.57 ],
[-10.078, -9.446, -9.453, ..., -3.835, -4.516, -4.652]],
[[-12.711, -13.747, -13.724, ..., -9.146, -9.143, -9.422],
[-13.55 , -14.726, -14.628, ..., -9.388, -9.465, -9.602],
[-14.549, -14.068, -10.487, ..., -8.216, -8.343, -8.696],
...,
[-11.672, -10.951, -11.892, ..., -3.246, -3.98 , -3.573],
[-11.611, -10.972, -13.579, ..., -2.537, -3.633, -3.678],
[-10.951, -10.45 , -10.52 , ..., -2.688, -3.553, -3.716]],
[[-15.221, -15.365, -15.136, ..., -8.747, -8.831, -9.081],
[-15.587, -16.217, -15.803, ..., -9.08 , -9.21 , -9.402],
[-16.21 , -15.599, -12.361, ..., -7.682, -8.062, -8.817],
...,
...
[-15.264, -14.601, -15.351, ..., -3.668, -4.375, -3.836],
[-14.747, -14.491, -16.132, ..., -3.102, -4.135, -4.08 ],
[-12.782, -13.469, -13.907, ..., -3.27 , -4.243, -4.357]],
[[-11.677, -11.301, -10.205, ..., -5.408, -5.402, -5.733],
[-11.544, -11.393, -10.52 , ..., -5.844, -5.928, -6.085],
[-11.615, -10.347, -6.72 , ..., -4.701, -4.819, -5.219],
...,
[ -9.272, -8.815, -9.667, ..., -0.4 , -0.951, -0.315],
[ -8.766, -8.534, -10.758, ..., 0.55 , -0.551, -0.491],
[ -7.597, -7.511, -7.857, ..., 0.83 , -0.108, -0.513]],
[[ -9.353, -11.372, -11.214, ..., -3.98 , -4.007, -4.333],
[-10.997, -12.387, -11.686, ..., -4.382, -4.498, -4.688],
[-12.303, -12.28 , -9.418, ..., -3.009, -3.235, -3.81 ],
...,
[ -8.704, -8.71 , -8.871, ..., -0.223, -0.544, -0.061],
[ -9.138, -8.464, -10.052, ..., 0.803, -0.241, -0.16 ],
[ -9.405, -8.619, -8.297, ..., 0.711, -0.126, -0.224]]],
dtype=float32)
Coordinates:
* time (time) datetime64[ns] 240B 2024-01-01 2024-01-02 ... 2024-01-30
* y (y) float64 192B 40.5 40.46 40.42 40.37 ... 39.67 39.62 39.58 39.54
* x (x) float64 192B -105.7 -105.7 -105.7 ... -104.9 -104.8 -104.8
Attributes:
long_name: daily minimum air temperature
standard_name: air_temperature
units: degC
source_variable: PRISM tmin
is_synthetic: 0Pipes · Build a small, auditable verb gallery¶
from cubedynamics import pipe, verbs as v
temporal_mean = (pipe(cube) | v.mean(dim="time")).unwrap()
temporal_variance = (pipe(cube) | v.variance(dim="time")).unwrap()
anomaly = (pipe(cube) | v.anomaly(dim="time")).unwrap()
standardized = (pipe(cube) | v.zscore(dim="time")).unwrap()
bounded = (pipe(standardized) | v.apply(lambda x: x.clip(-2, 2))).unwrap()
flat = (pipe(cube) | v.flatten_cube()).unwrap()
# Flattening changes layout, not the number of observed spatial samples.
assert flat.sizes["sample"] == cube.sizes["y"] * cube.sizes["x"]
Figure · Compare what each verb preserves and changes¶
import matplotlib.pyplot as plt
fig, axes = plt.subplots(2, 3, figsize=(12, 7), constrained_layout=True)
temporal_mean.plot(ax=axes[0, 0], cmap="coolwarm", cbar_kwargs={"label": "°C"})
axes[0, 0].set_title("Mean through time")
temporal_variance.plot(ax=axes[0, 1], cmap="viridis", cbar_kwargs={"label": "°C²"})
axes[0, 1].set_title("Variance through time")
anomaly.isel(time=15).plot(ax=axes[0, 2], cmap="RdBu_r", center=0)
axes[0, 2].set_title("Anomaly · 16 January")
standardized.isel(y=12, x=12).plot(ax=axes[1, 0], color="#356d76")
axes[1, 0].set_title("Standardized site history")
bounded.isel(time=15).plot(ax=axes[1, 1], cmap="RdBu_r", vmin=-2, vmax=2)
axes[1, 1].set_title("Custom clipped z-score")
flat.mean("sample").plot(ax=axes[1, 2], color="#6b6544")
axes[1, 2].set_title("Flattened regional mean")
plt.show()
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/verbs_gallery.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.
See also¶
temperature · precipitation · anomaly · apply · flatten_cube · mean · variance · zscore