Version comparison: Program-Area score equivalence

Parameterized v7 ↔︎ v8 (or any two versions) composite-score delta report

Published

2026-07-16

Not a pipeline stage — a standalone, parameterized report (no msens: target block, so tar_make() never runs it). Render it on demand for any two release versions whose sdm.duckdb are on disk. The reusable comparison core lives in msens (score_delta, score_delta_summary, assert_within_tolerance, pra_score_delta) so this notebook only orchestrates — the math is unit-tested in msens/tests/testthat/test-validate.R and can’t drift from what’s reported here.

# render with version suffixes in the output filename (see scripts/render_validate.R):
source("scripts/render_validate.R"); render_versions("v7", "v8")   # -> _output/validate_v7_v8.html
# or directly:
quarto render validate_versions.qmd -P version_a:v7 -P version_b:v8 --output validate_v7_v8.html

Scope note: the nc / gm density surfaces are not folded into either version’s scores (v8 built with MERGE_FOLD_DENSITY off — density is published but excluded from the merge), so this compares the same extinction-risk composite on each version’s species set.

1 Setup

Code
librarian::shelf(DBI, duckdb, dplyr, ggplot2, glue, knitr, scales,
                 MarineSensitivity/msens, quiet = TRUE)
source(here::here("libs/paths.R"))

va <- params$version_a
vb <- params$version_b
mk <- params$metric_key

# resolve each version's sdm.duckdb by swapping the version segment of the configured path
sdm_for <- function(v) sub(glue("/{ver}/"), glue("/{v}/"), path.expand(sdm_db))
db_a <- sdm_for(va); db_b <- sdm_for(vb)
stopifnot(file.exists(db_a), file.exists(db_b))

con_a <- dbConnect(duckdb(), db_a, read_only = TRUE)
con_b <- dbConnect(duckdb(), db_b, read_only = TRUE)

cat(glue("Comparing **{va}** ← → **{vb}**  \n",
         "- A ({va}): `{db_a}`  \n",
         "- B ({vb}): `{db_b}`  \n",
         "- composite metric: `{mk}`\n"))
Comparing **v7** ← → **v8**  
- A (v7): `/Users/bbest/_big/msens/derived/v7/sdm.duckdb`  
- B (v8): `/Users/bbest/_big/msens/derived/v8/sdm.duckdb`  
- composite metric: `score_extriskspcat_primprod_ecoregionrescaled_equalweights`

2 Overall Program-Area composite delta

The headline equivalence gate: the composite score (score_extriskspcat_primprod_ecoregionrescaled_equalweights) per BOEM Program Area, in each version, inner-joined so only Program Areas present in both are compared. delta = {vb} − {va}.

Code
d <- msens::pra_score_delta(con_a, con_b, metric_key = mk, labels = c(va, vb))
s <- msens::score_delta_summary(d)

# soft equivalence verdict — report, never hard-stop (a report must render either way)
gate <- tryCatch(
  { msens::assert_within_tolerance(d, mean_tol = params$mean_tol, max_tol = params$max_tol); "PASS" },
  error = function(e) conditionMessage(e))

knitr::kable(
  tibble::tibble(
    metric = c("Program Areas compared", glue("mean |Δ|"), glue("max |Δ|"), "RMSE",
               glue("tolerance (mean / max)"), "verdict"),
    value  = c(s$n, round(s$mean_abs, 3), round(s$max_abs, 3), round(s$rmse, 3),
               glue("{params$mean_tol} / {params$max_tol}"),
               ifelse(gate == "PASS", "✅ within tolerance", paste0("⚠️ ", gate)))),
  caption = glue("Composite-score equivalence, {va} → {vb}"))
Composite-score equivalence, v7 → v8
metric value
Program Areas compared 20
mean |Δ| 4.119
max |Δ| 10.289
RMSE 5.175
tolerance (mean / max) 5 / 15
verdict ✅ within tolerance
Code
va_col <- glue("score_{va}"); vb_col <- glue("score_{vb}")
d_plot <- d |>
  mutate(programarea_key = factor(programarea_key, levels = programarea_key[order(delta)]),
         sign = ifelse(delta >= 0, glue("higher in {vb}"), glue("higher in {va}")))

ggplot(d_plot, aes(delta, programarea_key, fill = sign)) +
  geom_col() +
  geom_vline(xintercept = 0, linewidth = 0.3) +
  scale_fill_manual(values = setNames(c("#20c997", "#e8590c"),
                                      c(glue("higher in {vb}"), glue("higher in {va}")))) +
  labs(x = glue("Δ composite score  ({vb} − {va})"), y = NULL, fill = NULL,
       title = glue("Program-Area composite-score change: {va} → {vb}")) +
  theme_minimal(base_size = 12) +
  theme(legend.position = "top")

Code
d |>
  transmute(`Program Area` = programarea_key,
            !!va := round(.data[[va_col]], 2),
            !!vb := round(.data[[vb_col]], 2),
            `Δ` = round(delta, 2),
            `Δ %` = round(100 * delta / .data[[va_col]], 1)) |>
  knitr::kable(caption = glue("Per-Program-Area composite scores ({va} vs {vb}), sorted by |Δ|"))
Per-Program-Area composite scores (v7 vs v8), sorted by |Δ|
Program Area v7 v8 Δ Δ %
SHU 29.68 19.39 -10.29 -34.7
KOD 32.09 22.19 -9.91 -30.9
GAB 25.68 18.14 -7.55 -29.4
GOA 39.48 32.08 -7.40 -18.8
SOC 29.68 22.58 -7.09 -23.9
NOC 20.71 14.33 -6.38 -30.8
GEO 33.92 28.94 -4.99 -14.7
CEC 21.87 17.01 -4.85 -22.2
NAV 27.32 22.60 -4.71 -17.3
COK 52.62 47.93 -4.69 -8.9
HOP 52.66 48.43 -4.23 -8.0
ALB 12.28 14.94 2.66 21.7
CHU 24.89 23.11 -1.78 -7.2
HAR 7.52 9.28 1.76 23.4
ALA 28.28 30.01 1.73 6.1
BFT 11.29 12.38 1.09 9.7
BOW 18.45 19.08 0.63 3.4
NOR 29.01 29.35 0.33 1.1
GAA 33.09 32.87 -0.22 -0.7
MAT 24.91 24.85 -0.06 -0.3

3 Per-category breakdown

The same delta, computed for each species-category ecoregion-rescaled extinction-risk metric that exists in both versions — so a shift in the composite can be attributed to a category (bird, fish, mammal, …) rather than read as one opaque number. Uses the same msens::score_delta core per metric.

Code
# metrics shared by both versions, of the per-category ecoregion-rescaled family
metrics_in <- function(con) DBI::dbGetQuery(con, "SELECT metric_key FROM metric")$metric_key
shared <- intersect(metrics_in(con_a), metrics_in(con_b))
cat_metrics <- grep("^extrisk_[a-z_]+_ecoregion_rescaled$", shared, value = TRUE)

# reuse the schema-adaptive core once per metric (handles the v7 `value` / v8 `val` rename)
cat_summary <- purrr::map_dfr(cat_metrics, function(metric) {
  dd <- msens::pra_score_delta(con_a, con_b, metric_key = metric, labels = c(va, vb))
  ss <- msens::score_delta_summary(dd)
  tibble::tibble(
    category = sub("^extrisk_(.*)_ecoregion_rescaled$", "\\1", metric),
    n = ss$n, `mean |Δ|` = round(ss$mean_abs, 3),
    `max |Δ|` = round(ss$max_abs, 3), rmse = round(ss$rmse, 3))
}) |> arrange(desc(`mean |Δ|`))

knitr::kable(cat_summary,
  caption = glue("Per-category ecoregion-rescaled extinction-risk delta, {va} → {vb}"))
Per-category ecoregion-rescaled extinction-risk delta, v7 → v8
category n mean |Δ| max |Δ| rmse
bird 20 20.089 54.560 26.167
mammal 20 9.890 22.358 11.408
turtle 11 8.616 19.747 11.058
fish 20 7.712 27.407 10.660
coral 20 0.842 5.566 1.445
invertebrate 20 0.827 2.212 1.050