From ac1d154faee6a597413a070021e45f30e557039c Mon Sep 17 00:00:00 2001 From: chross22 <52218551+chross22@users.noreply.github.com> Date: Fri, 7 Aug 2026 15:48:21 -0400 Subject: [PATCH] Record what each climate index is measured in The catalog gave every index a label and a description but no units, so nothing said that AMOC is in Sverdrups while NAO is in standard deviations. In a table of model coefficients those look comparable and are not. AMOC Sv AMO degrees C NAO standardized anomaly AO standardized anomaly PDO standardized anomaly LCR fraction The standardized ones say so rather than being left blank or called dimensionless. "Unitless" would be true and useless; what a reader needs to know is that the number is in standard deviations, which is why a coefficient fitted to it means something different. Assigned from the data rather than from memory: AMO ranges about -0.2 to 0.5 with sd 0.15 over 2000-2010, which is degrees C, while PDO ranges about -2 to 2 with sd 0.93, which is standardized. The two look similar in a catalog listing and are not the same kind of number. Surfaced in index_dictionary(), in its printed output, and in the README table alongside the record each index covers. Co-Authored-By: Claude Opus 5 --- NEWS.md | 6 ++++++ R/climate_indices.R | 21 ++++++++++++++----- README.Rmd | 21 ++++++++++++------- README.md | 21 ++++++++++++------- man/climate_indices.Rd | 4 ++-- tests/testthat/test-climate-indices.R | 30 +++++++++++++++++++++++++++ 6 files changed, 80 insertions(+), 23 deletions(-) diff --git a/NEWS.md b/NEWS.md index ee92a90..95f9096 100644 --- a/NEWS.md +++ b/NEWS.md @@ -173,6 +173,12 @@ * **`LCR`**, the Labrador Current retroflection index, joins the climate index catalog. Published with Jutras et al. (2023) and cited in `index_dictionary()`. +* **Every climate index now declares its `units`**, surfaced in + `index_dictionary()`. Most are standardized anomalies, in standard deviations + rather than anything physical; only `AMO` (degrees C) and `AMOC` (Sverdrups) + carry real units. A coefficient fitted to one of those is not comparable with + one fitted to `NAO`, and a table of model output gave no way to tell. + * **`AMOC`**, the overturning transport measured by the RAPID array at 26.5°N, joins it too. Unlike the other indices this is a direct measurement rather than a pressure or SST pattern — which is also its limitation: it begins in April diff --git a/R/climate_indices.R b/R/climate_indices.R index 7b539c6..bb44a9f 100644 --- a/R/climate_indices.R +++ b/R/climate_indices.R @@ -10,8 +10,8 @@ #' year?") and useless for spatial ones. A model given only indices cannot #' produce a map. #' -#' @return a named list, one entry per index, each with `label`, `source`, -#' `url`, and `description` +#' @return a named list, one entry per index, each with `label`, `units`, +#' `source`, `url`, and `description` #' @examples #' names(climate_indices()) #' @seealso [fetch_climate_index()], [attach_climate_index()] @@ -20,6 +20,7 @@ climate_indices <- function() { list( NAO = list( label = "North Atlantic Oscillation", + units = "standardized anomaly", source = "NOAA CPC", url = "https://www.cpc.ncep.noaa.gov/products/precip/CWlink/pna/norm.nao.monthly.b5001.current.ascii.table", format = "cpc_table", @@ -31,6 +32,7 @@ climate_indices <- function() { ), AO = list( label = "Arctic Oscillation", + units = "standardized anomaly", source = "NOAA CPC", url = "https://www.cpc.ncep.noaa.gov/products/precip/CWlink/daily_ao_index/monthly.ao.index.b50.current.ascii.table", format = "cpc_table", @@ -40,6 +42,7 @@ climate_indices <- function() { ), AMO = list( label = "Atlantic Multidecadal Oscillation", + units = "degrees C", source = "NOAA PSL", url = "https://psl.noaa.gov/data/correlation/amon.us.long.data", format = "psl_table", @@ -50,6 +53,7 @@ climate_indices <- function() { ), PDO = list( label = "Pacific Decadal Oscillation", + units = "standardized anomaly", source = "NOAA PSL", url = "https://psl.noaa.gov/data/correlation/pdo.data", format = "psl_table", @@ -60,6 +64,7 @@ climate_indices <- function() { ), LCR = list( label = "Labrador Current retroflection", + units = "fraction", source = "Jutras et al. 2023, Nature Communications", url = paste0("https://static-content.springer.com/esm/", "art%3A10.1038%2Fs41467-023-38321-y/MediaObjects/", @@ -83,6 +88,7 @@ climate_indices <- function() { ), AMOC = list( label = "Atlantic Meridional Overturning Circulation", + units = "Sv", source = "RAPID-MOCHA-WBTS array at 26.5N", url = "https://rapid.ac.uk/sites/default/files/rapid_data/moc_transports.nc", format = "rapid_netcdf", @@ -118,8 +124,13 @@ index_dictionary <- function() { catalog <- climate_indices() dictionary <- do.call(rbind, lapply(names(catalog), function(name) { entry <- catalog[[name]] - data.frame(name = name, label = entry$label, source = entry$source, - url = entry$url, + data.frame(name = name, label = entry$label, + # Most of these are standardized anomalies, which is a unit in + # the sense that matters: it says the number is in standard + # deviations rather than in anything physical, so a coefficient + # fitted to it is not comparable with one fitted to AMOC. + units = entry$units %||% NA_character_, + source = entry$source, url = entry$url, # Indices published with a paper carry its citation. The # operational ones from NOAA have no single paper to point at, # so this is empty for them rather than invented. @@ -139,7 +150,7 @@ print.datamatch_index_dictionary <- function(x, ...) { flat <- as.data.frame(x) cat("Climate indices available by name\n") cat(strrep("-", 62), "\n", sep = "") - print(flat[c("name", "label", "source")], row.names = FALSE, right = FALSE) + print(flat[c("name", "label", "units", "source")], row.names = FALSE, right = FALSE) cat("\nThese have no spatial dimension: one value per month, basin-wide.\n") cited <- flat[!is.na(flat$reference), ] diff --git a/README.Rmd b/README.Rmd index b876af4..0f8399a 100644 --- a/README.Rmd +++ b/README.Rmd @@ -701,14 +701,19 @@ useless for spatial ones. A model given only indices cannot produce a map. Six are available, and they are not interchangeable. -| Name | What it measures | Timescale | Record | -|---|---|---|---| -| `NAO` | Pressure difference, Icelandic Low to Azores High | Year to year | 1950– | -| `AO` | Strength of the polar vortex | Year to year | 1950– | -| `AMO` | Detrended North Atlantic SST anomaly | Multidecadal | 1948– | -| `PDO` | Leading mode of North Pacific SST | Decadal | 1948– | -| `LCR` | Retroflection of the Labrador Current | Year to year | 1993–2014 | -| `AMOC` | Overturning transport at 26.5°N | Year to year | 2004– | +| Name | What it measures | Units | Timescale | Record | +|---|---|---|---|---| +| `NAO` | Pressure difference, Icelandic Low to Azores High | standardized anomaly | Year to year | 1950– | +| `AO` | Strength of the polar vortex | standardized anomaly | Year to year | 1950– | +| `AMO` | Detrended North Atlantic SST anomaly | degrees C | Multidecadal | 1948– | +| `PDO` | Leading mode of North Pacific SST | standardized anomaly | Decadal | 1948– | +| `LCR` | Retroflection of the Labrador Current | fraction | Year to year | 1993–2014 | +| `AMOC` | Overturning transport at 26.5°N | Sv | Year to year | 2004– | + +Most are **standardized anomalies** — in standard deviations, not anything +physical. Only `AMO` (degrees C) and `AMOC` (Sverdrups) carry real units, so a +coefficient fitted to one of those is not comparable with one fitted to `NAO`. +`index_dictionary()` carries the units at runtime. **`NAO`** is the usual first choice in the Northwest Atlantic. It sets the strength and track of the westerlies, and with them heat flux and mixing over the diff --git a/README.md b/README.md index 6a04a24..7877926 100644 --- a/README.md +++ b/README.md @@ -780,14 +780,19 @@ useless for spatial ones. A model given only indices cannot produce a map. Six are available, and they are not interchangeable. -| Name | What it measures | Timescale | Record | -|---|---|---|---| -| `NAO` | Pressure difference, Icelandic Low to Azores High | Year to year | 1950– | -| `AO` | Strength of the polar vortex | Year to year | 1950– | -| `AMO` | Detrended North Atlantic SST anomaly | Multidecadal | 1948– | -| `PDO` | Leading mode of North Pacific SST | Decadal | 1948– | -| `LCR` | Retroflection of the Labrador Current | Year to year | 1993–2014 | -| `AMOC` | Overturning transport at 26.5°N | Year to year | 2004– | +| Name | What it measures | Units | Timescale | Record | +|---|---|---|---|---| +| `NAO` | Pressure difference, Icelandic Low to Azores High | standardized anomaly | Year to year | 1950– | +| `AO` | Strength of the polar vortex | standardized anomaly | Year to year | 1950– | +| `AMO` | Detrended North Atlantic SST anomaly | degrees C | Multidecadal | 1948– | +| `PDO` | Leading mode of North Pacific SST | standardized anomaly | Decadal | 1948– | +| `LCR` | Retroflection of the Labrador Current | fraction | Year to year | 1993–2014 | +| `AMOC` | Overturning transport at 26.5°N | Sv | Year to year | 2004– | + +Most are **standardized anomalies** — in standard deviations, not anything +physical. Only `AMO` (degrees C) and `AMOC` (Sverdrups) carry real units, so a +coefficient fitted to one of those is not comparable with one fitted to `NAO`. +`index_dictionary()` carries the units at runtime. **`NAO`** is the usual first choice in the Northwest Atlantic. It sets the strength and track of the westerlies, and with them heat flux and mixing over the diff --git a/man/climate_indices.Rd b/man/climate_indices.Rd index 77a20ee..89b0f50 100644 --- a/man/climate_indices.Rd +++ b/man/climate_indices.Rd @@ -7,8 +7,8 @@ climate_indices() } \value{ -a named list, one entry per index, each with \code{label}, \code{source}, -\code{url}, and \code{description} +a named list, one entry per index, each with \code{label}, \code{units}, +\code{source}, \code{url}, and \code{description} } \description{ Unlike the gridded variables, these have \strong{no spatial dimension} — one value diff --git a/tests/testthat/test-climate-indices.R b/tests/testthat/test-climate-indices.R index 233f862..87e1b89 100644 --- a/tests/testthat/test-climate-indices.R +++ b/tests/testthat/test-climate-indices.R @@ -542,3 +542,33 @@ test_that("the overturning index carries its citation", { expect_true(any(grepl("Cite when used", output))) expect_true(any(grepl("AMOC", output))) }) + +test_that("every index declares its units", { + # A coefficient fitted to AMOC is in Sverdrups; one fitted to NAO is per + # standard deviation. Without units recorded, the two look comparable in a + # table of model output and are not. + catalog <- climate_indices() + + for (name in names(catalog)) { + expect_true(nzchar(catalog[[name]]$units %||% ""), + info = paste(name, "has no units")) + } + + # The physical ones say what they are; the standardized ones say that they + # are standardized rather than pretending to be dimensionless. + expect_equal(catalog$AMOC$units, "Sv") + expect_equal(catalog$AMO$units, "degrees C") + expect_equal(catalog$NAO$units, "standardized anomaly") +}) + +test_that("units reach the dictionary and its printed output", { + dictionary <- as.data.frame(index_dictionary()) + + expect_true("units" %in% names(dictionary)) + expect_false(anyNA(dictionary$units)) + expect_equal(dictionary$units[dictionary$name == "AMOC"], "Sv") + + output <- capture.output(print(index_dictionary())) + expect_true(any(grepl("units", output))) + expect_true(any(grepl("Sv", output))) +})