This notebook cleans and analyses the Round-2 Expert Elicitation (EE) export to produce policy-relevant diagnostics of:
The survey is structured around taxa × habitat group
combinations. Some taxa are represented by multiple
survey_taxa rows within the same habitat group
(notably freshwater assemblages and lichens). To make sure headline
results are not an artefact of that survey-row multiplicity, the
analysis reports key summaries under two transparent aggregation
assumptions:
survey_taxa
rows contribute as-is (faithful to survey structure).Where A and B agree, conclusions are robust to the aggregation choice; where they differ, results should be interpreted as sensitive to how survey rows represent ecological “units”.
This analysis is structured as expert × taxa:habitat (survey row) × scenario observations.
Policy magnitudes (Q2) are explicitly defined as changes relative to the counterfactual (Q1). Therefore the implied signed outcome under policy is:
\[ \text{Absolute policy outcome (signed \%)} = \text{Counterfactual signed \%} + \text{Policy delta signed \%} \]
This ensures we keep separate:
Example (signed magnitudes): - If Q1b = 20 and Q1a indicates “decrease” → counterfactual = −20% - If Q2b = 10 and Q2a indicates “increase” → delta = +10% - Absolute outcome under policy = −20 + 10 = −10%
In the code, this quantity is stored as:
abs_policy_mag_signed = cf_mag_signed + pol_mag_signed
Outputs include summary tables, compact plots, category distributions, and a panel-size sensitivity test (resampling stability).
# Packages
# ============================================================
library(readxl)
library(dplyr)
library(tidyr)
library(stringr)
library(purrr)
library(readr)
library(ggplot2)
library(forcats)
library(knitr)
library(kableExtra)
library(scales)
# ------------------------------------------------------------
# Input file - update 2 March 2026
# ------------------------------------------------------------
#xlsx_path <- "./data/p591243814707_benjamin.firth_38179087.xlsx"
xlsx_path <- "./data/p591243814707_benjamin.firth_38179087_update_260302.xlsx"
df_raw <- read_excel(xlsx_path)
tibble(
n_rows = nrow(df_raw),
n_cols = ncol(df_raw),
n_Q_cols = sum(str_detect(names(df_raw), "^Q\\d"))
) %>%
kable(caption = "Raw Excel shape") %>%
kable_styling(full_width = FALSE)
| n_rows | n_cols | n_Q_cols |
|---|---|---|
| 7 | 445 | 420 |
Some respondent cells contain HTML tags
(e.g. <span style="...">No meaningful change</span>).
We remove tags while preserving the displayed text.
Some, but not all respondent cells contain HTML (e.g. No meaningful change). Remove tags while preserving text:
strip_html <- function(x) {
if (is.character(x) || inherits(x, "factor")) {
x2 <- as.character(x)
x2 <- str_replace_all(x2, "<[^>]+>", "")
str_squish(x2)
} else {
x
}
}
df_clean <- df_raw %>%
mutate(across(everything(), strip_html))
id_gj) mapped from
Contact_name (keep all experts)Two respondents are missing some metadata but do have
Contact_name. To keep all experts while removing
identifiable information, we assign a stable integer ID (1..N) based on
Contact_name and do not carry forward names.
df_clean <- df_clean %>%
mutate(Contact_name = str_squish(as.character(Contact_name)))
contact_key <- df_clean %>%
distinct(Contact_name) %>%
filter(!is.na(Contact_name) & Contact_name != "") %>%
arrange(Contact_name) %>%
mutate(id_gj = row_number())
# save if needed later
#write.csv(contact_key, "./data/respondent_key_lookup.csv", row.names = FALSE)
# Expert mapping: Contact_name → id_gj
df1 <- df_clean %>%
left_join(contact_key, by = "Contact_name")
df_tidy (expert ×
survey_taxa × scenario)We pivot all Q* columns into long format, parse
scenario, then pivot wide.
Parsing rules:
Q1* = "counterfactual" scenarioQ2*_*_{1..4} =
"policy_1".."policy_4" scenariodf_q <- df1 %>%
select(id_gj, matches("^Q\\d"))
long_q <- df_q %>%
pivot_longer(
cols = matches("^Q\\d"),
names_to = "colname",
values_to = "value",
values_transform = list(value = as.character) # avoid type conflicts
) %>%
mutate(
q_code = str_match(colname, "^(Q\\d+[a-z])_\\d+")[,2],
idx = as.integer(str_match(colname, "^(Q\\d+[a-z])_(\\d+)")[,3]),
scen_num = str_match(colname, "^(Q\\d+[a-z])_\\d+_(\\d+)")[,3],
scen_num = if_else(is.na(scen_num), NA_integer_, as.integer(scen_num)),
scenario = case_when(
str_detect(q_code, "^Q1") ~ "counterfactual",
str_detect(q_code, "^Q2") & !is.na(scen_num) ~ paste0("policy_", scen_num),
TRUE ~ NA_character_
),
question = str_trim(str_match(colname, ":\\s*([^\\(]+)\\s*\\(")[,2]),
survey_taxa = str_trim(str_match(colname, "\\((.*)\\)\\s*$")[,2])
) %>%
mutate(
col_out = paste0(
q_code, "_",
str_replace_all(str_to_lower(question), "[^a-z0-9]+", "_") %>%
str_replace_all("^_|_$", "")
)
) %>%
filter(!is.na(scenario)) %>%
select(id_gj, survey_taxa, scenario, col_out, value)
# reshape_wide
df_tidy <- long_q %>%
pivot_wider(
id_cols = c(id_gj, survey_taxa, scenario),
names_from = col_out,
values_from = value,
values_fn = ~ {
if (length(.x) == 0) NA_character_
else if (length(.x) == 1) .x
else paste(.x, collapse = " | ")
}
) %>%
arrange(id_gj, survey_taxa, scenario)
df_tidy %>%
count(scenario) %>%
kable(caption = "Scenario counts in df_tidy") %>%
kable_styling(full_width = FALSE)
| scenario | n |
|---|---|
| counterfactual | 147 |
| policy_1 | 147 |
| policy_2 | 147 |
| policy_3 | 147 |
| policy_4 | 147 |
habitat parsingGoal: 1. Keep habitat_group logic
exactly as-is (broad keyword mapping).
2. Set habitat_type to NA for all rows EXCEPT the
five listed survey_taxa values, where we want a human-readable,
standardised habitat_type label.
Why: - Many survey_taxa strings do not have a trailing “(…)” substring, and for reporting we only want habitat_type populated for the handful of taxa where we explicitly want a more granular label.
Output targets (exact): - “Ditch & Wetland Channel Freshwater invert assemblages” -> “Ditch & Wetland Channel” - “Running-Water Freshwater invert assemblages (Rivers/Streams)” -> “Running-Water (Rivers/Streams)” - “Standing-Water Freshwater invert assemblages (Ponds/Lakes)” -> “Standing-Water (Ponds/Lakes)” - “Saxicolous Lichens (Rock & Stone)” -> “Saxicolous (Rock & Stone)” - “Terricolous Heath/Bog Lichens” -> “Terricolous (Heath/Bog)”
# ============================================================
# Habitat parsing
# ============================================================
# Helper:
# Return NA unless survey_taxa is one of the five special cases
habitat_type <- function(survey_taxa) {
x <- stringr::str_squish(as.character(survey_taxa))
dplyr::case_when(
# Freshwater assemblages
stringr::str_detect(x, "^Ditch\\s*&\\s*Wetland\\s*Channel\\s+Freshwater\\s+invert\\s+assemblages\\s*$") ~
"Ditch & Wetland Channel",
stringr::str_detect(x, "^Running\\-Water\\s+Freshwater\\s+invert\\s+assemblages\\s*\\(Rivers/Streams\\)\\s*$") ~
"Running-Water (Rivers/Streams)",
stringr::str_detect(x, "^Standing\\-Water\\s+Freshwater\\s+invert\\s+assemblages\\s*\\(Ponds/Lakes\\)\\s*$") ~
"Standing-Water (Ponds/Lakes)",
# Lichens
stringr::str_detect(x, "^Saxicolous\\s+Lichens\\s*\\(Rock\\s*&\\s*Stone\\)\\s*$") ~
"Saxicolous (Rock & Stone)",
stringr::str_detect(x, "^Terricolous\\s+Heath/Bog\\s+Lichens\\s*$") ~
"Terricolous (Heath/Bog)",
TRUE ~ NA_character_
)
}
df_tidy <- df_tidy %>%
dplyr::mutate(
# --- habitat_type: NA except for five explicitly defined taxa ---
habitat_type = habitat_type(survey_taxa),
# Primary classification text for habitat_group mapping:
# keep our original intent: use habitat_type if present, else survey_taxa.
hg_txt = stringr::str_to_lower(dplyr::coalesce(habitat_type, survey_taxa)),
# --- habitat_group mapping (UNCHANGED) ---
habitat_group = dplyr::case_when(
stringr::str_detect(hg_txt, "wetland|aquatic|water\\b|water\\-|water\\s+margin|river|stream|rivers|streams|pond|ponds|lake|lakes|ditch|channel|riparian|rheophytic|rheo") ~
"Freshwater/Wetland",
stringr::str_detect(hg_txt, "woodland|parkland|scrub|deadwood|epiphyt|humidity|veteran|ancient|saproxylic") ~
"Woodland",
stringr::str_detect(hg_txt, "grassland|heath|bog|open\\-habitat|open\\s+habitat|open\\s+ground|bare\\-ground|bare\\s+ground|exposed\\s+substrates?|terricolous") ~
"Open habitats",
stringr::str_detect(hg_txt, "rock|stone|saxicolous") ~
"Open habitats",
TRUE ~ "Unknown"
),
# Force non-missing even if something slips through
habitat_group = dplyr::if_else(is.na(habitat_group) | habitat_group == "", "Unknown", habitat_group),
# taxa grouping (unchanged idea; keyword-based)
taxa = dplyr::case_when(
stringr::str_detect(survey_taxa, stringr::regex("\\bDiptera\\b", ignore_case = TRUE)) ~ "Diptera",
stringr::str_detect(survey_taxa, stringr::regex("\\bBeetles\\b", ignore_case = TRUE)) ~ "Beetles",
stringr::str_detect(survey_taxa, stringr::regex("Vascular Plants\\b", ignore_case = TRUE)) ~ "Vascular plants",
stringr::str_detect(survey_taxa, stringr::regex("\\bLichens\\b", ignore_case = TRUE)) ~ "Lichens",
stringr::str_detect(survey_taxa, stringr::regex("\\bBryophytes?\\b", ignore_case = TRUE)) ~ "Bryophytes",
stringr::str_detect(survey_taxa, stringr::regex("\\bSpiders\\b", ignore_case = TRUE)) ~ "Spiders",
stringr::str_detect(survey_taxa, stringr::regex("Freshwater invert assemblages\\b", ignore_case = TRUE)) ~ "Freshwater invertebrate assemblages",
TRUE ~ "Other/unknown"
)
) %>%
dplyr::select(-hg_txt)
# Diagnostics: do we have any NA habitat_group now?
df_tidy %>%
summarise(
n_rows = n(),
n_habitat_group_na = sum(is.na(habitat_group)),
n_habitat_group_unknown = sum(habitat_group == "Unknown", na.rm = TRUE)
) %>%
kable(caption = "habitat_group diagnostics (should have 0 NA)") %>%
kable_styling(full_width = FALSE)
| n_rows | n_habitat_group_na | n_habitat_group_unknown |
|---|---|---|
| 735 | 0 | 0 |
# Coverage: show habitat_group by survey_taxa (useful spot-check)
df_tidy %>%
distinct(survey_taxa, habitat_type, habitat_group) %>%
arrange(habitat_group, survey_taxa) %>%
kable(caption = "Mapping check: survey_taxa to habitat_group") %>%
kable_styling(full_width = FALSE) %>%
scroll_box(height = "320px")
| survey_taxa | habitat_type | habitat_group |
|---|---|---|
| Ditch & Wetland Channel Freshwater invert assemblages | Ditch & Wetland Channel | Freshwater/Wetland |
| Running-Water Freshwater invert assemblages (Rivers/Streams) | Running-Water (Rivers/Streams) | Freshwater/Wetland |
| Standing-Water Freshwater invert assemblages (Ponds/Lakes) | Standing-Water (Ponds/Lakes) | Freshwater/Wetland |
| Wetland & Aquatic Beetles | NA | Freshwater/Wetland |
| Wetland & Aquatic Vascular Plants | NA | Freshwater/Wetland |
| Wetland & Rheophytic Bryophytes | NA | Freshwater/Wetland |
| Wetland & Riparian Spiders | NA | Freshwater/Wetland |
| Wetland & Water-Margin Diptera | NA | Freshwater/Wetland |
| Open Grassland & Heath Vascular Plants | NA | Open habitats |
| Open Grassland/Heath/Bare-Ground Spiders | NA | Open habitats |
| Open-Ground Bryophytes (Grassland, Heath, Exposed Substrates) | NA | Open habitats |
| Open-Habitat & Flower-Visiting Diptera | NA | Open habitats |
| Open-Habitat Ground Beetles (Grassland & Heath) | NA | Open habitats |
| Saxicolous Lichens (Rock & Stone) | Saxicolous (Rock & Stone) | Open habitats |
| Terricolous Heath/Bog Lichens | Terricolous (Heath/Bog) | Open habitats |
| Epiphytic Lichens (Woodland & Parkland) | NA | Woodland |
| Saproxylic & Woodland Diptera | NA | Woodland |
| Saproxylic Beetles (Woodland Deadwood) | NA | Woodland |
| Woodland & Scrub Spiders | NA | Woodland |
| Woodland & Scrub Vascular Plants | NA | Woodland |
| Woodland Humidity & Epiphyte Bryophytes | NA | Woodland |
We now translate/compute:
cf_mag_signed + pol_mag_signedImportant convention: responses of “do not know/unsure” are treated the same as missing.
# ============================================================
# Scoring helpers
# ============================================================
score_dist <- function(x){
x0 <- str_squish(tolower(as.character(x)))
x0[x0 %in% c("do not know/unsure","do not know","unsure","don't know","dont know","dk","")] <- NA_character_
case_when(
is.na(x0) ~ NA_real_,
str_detect(x0, "robust|significant|large") & str_detect(x0, "decrease") ~ 1,
str_detect(x0, "moderate") & str_detect(x0, "decrease") ~ 2,
str_detect(x0, "no meaningful|no change") ~ 3,
str_detect(x0, "moderate") & str_detect(x0, "increase") ~ 4,
str_detect(x0, "robust|significant|large") & str_detect(x0, "increase") ~ 5,
TRUE ~ NA_real_
)
}
score_cert <- function(x){
x0 <- str_squish(tolower(as.character(x)))
x0[x0 %in% c("do not know/unsure","do not know","unsure","don't know","dont know","dk","")] <- NA_character_
case_when(
is.na(x0) ~ NA_real_,
x0 %in% c("very low","very_low","very-low") ~ 1,
x0 %in% c("low") ~ 2,
x0 %in% c("medium","moderate") ~ 3,
x0 %in% c("high") ~ 4,
x0 %in% c("very high","very_high","very-high") ~ 5,
TRUE ~ NA_real_
)
}
dir_sign <- function(x){
x0 <- str_squish(tolower(as.character(x)))
x0[x0 %in% c("do not know/unsure","do not know","unsure","don't know","dont know","dk","")] <- NA_character_
case_when(
is.na(x0) ~ NA_real_,
str_detect(x0, "decrease") ~ -1,
str_detect(x0, "increase") ~ 1,
str_detect(x0, "no meaningful|no change") ~ 0,
TRUE ~ NA_real_
)
}
# ============================================================
# Score and sign responses
# ============================================================
Q1a <- "Q1a_likely_change_in_distribution"
Q1b <- "Q1b_magnitude_of_change"
Q1c <- "Q1c_your_certainty"
Q2a <- "Q2a_likely_change_in_distribution_compared_to_counterfactual"
Q2b <- "Q2b_magnitude_of_change_compared_to_the_counterfactual"
Q2c <- "Q2c_your_certainty"
df_scored <- df_tidy %>%
mutate(
# parse numeric magnitudes (percent scale assumed in survey)
Q1b_num = parse_number(as.character(.data[[Q1b]]), na = c("N/A","NA","na","n/a","")),
Q2b_num = parse_number(as.character(.data[[Q2b]]), na = c("N/A","NA","na","n/a","")),
# scored categories
cf_dist = score_dist(.data[[Q1a]]),
pol_dist = score_dist(.data[[Q2a]]),
cf_cert = score_cert(.data[[Q1c]]),
pol_cert = score_cert(.data[[Q2c]]),
# direction
cf_dir = dir_sign(.data[[Q1a]]),
pol_dir = dir_sign(.data[[Q2a]]),
# signed magnitudes
cf_mag_signed = if_else(is.na(Q1b_num) | is.na(cf_dir), NA_real_, Q1b_num * cf_dir),
pol_mag_signed = if_else(is.na(Q2b_num) | is.na(pol_dir), NA_real_, Q2b_num * pol_dir)
)
# ============================================================
# Attach counterfactual (Q1) values onto policy rows
# so we can compute implied absolute outcomes later
# ============================================================
scenario_levels <- c("policy_1","policy_2","policy_3","policy_4")
cf_lookup <- df_scored %>%
filter(scenario == "counterfactual") %>%
transmute(
id_gj, survey_taxa,
cf_dist2 = cf_dist,
cf_cert2 = cf_cert,
cf_mag_signed2 = cf_mag_signed
)
df_scored2 <- df_scored %>%
left_join(cf_lookup, by = c("id_gj", "survey_taxa")) %>%
mutate(
cf_dist = cf_dist2,
cf_cert = cf_cert2,
cf_mag_signed = cf_mag_signed2
) %>%
select(-cf_dist2, -cf_cert2, -cf_mag_signed2) %>%
mutate(
abs_policy_mag_signed = if_else(
scenario %in% scenario_levels,
cf_mag_signed + pol_mag_signed,
cf_mag_signed
),
delta_cert = if_else(
scenario %in% scenario_levels,
pol_cert - cf_cert,
NA_real_
)
)
df_scored2 %>%
summarise(
n_rows = n(),
n_experts = n_distinct(id_gj),
n_survey_taxa = n_distinct(survey_taxa),
n_cf_signed = sum(!is.na(cf_mag_signed)),
n_pol_delta_signed = sum(!is.na(pol_mag_signed)),
n_abs_policy_signed = sum(!is.na(abs_policy_mag_signed))
) %>%
kable(caption = "Coverage: signed magnitudes and absolute policy outcomes") %>%
kable_styling(full_width = FALSE)
| n_rows | n_experts | n_survey_taxa | n_cf_signed | n_pol_delta_signed | n_abs_policy_signed |
|---|---|---|---|---|---|
| 735 | 7 | 21 | 705 | 564 | 705 |
# Expected columns from reshape
Q1a <- "Q1a_likely_change_in_distribution"
Q1b <- "Q1b_magnitude_of_change"
Q1c <- "Q1c_your_certainty"
Q2a <- "Q2a_likely_change_in_distribution_compared_to_counterfactual"
Q2b <- "Q2b_magnitude_of_change_compared_to_the_counterfactual"
Q2c <- "Q2c_your_certainty"
df_scored <- df_tidy %>%
mutate(
Q1b_num = parse_number(as.character(.data[[Q1b]]), na = c("N/A","NA","na","n/a","")),
Q2b_num = parse_number(as.character(.data[[Q2b]]), na = c("N/A","NA","na","n/a","")),
cf_dist = score_dist(.data[[Q1a]]),
pol_dist = score_dist(.data[[Q2a]]),
cf_cert = score_cert(.data[[Q1c]]),
pol_cert = score_cert(.data[[Q2c]]),
cf_dir = dir_sign(.data[[Q1a]]),
pol_dir = dir_sign(.data[[Q2a]]),
cf_mag_signed = if_else(is.na(Q1b_num) | is.na(cf_dir), NA_real_, Q1b_num * cf_dir),
pol_mag_signed = if_else(is.na(Q2b_num) | is.na(pol_dir), NA_real_, Q2b_num * pol_dir),
abs_policy_mag_signed = cf_mag_signed + pol_mag_signed,
delta_cert = pol_cert - cf_cert
)
df_scored %>%
summarise(
n_rows = n(),
n_cf_mag = sum(!is.na(Q1b_num)),
n_pol_mag = sum(!is.na(Q2b_num)),
n_cf_signed = sum(!is.na(cf_mag_signed)),
n_delta_signed = sum(!is.na(pol_mag_signed))
) %>%
kable(caption = "Coverage: magnitudes and signed magnitudes") %>%
kable_styling(full_width = FALSE)
| n_rows | n_cf_mag | n_pol_mag | n_cf_signed | n_delta_signed |
|---|---|---|---|---|
| 735 | 141 | 564 | 141 | 564 |
# ============================================================
# FIX: attach counterfactual (Q1) values onto policy rows
# so abs_policy_mag_signed and delta_cert can be computed
# ============================================================
scenario_levels <- c("policy_1","policy_2","policy_3","policy_4")
# 1) Build a CF lookup (one row per id_gj × survey_taxa)
cf_lookup <- df_scored %>%
filter(scenario == "counterfactual") %>%
transmute(
id_gj, survey_taxa,
cf_dist2 = cf_dist,
cf_cert2 = cf_cert,
cf_mag_signed2 = cf_mag_signed
)
# 2) Join CF values onto ALL rows (including policy rows)
df_scored2 <- df_scored %>%
left_join(cf_lookup, by = c("id_gj", "survey_taxa")) %>%
mutate(
# overwrite cf_* with joined versions (so they exist on policy rows too)
cf_dist = cf_dist2,
cf_cert = cf_cert2,
cf_mag_signed = cf_mag_signed2
) %>%
select(-cf_dist2, -cf_cert2, -cf_mag_signed2) %>%
mutate(
# Now these work (because cf_* exist on policy rows)
abs_policy_mag_signed = if_else(
scenario %in% scenario_levels,
cf_mag_signed + pol_mag_signed,
cf_mag_signed
),
delta_cert = if_else(
scenario %in% scenario_levels,
pol_cert - cf_cert,
NA_real_
)
)
# 3) Quick sanity check: abs_policy_mag_signed should no longer be all NA
df_scored2 %>%
group_by(scenario) %>%
summarise(
n = n(),
n_abs_policy_mag = sum(!is.na(abs_policy_mag_signed)),
n_delta_cert = sum(!is.na(delta_cert)),
.groups = "drop"
) %>% print()
## # A tibble: 5 × 4
## scenario n n_abs_policy_mag n_delta_cert
## <chr> <int> <int> <int>
## 1 counterfactual 147 141 0
## 2 policy_1 147 141 147
## 3 policy_2 147 141 147
## 4 policy_3 147 141 147
## 5 policy_4 147 141 147
The dataset is balanced across scenarios and experts. All scenarios are evaluated on the same underlying baseline expectations, ensuring comparability.
The elicitation design is such that the data includes
multiple survey_taxa rows for some taxa
within a single habitat group (notably freshwater assemblages and
lichens). If we compute means across all survey_taxa rows,
these taxa would contribute more weight to habitat-group and overall
summaries simply because they have more rows.
To make this transparent, we run the analysis under two assumptions:
Unit: > expert × scenario × survey_taxa
This preserves the survey structure exactly.
Unit: > expert × scenario × taxa × habitat_group
This averages across multiple survey_taxa rows that fall
into the same taxa:habitat_group cell for each expert and scenario.
Both assumptions are shown side-by-side for: - overall scenario summaries, and - habitat_group summaries.
scenario_levels <- c("counterfactual","policy_1","policy_2","policy_3","policy_4")
# A) Row-based: keep all survey_taxa rows (no collapsing)
df_A <- df_scored2 %>%
filter(scenario %in% scenario_levels) %>%
filter(!(taxa == "Lichens" & habitat_group == "Freshwater/Wetland")) %>%
mutate(assumption = "A (rows = all survey_taxa)")
# B) Collapse within habitat: unique taxa:habitat_group per expert × scenario
df_B <- df_A %>%
group_by(id_gj, scenario, habitat_group, taxa) %>%
summarise(
cf_mag_signed = mean(cf_mag_signed, na.rm = TRUE),
pol_mag_signed = mean(pol_mag_signed, na.rm = TRUE),
abs_policy_mag_signed = mean(abs_policy_mag_signed, na.rm = TRUE),
cf_dist = mean(cf_dist, na.rm = TRUE),
pol_dist = mean(pol_dist, na.rm = TRUE),
cf_cert = mean(cf_cert, na.rm = TRUE),
pol_cert = mean(pol_cert, na.rm = TRUE),
n_survey_taxa_rows_collapsed = n(),
.groups = "drop"
) %>%
mutate(assumption = "B (collapsed = unique taxa:habitat_group)")
# Combined object (handy for faceting tables/plots)
df_AB <- bind_rows(df_A, df_B)
# Diagnostics: show where collapsing occurred
df_B %>%
filter(n_survey_taxa_rows_collapsed > 1) %>%
count(habitat_group, taxa, n_survey_taxa_rows_collapsed, sort = TRUE) %>%
kable(caption = "Where Assumption B collapses multiple survey_taxa rows into one taxa:habitat_group unit") %>%
kable_styling(full_width = FALSE)
| habitat_group | taxa | n_survey_taxa_rows_collapsed | n |
|---|---|---|---|
| Freshwater/Wetland | Freshwater invertebrate assemblages | 3 | 35 |
| Open habitats | Lichens | 2 | 35 |
This section establishes the counterfactual (“business-as-usual”) baseline from Q1, before we interpret policy deltas from Q2. Because Q2 magnitudes are explicitly defined relative to Q1, we need a clear view of baseline “pull” to answer the policy-relevant question: do uplifts merely slow decline, or do they offset it sufficiently to deliver stability/recovery by 2042?
We report results under two aggregation assumptions:
Assumption A (rows = all survey_taxa): each survey row contributes as an observation. This reflects the survey design directly, but can implicitly overweight taxa that were split into multiple survey_taxa rows within a habitat group (notably freshwater assemblages and lichens).
Assumption B (collapsed = unique taxa:habitat_group): within each expert × scenario, multiple survey_taxa rows that represent the same taxa within the same habitat group are collapsed to one unit (averaged). This is the most direct way to prevent survey-row multiplicity from driving habitat-group summaries.
Importantly, taxa-level comparisons are not expected to change materially across A vs B, because the collapsing only affects where a taxon has multiple survey rows within the same habitat group. By construction, the main place where A vs B can matter is therefore habitat-group summaries.
cf_AB <- df_AB %>%
filter(scenario == "counterfactual")
# Headline baseline (A vs B)
cf_AB %>%
group_by(assumption) %>%
summarise(
n_units = n(),
n_experts = n_distinct(id_gj),
n_taxa = n_distinct(taxa),
n_hab_groups = n_distinct(habitat_group),
mean_cf = mean(cf_mag_signed, na.rm = TRUE),
median_cf = median(cf_mag_signed, na.rm = TRUE),
sd_cf = sd(cf_mag_signed, na.rm = TRUE),
p10 = quantile(cf_mag_signed, 0.10, na.rm = TRUE),
p90 = quantile(cf_mag_signed, 0.90, na.rm = TRUE),
prop_negative = mean(cf_mag_signed < 0, na.rm = TRUE),
prop_zero = mean(cf_mag_signed == 0, na.rm = TRUE),
prop_positive = mean(cf_mag_signed > 0, na.rm = TRUE),
.groups = "drop"
) %>%
mutate(across(where(is.numeric), ~ round(.x, 2))) %>%
kable(caption = "Counterfactual baseline (Q1 signed %): Assumptions A and B") %>%
kable_styling(full_width = FALSE)
| assumption | n_units | n_experts | n_taxa | n_hab_groups | mean_cf | median_cf | sd_cf | p10 | p90 | prop_negative | prop_zero | prop_positive |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| A (rows = all survey_taxa) | 147 | 7 | 7 | 3 | -12.33 | -10 | 11.46 | -30 | 0 | 0.79 | 0.13 | 0.07 |
| B (collapsed = unique taxa:habitat_group) | 126 | 7 | 7 | 3 | -11.95 | -10 | 11.74 | -30 | 0 | 0.78 | 0.15 | 0.07 |
The baseline tables report:
mean_cf: mean signed counterfactual
change (%), where negative values imply decline by 2042.prop_negative: share of units with baseline decline
(< 0), which is a compact signal of how widespread negative
expectations are.mean_cert: average reported certainty (1–5 scale).At headline level, the baseline is strongly negative under both assumptions (mean around −12% in A and slightly less negative in B), and the proportion negative is high (around four-fifths). This indicates that, in expert judgement, continued decline is the default state without further intervention.
The habitat-group baseline table is shown explicitly under both assumptions because this is where multiplicity can matter. In these data, the only systematic collapsing in B is:
so A can overweight those components in habitat-group means relative to other taxa that appear as a single survey row within habitat.
This table is shown under both assumptions because it is precisely where survey-row multiplicity can matter.
cf_by_hab_AB <- cf_AB %>%
group_by(assumption, habitat_group) %>%
summarise(
n = n(),
mean_cf = mean(cf_mag_signed, na.rm = TRUE),
prop_negative = mean(cf_mag_signed < 0, na.rm = TRUE),
mean_cert = mean(cf_cert, na.rm = TRUE),
.groups = "drop"
) %>%
arrange(assumption, mean_cf)
cf_by_hab_AB %>%
mutate(
mean_cf = round(mean_cf, 2),
mean_cert = round(mean_cert, 2),
prop_negative = scales::percent(prop_negative, accuracy = 1)
) %>%
kable(caption = "Counterfactual baseline by habitat_group (Assumptions A and B)") %>%
kable_styling(full_width = FALSE)
| assumption | habitat_group | n | mean_cf | prop_negative | mean_cert |
|---|---|---|---|---|---|
| A (rows = all survey_taxa) | Freshwater/Wetland | 56 | -13.83 | 87% | 2.02 |
| A (rows = all survey_taxa) | Open habitats | 49 | -13.43 | 85% | 1.98 |
| A (rows = all survey_taxa) | Woodland | 42 | -9.00 | 62% | 1.88 |
| B (collapsed = unique taxa:habitat_group) | Freshwater/Wetland | 42 | -13.47 | 85% | 1.94 |
| B (collapsed = unique taxa:habitat_group) | Open habitats | 42 | -13.36 | 85% | 1.95 |
| B (collapsed = unique taxa:habitat_group) | Woodland | 42 | -9.00 | 62% | 1.88 |
two_cols <- c("#c9643e", "#618f72")
ggplot(cf_by_hab_AB, aes(x = habitat_group, y = mean_cf, fill = assumption)) +
geom_col(position = position_dodge(width = 0.8)) +
coord_flip() +
geom_hline(yintercept = 0, linetype = "dashed") +
scale_fill_manual(values = two_cols) +
labs(
title = "Counterfactual baseline by habitat_group (Assumptions A and B)",
x = NULL,
y = "Mean signed % change (counterfactual)"
) +
theme_minimal()
ggplot(subset(cf_by_hab_AB, assumption = "A (rows = all survey_taxa)"), aes(x = habitat_group, y = mean_cf)) +
geom_col(position = position_dodge(width = 0.8)) +
coord_flip() +
geom_hline(yintercept = 0, linetype = "dashed") +
# scale_fill_manual(values = two_cols) +
labs(
title = "Counterfactual baseline by habitat_group (Assumptions A and B)",
x = NULL,
y = "Mean signed % change (counterfactual)"
) +
theme_minimal(base_size = 18)
## Warning: In subset.data.frame(cf_by_hab_AB, assumption = "A (rows = all survey_taxa)") :
## extra argument 'assumption' will be disregarded
Across both assumptions, Freshwater/Wetland and Open habitats show the most negative baseline means, while Woodland is less negative. Under Assumption A, the mean counterfactual baseline is approximately:
Under Assumption B, the corresponding habitat-group means are very similar (Freshwater/Wetland and Open habitats remain strongly negative; Woodland remains less negative), with small shifts consistent with removing survey-row multiplicity.
Policy implication at this stage: the baseline “pull” is large enough that even sizeable positive deltas may still leave net negative outcomes once CF + delta are combined. This is exactly what Sections 5–6 test.
Taxa-level comparisons are not meaningfully altered by collapsing within habitat, so we report once using the row-based structure.
cf_by_taxa <- df_A %>%
filter(scenario == "counterfactual") %>%
group_by(taxa) %>%
summarise(
n = n(),
mean_cf = mean(cf_mag_signed, na.rm = TRUE),
prop_negative = mean(cf_mag_signed < 0, na.rm = TRUE),
mean_cert = mean(cf_cert, na.rm = TRUE),
.groups = "drop"
) %>%
arrange(mean_cf)
cf_by_taxa %>%
mutate(
mean_cf = round(mean_cf, 2),
mean_cert = round(mean_cert, 2),
prop_negative = scales::percent(prop_negative, accuracy = 1)
) %>%
kable(caption = "Counterfactual baseline by taxa (A; identical interpretation under B)") %>%
kable_styling(full_width = FALSE)
| taxa | n | mean_cf | prop_negative | mean_cert |
|---|---|---|---|---|
| Freshwater invertebrate assemblages | 21 | -14.86 | 95% | 2.24 |
| Vascular plants | 21 | -13.33 | 76% | 2.19 |
| Lichens | 21 | -13.00 | 90% | 2.14 |
| Bryophytes | 21 | -12.62 | 86% | 2.00 |
| Diptera | 21 | -12.39 | 78% | 1.67 |
| Beetles | 21 | -10.48 | 67% | 1.95 |
| Spiders | 21 | -9.17 | 61% | 1.57 |
ggplot(cf_AB, aes(x = cf_mag_signed, fill = assumption)) +
geom_histogram(bins = 18, color = "white") +
geom_vline(xintercept = 0, linetype = "dashed") +
facet_wrap(~ assumption) +
scale_fill_manual(values = two_cols) +
guides(fill = "none") +
labs(
title = "Counterfactual (Q1): distribution of signed magnitude (% change)",
x = "Signed % change under counterfactual",
y = "Count"
) +
theme_minimal()
Section 5 interprets Q2 policy deltas, which are defined as incremental change relative to the counterfactual. These are not absolute outcomes. A positive delta means “less decline than counterfactual” (or “more increase than counterfactual”), but it does not tell us whether the system is stable/recovering overall.
We therefore treat Section 5 as answering:
The coverage table confirms that both A and B are based on the same expert panel (n=7), with B having fewer units because of the within-habitat collapsing step.
Q2 magnitudes are defined relative to the counterfactual. We show overall and habitat-group summaries under both assumptions.
pol_AB <- df_AB %>%
filter(str_detect(scenario, "^policy_"))
pol_AB %>%
group_by(assumption) %>%
summarise(
n_units = n(),
n_nonmiss = sum(!is.na(pol_mag_signed)),
n_experts = n_distinct(id_gj),
.groups = "drop"
) %>%
kable(caption = "Incremental policy delta (Q2) coverage check (Assumptions A and B)") %>%
kable_styling(full_width = FALSE)
| assumption | n_units | n_nonmiss | n_experts |
|---|---|---|---|
| A (rows = all survey_taxa) | 588 | 564 | 7 |
| B (collapsed = unique taxa:habitat_group) | 504 | 480 | 7 |
The scenario-level delta table shows positive mean uplifts for every policy scenario under both assumptions. Under Assumption A (row-based), the mean deltas are:
This produces a clear and stable ranking: policy_3 > policy_2 ≈ policy_4 > policy_1.
The “prop_positive” column shows that most expert–unit responses are positive deltas (for example, under policy_3 it is very high), meaning experts generally agree policies improve outcomes relative to baseline even when they disagree about magnitude.
A vs B: At the overall scenario level, collapsing multiplicity (B) should not materially change the ordering and typically shifts headline means only modestly, because only a small subset of taxa:habitat combinations are affected by the collapse step. The main sensitivity to A vs B is expected to show up in habitat-group summaries, not in the overall ranking.
delta_headline_AB <- pol_AB %>%
group_by(assumption, scenario) %>%
summarise(
mean_delta = mean(pol_mag_signed, na.rm = TRUE),
median_delta = median(pol_mag_signed, na.rm = TRUE),
sd_delta = sd(pol_mag_signed, na.rm = TRUE),
p10 = quantile(pol_mag_signed, 0.10, na.rm = TRUE),
p90 = quantile(pol_mag_signed, 0.90, na.rm = TRUE),
prop_positive = mean(pol_mag_signed > 0, na.rm = TRUE),
prop_zero = mean(pol_mag_signed == 0, na.rm = TRUE),
prop_negative = mean(pol_mag_signed < 0, na.rm = TRUE),
.groups = "drop"
) %>%
arrange(assumption, desc(mean_delta))
delta_headline_AB %>%
mutate(across(where(is.numeric), ~ round(.x, 2))) %>%
kable(caption = "Incremental policy uplifts (Q2 deltas): overall by scenario (Assumptions A and B)") %>%
kable_styling(full_width = FALSE)
| assumption | scenario | mean_delta | median_delta | sd_delta | p10 | p90 | prop_positive | prop_zero | prop_negative |
|---|---|---|---|---|---|---|---|---|---|
| A (rows = all survey_taxa) | policy_3 | 16.55 | 15 | 6.43 | 8.00 | 25.0 | 0.99 | 0.01 | 0.00 |
| A (rows = all survey_taxa) | policy_4 | 11.81 | 13 | 5.02 | 5.00 | 18.0 | 0.96 | 0.04 | 0.00 |
| A (rows = all survey_taxa) | policy_2 | 10.75 | 10 | 5.77 | 4.00 | 20.0 | 0.94 | 0.06 | 0.00 |
| A (rows = all survey_taxa) | policy_1 | 4.15 | 5 | 6.98 | -10.00 | 10.0 | 0.73 | 0.13 | 0.14 |
| B (collapsed = unique taxa:habitat_group) | policy_3 | 16.58 | 15 | 6.34 | 7.90 | 25.0 | 0.98 | 0.02 | 0.00 |
| B (collapsed = unique taxa:habitat_group) | policy_4 | 11.81 | 13 | 4.98 | 5.90 | 17.1 | 0.96 | 0.04 | 0.00 |
| B (collapsed = unique taxa:habitat_group) | policy_2 | 10.94 | 10 | 5.63 | 4.95 | 20.0 | 0.97 | 0.03 | 0.00 |
| B (collapsed = unique taxa:habitat_group) | policy_1 | 4.24 | 5 | 6.96 | -10.00 | 10.0 | 0.77 | 0.09 | 0.14 |
ggplot(delta_headline_AB, aes(x = scenario, y = mean_delta, fill = assumption)) +
geom_col(position = position_dodge(width = 0.8)) +
geom_hline(yintercept = 0, linetype = "dashed") +
scale_fill_manual(values = two_cols) +
labs(
title = "Incremental policy uplifts by scenario (Assumptions A and B)",
x = NULL,
y = "Mean signed % delta"
) +
theme_minimal()
delta_by_hab_AB <- pol_AB %>%
group_by(assumption, habitat_group, scenario) %>%
summarise(
n = n(),
mean_delta = mean(pol_mag_signed, na.rm = TRUE),
prop_positive = mean(pol_mag_signed > 0, na.rm = TRUE),
.groups = "drop"
)
delta_by_hab_AB %>%
mutate(
mean_delta = round(mean_delta, 2),
prop_positive = scales::percent(prop_positive, accuracy = 1)
) %>%
arrange(assumption, habitat_group, scenario) %>%
kable(caption = "Incremental policy uplifts by habitat_group (Assumptions A and B)") %>%
kable_styling(full_width = FALSE) %>%
scroll_box(height = "320px")
| assumption | habitat_group | scenario | n | mean_delta | prop_positive |
|---|---|---|---|---|---|
| A (rows = all survey_taxa) | Freshwater/Wetland | policy_1 | 56 | 4.44 | 74% |
| A (rows = all survey_taxa) | Freshwater/Wetland | policy_2 | 56 | 10.83 | 93% |
| A (rows = all survey_taxa) | Freshwater/Wetland | policy_3 | 56 | 17.61 | 100% |
| A (rows = all survey_taxa) | Freshwater/Wetland | policy_4 | 56 | 13.13 | 98% |
| A (rows = all survey_taxa) | Open habitats | policy_1 | 49 | 3.94 | 72% |
| A (rows = all survey_taxa) | Open habitats | policy_2 | 49 | 10.81 | 94% |
| A (rows = all survey_taxa) | Open habitats | policy_3 | 49 | 16.32 | 98% |
| A (rows = all survey_taxa) | Open habitats | policy_4 | 49 | 11.51 | 98% |
| A (rows = all survey_taxa) | Woodland | policy_1 | 42 | 4.00 | 72% |
| A (rows = all survey_taxa) | Woodland | policy_2 | 42 | 10.57 | 98% |
| A (rows = all survey_taxa) | Woodland | policy_3 | 42 | 15.40 | 98% |
| A (rows = all survey_taxa) | Woodland | policy_4 | 42 | 10.38 | 92% |
| B (collapsed = unique taxa:habitat_group) | Freshwater/Wetland | policy_1 | 42 | 4.35 | 78% |
| B (collapsed = unique taxa:habitat_group) | Freshwater/Wetland | policy_2 | 42 | 10.89 | 98% |
| B (collapsed = unique taxa:habitat_group) | Freshwater/Wetland | policy_3 | 42 | 17.59 | 100% |
| B (collapsed = unique taxa:habitat_group) | Freshwater/Wetland | policy_4 | 42 | 13.12 | 98% |
| B (collapsed = unique taxa:habitat_group) | Open habitats | policy_1 | 42 | 4.38 | 80% |
| B (collapsed = unique taxa:habitat_group) | Open habitats | policy_2 | 42 | 11.35 | 95% |
| B (collapsed = unique taxa:habitat_group) | Open habitats | policy_3 | 42 | 16.75 | 98% |
| B (collapsed = unique taxa:habitat_group) | Open habitats | policy_4 | 42 | 11.93 | 98% |
| B (collapsed = unique taxa:habitat_group) | Woodland | policy_1 | 42 | 4.00 | 72% |
| B (collapsed = unique taxa:habitat_group) | Woodland | policy_2 | 42 | 10.57 | 98% |
| B (collapsed = unique taxa:habitat_group) | Woodland | policy_3 | 42 | 15.40 | 98% |
| B (collapsed = unique taxa:habitat_group) | Woodland | policy_4 | 42 | 10.38 | 92% |
ggplot(delta_by_hab_AB, aes(x = scenario, y = mean_delta, fill = assumption)) +
geom_col(position = position_dodge(width = 0.8)) +
facet_wrap(~ habitat_group) +
geom_hline(yintercept = 0, linetype = "dashed") +
scale_fill_manual(values = two_cols) +
labs(
title = "Incremental policy uplifts by habitat_group (Assumptions A and B)",
x = NULL,
y = "Mean signed % delta"
) +
theme_minimal() +
theme(
axis.text.x = element_text(angle = 90, vjust = 0.5, hjust = 0.5)
)
Habitat-group uplift summaries provide the first indication of where policy leverage is expected to be strongest. The key point is not just “uplifts are positive”, but whether uplifts in the most-negative baseline habitat groups (Freshwater/Wetland and Open habitats) are large enough to offset the baseline pull established in Section 4.
Because Assumption B collapses the freshwater assemblage multiplicity (and lichen multiplicity in open habitats), the habitat-group mean delta is the place to look for any material change between A vs B. If the habitat-group conclusions are robust across A and B, that strengthens confidence that the reported habitat differences are not artefacts of how the survey rows were structured.
pol_A <- df_A %>%
filter(str_detect(scenario, "^policy_"))
delta_by_taxa <- pol_A %>%
group_by(taxa, scenario) %>%
summarise(
n = n(),
mean_delta = mean(pol_mag_signed, na.rm = TRUE),
prop_positive = mean(pol_mag_signed > 0, na.rm = TRUE),
.groups = "drop"
)
delta_by_taxa %>%
mutate(
mean_delta = round(mean_delta, 2),
prop_positive = scales::percent(prop_positive, accuracy = 1)
) %>%
kable(caption = "Incremental policy uplifts by taxa (A; identical interpretation under B)") %>%
kable_styling(full_width = FALSE) %>%
scroll_box(height = "320px")
| taxa | scenario | n | mean_delta | prop_positive |
|---|---|---|---|---|
| Beetles | policy_1 | 21 | 4.76 | 71% |
| Beetles | policy_2 | 21 | 12.43 | 100% |
| Beetles | policy_3 | 21 | 17.10 | 100% |
| Beetles | policy_4 | 21 | 11.71 | 95% |
| Bryophytes | policy_1 | 21 | 3.24 | 76% |
| Bryophytes | policy_2 | 21 | 9.95 | 95% |
| Bryophytes | policy_3 | 21 | 16.10 | 95% |
| Bryophytes | policy_4 | 21 | 11.52 | 95% |
| Diptera | policy_1 | 21 | 3.89 | 67% |
| Diptera | policy_2 | 21 | 10.44 | 89% |
| Diptera | policy_3 | 21 | 15.83 | 94% |
| Diptera | policy_4 | 21 | 11.56 | 83% |
| Freshwater invertebrate assemblages | policy_1 | 21 | 4.71 | 71% |
| Freshwater invertebrate assemblages | policy_2 | 21 | 10.67 | 86% |
| Freshwater invertebrate assemblages | policy_3 | 21 | 17.67 | 100% |
| Freshwater invertebrate assemblages | policy_4 | 21 | 13.14 | 100% |
| Lichens | policy_1 | 21 | 2.52 | 62% |
| Lichens | policy_2 | 21 | 7.90 | 90% |
| Lichens | policy_3 | 21 | 13.90 | 100% |
| Lichens | policy_4 | 21 | 9.19 | 100% |
| Spiders | policy_1 | 21 | 3.50 | 78% |
| Spiders | policy_2 | 21 | 10.94 | 100% |
| Spiders | policy_3 | 21 | 16.28 | 100% |
| Spiders | policy_4 | 21 | 11.44 | 100% |
| Vascular plants | policy_1 | 21 | 6.29 | 86% |
| Vascular plants | policy_2 | 21 | 12.90 | 100% |
| Vascular plants | policy_3 | 21 | 18.86 | 100% |
| Vascular plants | policy_4 | 21 | 14.00 | 100% |
Across both aggregation assumptions, experts perceive all policy scenarios as improving outcomes relative to the counterfactual. The policy scenarios are clearly differentiated in strength, with policy_3 consistently eliciting the largest average uplift, followed by policy_2 and policy_4, with policy_1 weakest.
However, deltas alone do not answer whether the target-relevant condition (stability/recovery by 2042) is achieved. That requires combining Q1 and Q2 into absolute outcomes (Section 6).
Section 6 performs the key “sufficiency test” by converting the elicited components into absolute outcomes under each policy scenario:
abs_policy_mag_signed = cf_mag_signed + pol_mag_signed
This step matters because it answers the policy-facing question:
We present headline results under both Assumptions A and B. Here, A vs B matters most for habitat-group interpretation, because the collapsing step in B prevents freshwater assemblage and lichen multiplicity from dominating habitat-group means.
Absolute signed outcomes under policy are:
Absolute policy outcome = Counterfactual (Q1) + Policy delta (Q2)policy_summary_AB <- pol_AB %>%
group_by(assumption, scenario) %>%
summarise(
mean_policy = mean(abs_policy_mag_signed, na.rm = TRUE),
median_policy = median(abs_policy_mag_signed, na.rm = TRUE),
sd_policy = sd(abs_policy_mag_signed, na.rm = TRUE),
p10 = quantile(abs_policy_mag_signed, 0.10, na.rm = TRUE),
p90 = quantile(abs_policy_mag_signed, 0.90, na.rm = TRUE),
prop_positive = mean(abs_policy_mag_signed > 0, na.rm = TRUE),
prop_near_zero = mean(abs(abs_policy_mag_signed) <= 1, na.rm = TRUE),
prop_negative = mean(abs_policy_mag_signed < 0, na.rm = TRUE),
.groups = "drop"
) %>%
arrange(assumption, desc(mean_policy))
policy_summary_AB %>%
mutate(across(where(is.numeric), ~ round(.x, 2))) %>%
kable(caption = "Absolute signed % outcome under each policy (Assumptions A and B)") %>%
kable_styling(full_width = FALSE)
| assumption | scenario | mean_policy | median_policy | sd_policy | p10 | p90 | prop_positive | prop_near_zero | prop_negative |
|---|---|---|---|---|---|---|---|---|---|
| A (rows = all survey_taxa) | policy_3 | 4.23 | 5 | 9.42 | -10 | 15.0 | 0.67 | 0.13 | 0.21 |
| A (rows = all survey_taxa) | policy_4 | -0.52 | 0 | 10.30 | -15 | 13.0 | 0.41 | 0.23 | 0.36 |
| A (rows = all survey_taxa) | policy_2 | -1.57 | 0 | 10.54 | -20 | 9.0 | 0.34 | 0.28 | 0.39 |
| A (rows = all survey_taxa) | policy_1 | -8.18 | -5 | 12.20 | -25 | 5.0 | 0.18 | 0.20 | 0.63 |
| B (collapsed = unique taxa:habitat_group) | policy_3 | 4.63 | 5 | 9.78 | -10 | 15.0 | 0.68 | 0.12 | 0.21 |
| B (collapsed = unique taxa:habitat_group) | policy_4 | -0.14 | 0 | 10.67 | -15 | 13.0 | 0.42 | 0.22 | 0.35 |
| B (collapsed = unique taxa:habitat_group) | policy_2 | -1.01 | 0 | 10.89 | -20 | 10.0 | 0.38 | 0.31 | 0.34 |
| B (collapsed = unique taxa:habitat_group) | policy_1 | -7.70 | -5 | 12.64 | -25 | 5.3 | 0.20 | 0.22 | 0.59 |
The scenario-level absolute outcome table shows a consistent pattern:
Under Assumption A, the mean absolute outcomes are:
This is the core quantitative message: policy_3 comes close to offsetting baseline decline on average, but does not cross the stability threshold (0%) in the mean.
Two nuances are important for interpretation:
A vs B: the table is shown under both assumptions because the “headline mean” is sensitive to how units are counted. If the A vs B results are close, that strengthens confidence that conclusions are not being driven by survey-row multiplicity.
ggplot(policy_summary_AB, aes(x = scenario, y = mean_policy, fill = assumption)) +
geom_col(position = position_dodge(width = 0.8)) +
geom_hline(yintercept = 0, linetype = "dashed") +
scale_fill_manual(values = two_cols) +
labs(
title = "Absolute signed % outcome under each policy (Assumptions A and B)",
x = NULL,
y = "Mean signed % change (CF + delta)"
) +
theme_minimal()
ggplot(subset(policy_summary_AB, assumption = "A (rows = all survey_taxa)"), aes(x = scenario, y = mean_policy)) +
geom_col(position = position_dodge(width = 0.8)) +
geom_hline(yintercept = 0, linetype = "dashed") +
labs(
title = "Absolute signed % outcome under each policy (Assumptions A and B)",
x = NULL,
y = "Mean signed % change (CF + delta)"
) +
theme_minimal(base_size = 18)
## Warning: In subset.data.frame(policy_summary_AB, assumption = "A (rows = all survey_taxa)") :
## extra argument 'assumption' will be disregarded
cf_mean_AB <- cf_AB %>%
group_by(assumption) %>%
summarise(mean_cf = mean(cf_mag_signed, na.rm = TRUE), .groups = "drop")
policy_summary_plot_AB <- policy_summary_AB %>%
left_join(cf_mean_AB, by = "assumption")
ggplot(policy_summary_plot_AB, aes(x = scenario)) +
geom_col(aes(y = mean_policy, fill = assumption), position = position_dodge(width = 0.8)) +
facet_wrap(~ assumption) +
geom_hline(yintercept = 0, linetype = "dashed") +
geom_hline(aes(yintercept = mean_cf), linetype = "dotted") +
scale_fill_manual(values = two_cols) +
labs(
title = "Mean signed % outcome: Counterfactual vs Policy (A and B)",
subtitle = "Dotted = counterfactual mean; dashed = stability (0%)",
x = NULL,
y = "Mean signed % change"
) +
theme_minimal()
In the mean comparison plot, the dashed line at 0% represents stability (no net change by 2042). The dotted line marks the counterfactual mean. The vertical distance:
The important result is that all scenario means sit above the counterfactual mean (uplift), but still below zero (insufficient for stability in the mean). This graph makes the distinction between “uplift” and “recovery” visually explicit.
pol_AB %>%
group_by(assumption, scenario) %>%
summarise(
prop_recovery = mean(abs_policy_mag_signed > 0, na.rm = TRUE),
prop_stabilised = mean(abs_policy_mag_signed >= 0, na.rm = TRUE),
prop_decline_remaining = mean(abs_policy_mag_signed < 0, na.rm = TRUE),
.groups = "drop"
) %>%
mutate(across(where(is.numeric), ~ scales::percent(.x, accuracy = 1))) %>%
kable(caption = "Proportion stabilised/recovered under each policy (Assumptions A and B)") %>%
kable_styling(full_width = FALSE)
| assumption | scenario | prop_recovery | prop_stabilised | prop_decline_remaining |
|---|---|---|---|---|
| A (rows = all survey_taxa) | policy_1 | 18% | 37% | 63% |
| A (rows = all survey_taxa) | policy_2 | 34% | 61% | 39% |
| A (rows = all survey_taxa) | policy_3 | 67% | 79% | 21% |
| A (rows = all survey_taxa) | policy_4 | 41% | 64% | 36% |
| B (collapsed = unique taxa:habitat_group) | policy_1 | 20% | 41% | 59% |
| B (collapsed = unique taxa:habitat_group) | policy_2 | 38% | 66% | 34% |
| B (collapsed = unique taxa:habitat_group) | policy_3 | 68% | 79% | 21% |
| B (collapsed = unique taxa:habitat_group) | policy_4 | 42% | 65% | 35% |
The stabilised/recovered fractions provide a more decision-friendly view than the mean alone:
This reconciles two facts that can otherwise look contradictory:
A vs B: the fraction table is presented under both assumptions; the similarity between A and B proportions indicates the “many improve, some remain strongly negative” pattern is not an artefact of survey-row multiplicity.
policy_by_hab_AB <- df_AB %>%
group_by(assumption, habitat_group, scenario) %>%
summarise(
n = n(),
mean_policy = mean(abs_policy_mag_signed, na.rm = TRUE),
prop_positive = mean(abs_policy_mag_signed > 0, na.rm = TRUE),
.groups = "drop"
)
policy_by_hab_AB %>%
mutate(
mean_policy = round(mean_policy, 2),
prop_positive = scales::percent(prop_positive, accuracy = 1)
) %>%
arrange(assumption, habitat_group, scenario) %>%
kable(caption = "Absolute outcomes under policy by habitat_group (Assumptions A and B)") %>%
kable_styling(full_width = FALSE) %>%
scroll_box(height = "360px")
| assumption | habitat_group | scenario | n | mean_policy | prop_positive |
|---|---|---|---|---|---|
| A (rows = all survey_taxa) | Freshwater/Wetland | counterfactual | 56 | -13.83 | 2% |
| A (rows = all survey_taxa) | Freshwater/Wetland | policy_1 | 56 | -9.39 | 9% |
| A (rows = all survey_taxa) | Freshwater/Wetland | policy_2 | 56 | -3.00 | 24% |
| A (rows = all survey_taxa) | Freshwater/Wetland | policy_3 | 56 | 3.78 | 69% |
| A (rows = all survey_taxa) | Freshwater/Wetland | policy_4 | 56 | -0.70 | 41% |
| A (rows = all survey_taxa) | Open habitats | counterfactual | 49 | -13.43 | 0% |
| A (rows = all survey_taxa) | Open habitats | policy_1 | 49 | -9.49 | 17% |
| A (rows = all survey_taxa) | Open habitats | policy_2 | 49 | -2.62 | 36% |
| A (rows = all survey_taxa) | Open habitats | policy_3 | 49 | 2.89 | 66% |
| A (rows = all survey_taxa) | Open habitats | policy_4 | 49 | -1.91 | 40% |
| A (rows = all survey_taxa) | Woodland | counterfactual | 42 | -9.00 | 22% |
| A (rows = all survey_taxa) | Woodland | policy_1 | 42 | -5.00 | 30% |
| A (rows = all survey_taxa) | Woodland | policy_2 | 42 | 1.57 | 45% |
| A (rows = all survey_taxa) | Woodland | policy_3 | 42 | 6.40 | 68% |
| A (rows = all survey_taxa) | Woodland | policy_4 | 42 | 1.38 | 42% |
| B (collapsed = unique taxa:habitat_group) | Freshwater/Wetland | counterfactual | 42 | -13.47 | 0% |
| B (collapsed = unique taxa:habitat_group) | Freshwater/Wetland | policy_1 | 42 | -9.12 | 10% |
| B (collapsed = unique taxa:habitat_group) | Freshwater/Wetland | policy_2 | 42 | -2.58 | 30% |
| B (collapsed = unique taxa:habitat_group) | Freshwater/Wetland | policy_3 | 42 | 4.12 | 68% |
| B (collapsed = unique taxa:habitat_group) | Freshwater/Wetland | policy_4 | 42 | -0.35 | 42% |
| B (collapsed = unique taxa:habitat_group) | Open habitats | counterfactual | 42 | -13.36 | 0% |
| B (collapsed = unique taxa:habitat_group) | Open habitats | policy_1 | 42 | -8.99 | 20% |
| B (collapsed = unique taxa:habitat_group) | Open habitats | policy_2 | 42 | -2.01 | 40% |
| B (collapsed = unique taxa:habitat_group) | Open habitats | policy_3 | 42 | 3.39 | 70% |
| B (collapsed = unique taxa:habitat_group) | Open habitats | policy_4 | 42 | -1.44 | 42% |
| B (collapsed = unique taxa:habitat_group) | Woodland | counterfactual | 42 | -9.00 | 22% |
| B (collapsed = unique taxa:habitat_group) | Woodland | policy_1 | 42 | -5.00 | 30% |
| B (collapsed = unique taxa:habitat_group) | Woodland | policy_2 | 42 | 1.57 | 45% |
| B (collapsed = unique taxa:habitat_group) | Woodland | policy_3 | 42 | 6.40 | 68% |
| B (collapsed = unique taxa:habitat_group) | Woodland | policy_4 | 42 | 1.38 | 42% |
ggplot(policy_by_hab_AB, aes(x = scenario, y = mean_policy, fill = assumption)) +
geom_col(position = position_dodge(width = 0.8)) +
facet_wrap(~ habitat_group) +
geom_hline(yintercept = 0, linetype = "dashed") +
scale_fill_manual(values = two_cols) +
labs(
title = "Absolute outcomes under policy by habitat_group (Assumptions A and B)",
x = NULL,
y = "Mean signed % change (CF + delta)"
) +
theme_minimal()
The habitat-group absolute outcomes sharpen where the recovery gap sits:
This pattern is consistent with the baseline in Section 4: the most negative baseline domains (Freshwater/Wetland and Open habitats) remain the binding constraints on achieving system-wide stability by 2042.
Because these habitat-group results are shown for both A and B, we can interpret the habitat differences with higher confidence: the story persists even after removing the freshwater/lichen row multiplicity in Assumption B.
policy_by_taxa <- pol_A %>%
group_by(taxa, scenario) %>%
summarise(
mean_policy = mean(abs_policy_mag_signed, na.rm = TRUE),
prop_positive = mean(abs_policy_mag_signed > 0, na.rm = TRUE),
.groups = "drop"
)
policy_by_taxa %>%
mutate(
mean_policy = round(mean_policy, 2),
prop_positive = scales::percent(prop_positive, accuracy = 1)
) %>%
kable(caption = "Absolute outcomes under policy by taxa (A; identical interpretation under B)") %>%
kable_styling(full_width = FALSE) %>%
scroll_box(height = "360px")
| taxa | scenario | mean_policy | prop_positive |
|---|---|---|---|
| Beetles | policy_1 | -5.71 | 29% |
| Beetles | policy_2 | 1.95 | 57% |
| Beetles | policy_3 | 6.62 | 67% |
| Beetles | policy_4 | 1.24 | 52% |
| Bryophytes | policy_1 | -9.38 | 10% |
| Bryophytes | policy_2 | -2.67 | 24% |
| Bryophytes | policy_3 | 3.48 | 71% |
| Bryophytes | policy_4 | -1.10 | 33% |
| Diptera | policy_1 | -8.50 | 22% |
| Diptera | policy_2 | -1.94 | 28% |
| Diptera | policy_3 | 3.44 | 61% |
| Diptera | policy_4 | -0.83 | 33% |
| Freshwater invertebrate assemblages | policy_1 | -10.14 | 5% |
| Freshwater invertebrate assemblages | policy_2 | -4.19 | 10% |
| Freshwater invertebrate assemblages | policy_3 | 2.81 | 71% |
| Freshwater invertebrate assemblages | policy_4 | -1.71 | 38% |
| Lichens | policy_1 | -10.48 | 5% |
| Lichens | policy_2 | -5.10 | 14% |
| Lichens | policy_3 | 0.90 | 52% |
| Lichens | policy_4 | -3.81 | 24% |
| Spiders | policy_1 | -5.67 | 33% |
| Spiders | policy_2 | 1.78 | 61% |
| Spiders | policy_3 | 7.11 | 83% |
| Spiders | policy_4 | 2.28 | 61% |
| Vascular plants | policy_1 | -7.05 | 24% |
| Vascular plants | policy_2 | -0.43 | 48% |
| Vascular plants | policy_3 | 5.52 | 67% |
| Vascular plants | policy_4 | 0.67 | 48% |
To make the scenario results easier to interpret, we compute the probability of full recovery for each scenario:
abs_policy_mag_signed (counterfactual signed
magnitude plus policy delta).To quantify uncertainty given a small expert panel, we compute the
recovery probability for each scenario and assumption, and attach
95% percentile intervals from a cluster
bootstrap over experts (resampling experts,id_gj,
with replacement).
# ============================================================
# probability of full recovery (abs_policy_mag_signed >= 0)
# cluster bootstrap over experts (id_gj), by Assumption (A vs B)
# ============================================================
# packagess
library(dplyr)
library(tidyr)
library(purrr)
library(scales)
library(knitr)
library(kableExtra)
# ---- 0) Use the Section 6 object exactly as-is ----
stopifnot(exists("df_AB"))
stopifnot(all(c("assumption","scenario","id_gj","abs_policy_mag_signed") %in% names(df_AB)))
policy_levels <- c("policy_1","policy_2","policy_3","policy_4")
dat <- df_AB %>%
mutate(
assumption = as.character(assumption),
scenario = as.character(scenario)
) %>%
filter(scenario %in% policy_levels)
assumptions <- sort(unique(dat$assumption))
scenarios <- policy_levels
# ---- 1) Define full recovery indicator ----
dat <- dat %>%
mutate(full_recovery = abs_policy_mag_signed >= 0)
# ---- 2) Bootstrap settings ----
set.seed(1)
B <- 5000
# helper: weighted mean for logical indicators under expert-resampling weights
wtd_mean <- function(x, w) {
ok <- !is.na(x) & !is.na(w)
if (!any(ok)) return(NA_real_)
sum(w[ok] * x[ok]) / sum(w[ok])
}
# ---- 3) One cluster-bootstrap draw within ONE assumption ----
boot_once_assumption <- function(df_a) {
experts <- sort(unique(df_a$id_gj[!is.na(df_a$id_gj)]))
Nexp <- length(experts)
samp <- sample(experts, size = Nexp, replace = TRUE)
w <- table(samp)
dfb <- df_a %>%
semi_join(tibble(id_gj = as.integer(names(w))), by = "id_gj") %>%
mutate(.w = as.numeric(w[as.character(id_gj)]))
dfb %>%
group_by(scenario) %>%
summarise(
p = wtd_mean(as.numeric(full_recovery), .w),
.groups = "drop"
)
}
# ---- 4) Run bootstrap separately for A and B ----
boot_draws <- map_dfr(assumptions, function(a) {
df_a <- dat %>% filter(assumption == a)
map_dfr(seq_len(B), function(b) {
boot_once_assumption(df_a) %>%
mutate(assumption = a, b = b)
})
})
# ---- 5) Point estimates (non-bootstrap) ----
point_est <- dat %>%
group_by(assumption, scenario) %>%
summarise(
p_hat = mean(full_recovery, na.rm = TRUE),
n = n(),
.groups = "drop"
)
# ---- 6) Bootstrap percentile CIs ----
boot_ci <- boot_draws %>%
group_by(assumption, scenario) %>%
summarise(
p_lo = quantile(p, 0.025, na.rm = TRUE),
p_med = quantile(p, 0.50, na.rm = TRUE),
p_hi = quantile(p, 0.975, na.rm = TRUE),
.groups = "drop"
)
prob_recovery_AB <- point_est %>%
left_join(boot_ci, by = c("assumption","scenario")) %>%
mutate(
scenario = factor(scenario, levels = policy_levels),
assumption = factor(assumption, levels = assumptions)
) %>%
arrange(assumption, scenario)
# ---- 7) Pretty table ----
prob_recovery_AB %>%
mutate(
p_hat = percent(p_hat, accuracy = 1),
p_lo = percent(p_lo, accuracy = 1),
p_hi = percent(p_hi, accuracy = 1),
ci95 = paste0(p_lo, "–", p_hi)
) %>%
select(assumption, scenario, n, p_hat, ci95) %>%
pivot_wider(
id_cols = c(scenario),
names_from = assumption,
values_from = c(n, p_hat, ci95),
names_glue = "{assumption}_{.value}"
) %>%
kable(caption = "Probability of full recovery (abs_policy_mag_signed ≥ 0%) by scenario and assumption (cluster bootstrap over experts; 95% CI)") %>%
kable_styling(full_width = FALSE)
| scenario | A (rows = all survey_taxa)_n | B (collapsed = unique taxa:habitat_group)_n | A (rows = all survey_taxa)_p_hat | B (collapsed = unique taxa:habitat_group)_p_hat | A (rows = all survey_taxa)_ci95 | B (collapsed = unique taxa:habitat_group)_ci95 |
|---|---|---|---|---|---|---|
| policy_1 | 147 | 126 | 37% | 41% | 17%–58% | 18%–64% |
| policy_2 | 147 | 126 | 61% | 66% | 37%–80% | 41%–85% |
| policy_3 | 147 | 126 | 79% | 79% | 50%–97% | 51%–98% |
| policy_4 | 147 | 126 | 64% | 65% | 33%–90% | 35%–91% |
# ---- 8) Plot: probability with CI (facet by assumption) ----
prob_recovery_AB %>%
ggplot(aes(x = scenario, y = p_hat)) +
geom_point(size = 3) +
geom_errorbar(aes(ymin = p_lo, ymax = p_hi), width = 0.15) +
facet_wrap(~ assumption) +
geom_hline(yintercept=1, linetype="dashed") +
scale_y_continuous(labels = percent_format(accuracy = 1), limits = c(0, 1)) +
labs(
title = "Probability of full recovery (abs_policy_mag_signed ≥ 0%)",
subtitle = "Cluster bootstrap over experts (id_gj), 95% percentile intervals",
x = NULL,
y = "Probability"
) +
theme_minimal()
# ---- 9) context table showing mean absolute outcomes too ----
dat %>%
group_by(assumption, scenario) %>%
summarise(
mean_abs_policy = mean(abs_policy_mag_signed, na.rm = TRUE),
median_abs_policy = median(abs_policy_mag_signed, na.rm = TRUE),
prop_full_recovery = mean(full_recovery, na.rm = TRUE),
.groups = "drop"
) %>%
arrange(assumption, scenario) %>%
kable(digits = 2, caption = "Context: mean/median absolute policy outcome and raw recovery share") %>%
kable_styling(full_width = FALSE)
| assumption | scenario | mean_abs_policy | median_abs_policy | prop_full_recovery |
|---|---|---|---|---|
| A (rows = all survey_taxa) | policy_1 | -8.18 | -5 | 0.37 |
| A (rows = all survey_taxa) | policy_2 | -1.57 | 0 | 0.61 |
| A (rows = all survey_taxa) | policy_3 | 4.23 | 5 | 0.79 |
| A (rows = all survey_taxa) | policy_4 | -0.52 | 0 | 0.64 |
| B (collapsed = unique taxa:habitat_group) | policy_1 | -7.70 | -5 | 0.41 |
| B (collapsed = unique taxa:habitat_group) | policy_2 | -1.01 | 0 | 0.66 |
| B (collapsed = unique taxa:habitat_group) | policy_3 | 4.63 | 5 | 0.79 |
| B (collapsed = unique taxa:habitat_group) | policy_4 | -0.14 | 0 | 0.65 |
Across both assumptions, recovery probabilities increase strongly with policy ambition. Under Assumption A (all survey_taxa rows), the probability that the implied absolute policy outcome is ≥0% rises from 37% under policy_1 to 61% under policy_2, 79% under policy_3, and 64% under policy_4. The bootstrap intervals are wide—reflecting the small expert panel—but clearly shift upward with scenario strength (e.g. policy_3: 50–97%).
Results are highly consistent under Assumption B (collapsed to unique taxa × habitat_group), with probabilities of 41%, 66%, 79%, and 65% for policy_1–4 respectively. The similarity between A and B indicates that aggregation structure does not materially alter the overall recovery signal.
Policy_3 consistently emerges as the strongest scenario: it produces the highest probability of full recovery (~79%) under both assumptions. However, the wide bootstrap intervals (e.g. ~51–98% under B) indicate substantial expert-level uncertainty. Policy_1 remains clearly insufficient, with recovery probabilities below 50% under both assumptions and lower bounds well below 20%. Overall, stronger policy scenarios substantially increase the likelihood of stabilisation or recovery, but do not deliver near-certainty of full recovery across taxa.
Section 6 provides the core “sufficiency” conclusion:
This creates a clear bridge to Section 7: we now test whether this sufficiency verdict is robust given panel size and disagreement.
Section 7 examines whether the headline conclusions (notably: “policy_3 is strongest but still not sufficient on average”) are robust to the small expert panel and to disagreement about magnitude.
We focus on three complementary diagnostics:
To keep uncertainty aligned with the outcome summaries, dispersion and resampling are reported under both assumptions A and B.
consensus_var_AB <- df_AB %>%
filter(str_detect(scenario, "^policy_") | scenario == "counterfactual") %>%
group_by(assumption, taxa, habitat_group, scenario) %>%
summarise(
mean_signed = mean(abs_policy_mag_signed, na.rm = TRUE),
sd_signed = sd(abs_policy_mag_signed, na.rm = TRUE),
n = n(),
.groups = "drop"
)
consensus_summary_AB <- consensus_var_AB %>%
group_by(assumption, scenario) %>%
summarise(
mean_sd = mean(sd_signed, na.rm = TRUE),
median_sd = median(sd_signed, na.rm = TRUE),
.groups = "drop"
)
consensus_summary_AB %>%
mutate(across(where(is.numeric), ~ round(.x, 2))) %>%
kable(caption = "Mean dispersion (SD) in signed magnitude by scenario (Assumptions A and B)") %>%
kable_styling(full_width = FALSE)
| assumption | scenario | mean_sd | median_sd |
|---|---|---|---|
| A (rows = all survey_taxa) | counterfactual | 11.94 | 11.13 |
| A (rows = all survey_taxa) | policy_1 | 12.77 | 12.47 |
| A (rows = all survey_taxa) | policy_2 | 10.67 | 10.05 |
| A (rows = all survey_taxa) | policy_3 | 9.54 | 8.70 |
| A (rows = all survey_taxa) | policy_4 | 10.51 | 9.67 |
| B (collapsed = unique taxa:habitat_group) | counterfactual | 11.92 | 11.13 |
| B (collapsed = unique taxa:habitat_group) | policy_1 | 12.77 | 12.47 |
| B (collapsed = unique taxa:habitat_group) | policy_2 | 10.65 | 10.05 |
| B (collapsed = unique taxa:habitat_group) | policy_3 | 9.51 | 8.70 |
| B (collapsed = unique taxa:habitat_group) | policy_4 | 10.49 | 9.67 |
The dispersion table shows that SD in signed outcomes is lowest under the counterfactual and increases under stronger policy packages, peaking around policy_3:
This pattern is typical of expert elicitation: experts agree more on the existence of baseline pressures than on the extent to which ambitious intervention can offset them. Importantly, the pattern is essentially the same under A and B, suggesting the uncertainty structure is not being driven by survey-row multiplicity.
Policy interpretation: uncertainty is concentrated where decisions are highest leverage (policy_3), so “how close to zero” policy_3 sits should be treated probabilistically rather than as a point estimate, even when the ranking of scenarios is stable.
ggplot(consensus_var_AB, aes(x = mean_signed, y = sd_signed)) +
geom_point(alpha = 0.6) +
facet_grid(assumption ~ scenario) +
labs(
title = "Mean–variance relationship across taxa × habitat_group (A and B)",
x = "Mean signed % outcome",
y = "Standard deviation (expert disagreement)"
) +
theme_minimal()
The mean–variance plot tests whether disagreement tends to increase as projected outcomes become more extreme. A widening spread away from zero (a “funnel” pattern) is a healthy signal: it suggests experts are most aligned around modest changes and diverge most when asked about large recovery or large decline.
In this analysis, the combination of (i) increasing dispersion under ambitious policy and (ii) mean–variance structure supports the interpretation that uncertainty is epistemic and structured (reflecting real limits on knowledge of intervention efficacy), rather than arbitrary disagreement.
certainty_summary_AB <- df_AB %>%
filter(str_detect(scenario, "^policy_") | scenario == "counterfactual") %>%
group_by(assumption, scenario) %>%
summarise(
mean_cert = mean(if_else(scenario == "counterfactual", cf_cert, pol_cert), na.rm = TRUE),
sd_cert = sd(if_else(scenario == "counterfactual", cf_cert, pol_cert), na.rm = TRUE),
.groups = "drop"
)
certainty_summary_AB %>%
mutate(across(where(is.numeric), round, 2)) %>%
kable(caption = "Mean and SD of certainty scores (1–5) (Assumptions A and B)") %>%
kable_styling(full_width = FALSE)
## Warning: There was 1 warning in `mutate()`.
## ℹ In argument: `across(where(is.numeric), round, 2)`.
## Caused by warning:
## ! The `...` argument of `across()` is deprecated as of dplyr 1.1.0.
## Supply arguments directly to `.fns` through an anonymous function instead.
##
## # Previously
## across(a:b, mean, na.rm = TRUE)
##
## # Now
## across(a:b, \(x) mean(x, na.rm = TRUE))
| assumption | scenario | mean_cert | sd_cert |
|---|---|---|---|
| A (rows = all survey_taxa) | counterfactual | 1.97 | 0.57 |
| A (rows = all survey_taxa) | policy_1 | 1.99 | 0.79 |
| A (rows = all survey_taxa) | policy_2 | 1.97 | 0.72 |
| A (rows = all survey_taxa) | policy_3 | 1.93 | 0.75 |
| A (rows = all survey_taxa) | policy_4 | 1.95 | 0.73 |
| B (collapsed = unique taxa:habitat_group) | counterfactual | 1.93 | 0.55 |
| B (collapsed = unique taxa:habitat_group) | policy_1 | 1.97 | 0.79 |
| B (collapsed = unique taxa:habitat_group) | policy_2 | 1.95 | 0.74 |
| B (collapsed = unique taxa:habitat_group) | policy_3 | 1.91 | 0.76 |
| B (collapsed = unique taxa:habitat_group) | policy_4 | 1.93 | 0.75 |
Certainty scores provide a self-reported complement to dispersion. In practice, with small panels, certainty scales can be “sticky” (limited spread), so it is appropriate that the analysis relies more heavily on:
Where certainty is systematically low in the most negative baseline domains, it reinforces the case for carrying uncertainty forward into trajectory modelling rather than over-interpreting point means.
set.seed(123)
scenario_levels_pol <- c("policy_1","policy_2","policy_3","policy_4")
resample_means_any <- function(dat, k, scenario_name, n_iter = 1000){
replicate(n_iter, {
sample_ids <- sample(unique(dat$id_gj), k, replace = FALSE)
dat %>%
filter(id_gj %in% sample_ids, scenario == scenario_name) %>%
summarise(mean_signed = mean(abs_policy_mag_signed, na.rm = TRUE)) %>%
pull(mean_signed)
})
}
resampling_results_AB <- df_AB %>%
filter(str_detect(scenario, "^policy_")) %>%
group_by(assumption) %>%
group_modify(~{
dat <- .x
purrr::map_dfr(scenario_levels_pol, function(sc){
tibble(
scenario = sc,
k2 = sd(resample_means_any(dat, 2, sc)),
k3 = sd(resample_means_any(dat, 3, sc)),
k4 = sd(resample_means_any(dat, 4, sc)),
k5 = sd(resample_means_any(dat, 5, sc)),
k6 = sd(resample_means_any(dat, 6, sc))
)
})
}) %>%
ungroup()
resampling_results_AB %>%
mutate(across(where(is.numeric), round, 2)) %>%
kable(caption = "Resampling SD of mean signed outcome by panel size (Assumptions A & B)") %>%
kable_styling(full_width = FALSE)
| assumption | scenario | k2 | k3 | k4 | k5 | k6 |
|---|---|---|---|---|---|---|
| A (rows = all survey_taxa) | policy_1 | 6.47 | 4.67 | 3.48 | 2.49 | 1.63 |
| A (rows = all survey_taxa) | policy_2 | 5.11 | 3.75 | 2.82 | 2.06 | 1.28 |
| A (rows = all survey_taxa) | policy_3 | 4.30 | 3.09 | 2.34 | 1.72 | 1.14 |
| A (rows = all survey_taxa) | policy_4 | 5.05 | 3.67 | 2.75 | 2.03 | 1.31 |
| B (collapsed = unique taxa:habitat_group) | policy_1 | 6.39 | 4.91 | 3.72 | 2.69 | 1.70 |
| B (collapsed = unique taxa:habitat_group) | policy_2 | 5.35 | 3.90 | 2.94 | 2.11 | 1.41 |
| B (collapsed = unique taxa:habitat_group) | policy_3 | 4.22 | 3.30 | 2.42 | 1.78 | 1.19 |
| B (collapsed = unique taxa:habitat_group) | policy_4 | 5.12 | 3.83 | 2.86 | 2.01 | 1.36 |
resampling_long_AB <- resampling_results_AB %>%
pivot_longer(cols = starts_with("k"), names_to = "panel_size", values_to = "sd_mean") %>%
mutate(panel_size = as.numeric(str_remove(panel_size, "k")))
ggplot(resampling_long_AB, aes(x = panel_size, y = sd_mean)) +
geom_line() +
geom_point() +
facet_grid(assumption ~ scenario) +
labs(
title = "Panel-size sensitivity of mean signed policy outcome (Assumptions A and B)",
x = "Number of experts sampled",
y = "SD of mean signed outcome"
) +
theme_minimal()
The resampling analysis asks: “If we had elicited from a slightly different subset of experts, would the headline means and scenario ranking change?”
The expected and policy-relevant pattern is:
This supports a nuanced conclusion:
Uncertainty diagnostics strengthen (rather than weaken) the credibility of the results:
Together, this supports using the elicitation outputs as inputs to trajectory modelling, with uncertainty propagation and careful interpretation of near-zero outcomes.
This subsection applies the same three agreement/robustness diagnostics used above, but after aggregating outcomes within taxonomic groups. This shows whether expert consensus differs systematically among broad taxa rather than among individual taxa × habitat combinations.
practical note: these chunks assume df_AB and abs_policy_mag_signed already exist exactly as in Section 7 .
## ---- taxon-level-setup ------------------------------------------------------
# Taxonomic group-level aggregation of absolute signed outcomes
# Mirrors Section 7.1 logic, but aggregates within taxon first so that
# agreement/uncertainty is examined BETWEEN experts WITHIN each taxonomic group.
taxon_AB <- df_AB %>%
filter(str_detect(scenario, "^policy_") | scenario == "counterfactual") %>%
group_by(assumption, id_gj, scenario, taxa) %>%
summarise(
mean_signed_taxon = mean(abs_policy_mag_signed, na.rm = TRUE),
.groups = "drop"
)
taxon_AB %>%
summarise(
n_rows = n(),
n_assumptions = n_distinct(assumption),
n_experts = n_distinct(id_gj),
n_scenarios = n_distinct(scenario),
n_taxa = n_distinct(taxa)
) %>%
kable(caption = "Coverage check: taxonomic group-level uncertainty dataset") %>%
kable_styling(full_width = FALSE)
| n_rows | n_assumptions | n_experts | n_scenarios | n_taxa |
|---|---|---|---|---|
| 490 | 2 | 7 | 5 | 7 |
## ---- taxon-dispersion -------------------------------------------------------
# 1) Dispersion in signed outcomes across experts, at taxonomic group level
taxon_consensus_var_AB <- taxon_AB %>%
group_by(assumption, taxa, scenario) %>%
summarise(
mean_signed = mean(mean_signed_taxon, na.rm = TRUE),
sd_signed = sd(mean_signed_taxon, na.rm = TRUE),
n = n(),
.groups = "drop"
)
taxon_consensus_summary_AB <- taxon_consensus_var_AB %>%
group_by(assumption, scenario) %>%
summarise(
mean_sd = mean(sd_signed, na.rm = TRUE),
median_sd = median(sd_signed, na.rm = TRUE),
.groups = "drop"
)
taxon_consensus_summary_AB %>%
mutate(across(where(is.numeric), \(x) round(x, 2))) %>%
kable(caption = "Mean dispersion (SD) in signed outcomes by scenario at taxonomic group level (Assumptions A and B)") %>%
kable_styling(full_width = FALSE)
| assumption | scenario | mean_sd | median_sd |
|---|---|---|---|
| A (rows = all survey_taxa) | counterfactual | 10.73 | 11.09 |
| A (rows = all survey_taxa) | policy_1 | 11.24 | 12.32 |
| A (rows = all survey_taxa) | policy_2 | 9.19 | 9.88 |
| A (rows = all survey_taxa) | policy_3 | 7.91 | 8.49 |
| A (rows = all survey_taxa) | policy_4 | 8.89 | 9.24 |
| B (collapsed = unique taxa:habitat_group) | counterfactual | 10.76 | 11.09 |
| B (collapsed = unique taxa:habitat_group) | policy_1 | 11.28 | 12.32 |
| B (collapsed = unique taxa:habitat_group) | policy_2 | 9.24 | 9.88 |
| B (collapsed = unique taxa:habitat_group) | policy_3 | 7.95 | 8.49 |
| B (collapsed = unique taxa:habitat_group) | policy_4 | 8.91 | 9.24 |
## ---- taxon-dispersion-plot --------------------------------------------------
ggplot(
taxon_consensus_var_AB,
aes(x = scenario, y = sd_signed)
) +
geom_boxplot() +
facet_wrap(~ assumption) +
labs(
title = "Expert disagreement by scenario at taxonomic group level",
x = NULL,
y = "SD across experts"
) +
theme_minimal()
At the taxonomic-group level, expert disagreement under Assumption B is moderate rather than extreme. The counterfactual shows a mean SD of 10.76, and this rises slightly under policy_1 to 11.28, indicating that the weakest intervention introduces a little more variation in expert judgement at broad taxonomic scale. After that, however, the pattern moves back towards lower dispersion under the stronger scenarios, implying that once responses are averaged within taxonomic groups, the panel becomes more rather than less aligned about the expected direction and broad magnitude of policy effects.
This matters because it suggests that a substantial share of the disagreement seen earlier in the more disaggregated analysis is likely to reflect finer within-taxon ecological variation rather than fundamental disagreement over how the main taxonomic groups respond overall. In other words, experts may differ over details within taxa, but at the broader taxonomic level there is a clearer shared signal, particularly for the stronger policy packages. The taxonomic-group dataset underpinning this section covers seven taxa, five scenarios and all seven experts under Assumption B, so these summaries reflect the full panel at this level of aggregation.
## ---- taxon-mean-variance-correlation ----------------------------------------
# Optional compact diagnostic table: correlation between extremity and disagreement
# Uses absolute mean magnitude to ask whether more extreme projected outcomes
# tend to attract more disagreement.
taxon_mv_diag_AB <- taxon_consensus_var_AB %>%
mutate(abs_mean_signed = abs(mean_signed)) %>%
group_by(assumption, scenario) %>%
summarise(
cor_absmean_sd = cor(abs_mean_signed, sd_signed, use = "complete.obs"),
.groups = "drop"
)
taxon_mv_diag_AB %>%
mutate(across(where(is.numeric), \(x) round(x, 2))) %>%
kable(caption = "Correlation between absolute mean outcome and disagreement at taxonomic group level") %>%
kable_styling(full_width = FALSE)
| assumption | scenario | cor_absmean_sd |
|---|---|---|
| A (rows = all survey_taxa) | counterfactual | -0.52 |
| A (rows = all survey_taxa) | policy_1 | -0.89 |
| A (rows = all survey_taxa) | policy_2 | -0.78 |
| A (rows = all survey_taxa) | policy_3 | 0.74 |
| A (rows = all survey_taxa) | policy_4 | -0.59 |
| B (collapsed = unique taxa:habitat_group) | counterfactual | -0.51 |
| B (collapsed = unique taxa:habitat_group) | policy_1 | -0.87 |
| B (collapsed = unique taxa:habitat_group) | policy_2 | -0.76 |
| B (collapsed = unique taxa:habitat_group) | policy_3 | 0.72 |
| B (collapsed = unique taxa:habitat_group) | policy_4 | -0.57 |
The mean–variance diagnostic adds an important nuance. Under Assumption B, the correlation between absolute mean outcome and disagreement is negative for the counterfactual (-0.51), policy_1 (-0.87), policy_2 (-0.76) and policy_4 (-0.57), but positive for policy_3 (+0.72). This indicates that, for most scenarios, taxonomic groups with larger average responses are not the ones attracting the greatest disagreement; if anything, stronger mean responses are associated with slightly tighter agreement. Policy_3 is the exception. Under the strongest scenario, the taxonomic groups expected to do best are also the groups where experts diverge more on magnitude.
Substantively, that is a reassuring rather than contradictory pattern. It suggests that under the strongest package there is still broad directional agreement that some taxonomic groups respond especially well, but less agreement over exactly how large those gains would be. With only seven taxonomic groups, these correlations should not be over-interpreted as precise statistical relationships, but they do support a sensible reading of the plot: uncertainty at taxonomic-group level is primarily about the size of gains under ambitious intervention, not about whether the stronger package is beneficial overall.
## ---- taxon-resampling -------------------------------------------------------
# 3) Resampling / panel-size sensitivity at taxonomic group level
# Here the quantity being stabilised is the mean signed outcome across taxonomic groups.
set.seed(123)
taxon_panel_mean <- function(dat, k) {
sampled_ids <- sample(unique(dat$id_gj), size = k, replace = FALSE)
dat %>%
filter(id_gj %in% sampled_ids) %>%
group_by(assumption, scenario, taxa) %>%
summarise(
mean_signed_taxon = mean(mean_signed_taxon, na.rm = TRUE),
.groups = "drop"
) %>%
group_by(assumption, scenario) %>%
summarise(
panel_mean = mean(mean_signed_taxon, na.rm = TRUE),
.groups = "drop"
)
}
taxon_resample_means <- function(k, assumption_in, scenario_in, n_iter = 1000) {
dat <- taxon_AB %>%
filter(assumption == assumption_in, scenario == scenario_in)
map_dbl(seq_len(n_iter), ~{
sampled_ids <- sample(unique(dat$id_gj), size = k, replace = FALSE)
dat %>%
filter(id_gj %in% sampled_ids) %>%
group_by(taxa) %>%
summarise(
mean_signed_taxon = mean(mean_signed_taxon, na.rm = TRUE),
.groups = "drop"
) %>%
summarise(panel_mean = mean(mean_signed_taxon, na.rm = TRUE)) %>%
pull(panel_mean)
})
}
scenario_levels <- c("counterfactual", "policy_1", "policy_2", "policy_3", "policy_4")
assumption_levels <- unique(taxon_AB$assumption)
panel_sizes <- sort(unique(taxon_AB$id_gj)) |> length() |> seq_len()
taxon_resampling_results_AB <- expand_grid(
assumption = assumption_levels,
scenario = scenario_levels
) %>%
mutate(
k2 = map2_dbl(assumption, scenario, \(a, s) sd(taxon_resample_means(2, a, s))),
k3 = map2_dbl(assumption, scenario, \(a, s) sd(taxon_resample_means(3, a, s))),
k4 = map2_dbl(assumption, scenario, \(a, s) sd(taxon_resample_means(4, a, s))),
k5 = map2_dbl(assumption, scenario, \(a, s) sd(taxon_resample_means(5, a, s))),
k6 = map2_dbl(assumption, scenario, \(a, s) sd(taxon_resample_means(6, a, s))),
k7 = map2_dbl(assumption, scenario, \(a, s) sd(taxon_resample_means(7, a, s)))
)
taxon_resampling_results_AB %>%
mutate(across(where(is.numeric), \(x) round(x, 2))) %>%
kable(caption = "Resampling SD of mean signed outcome by panel size at taxonomic group level") %>%
kable_styling(full_width = FALSE)
| assumption | scenario | k2 | k3 | k4 | k5 | k6 | k7 |
|---|---|---|---|---|---|---|---|
| A (rows = all survey_taxa) | counterfactual | 6.30 | 4.51 | 3.41 | 2.49 | 1.62 | 0 |
| A (rows = all survey_taxa) | policy_1 | 6.52 | 4.74 | 3.56 | 2.65 | 1.66 | 0 |
| A (rows = all survey_taxa) | policy_2 | 5.19 | 3.78 | 2.89 | 2.05 | 1.41 | 0 |
| A (rows = all survey_taxa) | policy_3 | 4.30 | 3.14 | 2.40 | 1.75 | 1.12 | 0 |
| A (rows = all survey_taxa) | policy_4 | 5.10 | 3.73 | 2.80 | 2.00 | 1.31 | 0 |
| B (collapsed = unique taxa:habitat_group) | counterfactual | 6.22 | 4.60 | 3.39 | 2.43 | 1.63 | 0 |
| B (collapsed = unique taxa:habitat_group) | policy_1 | 6.57 | 4.82 | 3.53 | 2.63 | 1.65 | 0 |
| B (collapsed = unique taxa:habitat_group) | policy_2 | 5.33 | 3.84 | 2.89 | 2.08 | 1.33 | 0 |
| B (collapsed = unique taxa:habitat_group) | policy_3 | 4.45 | 3.23 | 2.42 | 1.75 | 1.03 | 0 |
| B (collapsed = unique taxa:habitat_group) | policy_4 | 5.08 | 3.69 | 2.74 | 2.02 | 1.29 | 0 |
## ---- taxon-resampling-plot --------------------------------------------------
taxon_resampling_long_AB <- taxon_resampling_results_AB %>%
pivot_longer(
cols = starts_with("k"),
names_to = "panel_size",
values_to = "sd_mean"
) %>%
mutate(panel_size = as.numeric(str_remove(panel_size, "^k")))
ggplot(
taxon_resampling_long_AB,
aes(x = panel_size, y = sd_mean)
) +
geom_line() +
geom_point() +
facet_grid(assumption ~ scenario) +
labs(
title = "Panel-size sensitivity of mean signed outcome at taxonomic group level",
x = "Number of experts sampled",
y = "SD of mean signed outcome"
) +
theme_minimal()
The resampling analysis indicates that the taxon-level results under Assumption B are reasonably robust to panel composition. For every scenario, the SD of the resampled mean declines steadily as panel size increases, showing that the headline mean outcome stabilises quickly as more experts are included. At k = 6, the SD has already fallen to 1.63 for the counterfactual, 1.65 for policy_1, 1.33 for policy_2, 1.03 for policy_3 and 1.29 for policy_4.
Two features stand out. First, there is no sign here that the taxonomic-group conclusions are being driven by one unusually optimistic or pessimistic respondent; the mean stabilises in a smooth and expected way as k increases. Second, policy_3 is not only the strongest scenario in outcome terms elsewhere in the document, but also the most stable to panel re-composition at this taxonomic level. That means the ranking of policy_3 as the best-performing package appears to be robust, even though the precise magnitude of improvement remains uncertain.
Under Assumption B, the taxonomic-group analysis suggests that the expert panel is more coherent at broad taxonomic level than might be inferred from the more disaggregated results alone. Disagreement is moderate under the counterfactual, increases slightly under the weakest policy package, and then moves back towards lower levels under the stronger scenarios, indicating that broad taxonomic responses become clearer once within-group ecological detail is averaged out.
The mean–variance diagnostic shows that this coherence is not uniform across all scenarios. For most scenarios, stronger average taxonomic responses are not associated with greater disagreement, but under policy_3 the relationship becomes positive, suggesting that the taxonomic groups expected to benefit most are also those for which experts differ most on the size of the gain. This points to magnitude uncertainty at the optimistic end, rather than disagreement over which scenario performs best.
Finally, the resampling results strengthen confidence in the headline conclusion. Taxon-level means stabilise quickly as panel size increases, and policy_3 is the most stable scenario under panel re-sampling as well as the strongest in substantive terms. Taken together, the taxonomic-group results support a clear directional interpretation under Assumption B: the strongest package remains policy_3, and that conclusion appears robust, even though uncertainty should still be carried forward around the size of taxonomic-group gains.
This section is based on Assumption B only.
These additional analyses examine whether expert agreement differs systematically among the specific taxonomic groups included in the elicitation. They first assess variation in disagreement among taxonomic groups within each scenario, and then assess whether some taxonomic groups are consistently associated with greater or lower disagreement when all scenarios are considered together. Only Assumption B is included.
Setup: Assumption B only
## ---- taxon-uncertainty-B-setup ----------------------------------------------
# Restrict to Assumption B only for all additional taxonomic-group analyses
taxon_B <- taxon_AB %>%
filter(assumption == "B (collapsed = unique taxa:habitat_group)")
taxon_B %>%
summarise(
n_rows = n(),
n_experts = n_distinct(id_gj),
n_scenarios = n_distinct(scenario),
n_taxa = n_distinct(taxa)
) %>%
knitr::kable(
caption = "Coverage check for additional taxonomic-group uncertainty analyses (Assumption B only)"
)
| n_rows | n_experts | n_scenarios | n_taxa |
|---|---|---|---|
| 245 | 7 | 5 | 7 |
## ---- taxon-by-scenario-uncertainty-table ------------------------------------
# For each scenario, summarise expert disagreement separately for each taxonomic group
taxon_by_scenario_B <- taxon_B %>%
group_by(scenario, taxa) %>%
summarise(
mean_signed = mean(mean_signed_taxon, na.rm = TRUE),
sd_signed = sd(mean_signed_taxon, na.rm = TRUE),
min_signed = min(mean_signed_taxon, na.rm = TRUE),
max_signed = max(mean_signed_taxon, na.rm = TRUE),
n = n(),
.groups = "drop"
) %>%
arrange(scenario, desc(sd_signed))
taxon_by_scenario_B %>%
mutate(across(where(is.numeric), \(x) round(x, 2))) %>%
knitr::kable(
caption = "Expert disagreement by taxonomic group within each scenario (Assumption B)"
)
| scenario | taxa | mean_signed | sd_signed | min_signed | max_signed | n |
|---|---|---|---|---|---|---|
| counterfactual | Diptera | -12.39 | 12.15 | -30 | 1.67 | 7 |
| counterfactual | Vascular plants | -13.33 | 11.75 | -30 | -1.67 | 7 |
| counterfactual | Beetles | -10.48 | 11.50 | -30 | 1.67 | 7 |
| counterfactual | Spiders | -9.17 | 11.09 | -30 | 1.67 | 7 |
| counterfactual | Bryophytes | -12.62 | 10.04 | -30 | 0.00 | 7 |
| counterfactual | Lichens | -12.61 | 10.02 | -30 | -2.50 | 7 |
| counterfactual | Freshwater invertebrate assemblages | -14.86 | 8.79 | -30 | -2.33 | 7 |
| policy_1 | Spiders | -5.67 | 13.22 | -25 | 4.33 | 7 |
| policy_1 | Vascular plants | -7.05 | 12.76 | -25 | 8.33 | 7 |
| policy_1 | Diptera | -8.50 | 12.51 | -25 | 6.67 | 7 |
| policy_1 | Beetles | -5.71 | 12.32 | -25 | 8.33 | 7 |
| policy_1 | Bryophytes | -9.38 | 10.04 | -25 | 2.67 | 7 |
| policy_1 | Lichens | -9.54 | 9.18 | -25 | 2.50 | 7 |
| policy_1 | Freshwater invertebrate assemblages | -10.14 | 8.95 | -25 | -1.00 | 7 |
| policy_2 | Spiders | 1.78 | 11.49 | -20 | 13.33 | 7 |
| policy_2 | Beetles | 1.95 | 10.62 | -20 | 11.33 | 7 |
| policy_2 | Diptera | -1.94 | 10.04 | -20 | 9.67 | 7 |
| policy_2 | Vascular plants | -0.43 | 9.88 | -20 | 10.00 | 7 |
| policy_2 | Bryophytes | -2.67 | 7.75 | -20 | 2.67 | 7 |
| policy_2 | Lichens | -4.61 | 7.65 | -20 | 2.50 | 7 |
| policy_2 | Freshwater invertebrate assemblages | -4.19 | 7.26 | -20 | 0.33 | 7 |
| policy_3 | Diptera | 3.44 | 9.92 | -10 | 20.00 | 7 |
| policy_3 | Spiders | 7.11 | 9.38 | -10 | 16.67 | 7 |
| policy_3 | Beetles | 6.62 | 9.16 | -10 | 20.00 | 7 |
| policy_3 | Vascular plants | 5.52 | 8.49 | -10 | 15.00 | 7 |
| policy_3 | Freshwater invertebrate assemblages | 2.81 | 6.42 | -10 | 10.33 | 7 |
| policy_3 | Bryophytes | 3.48 | 6.29 | -10 | 10.00 | 7 |
| policy_3 | Lichens | 1.32 | 5.98 | -10 | 7.50 | 7 |
| policy_4 | Diptera | -0.83 | 10.80 | -15 | 17.00 | 7 |
| policy_4 | Beetles | 1.24 | 10.34 | -15 | 17.00 | 7 |
| policy_4 | Vascular plants | 0.67 | 9.77 | -15 | 13.00 | 7 |
| policy_4 | Spiders | 2.28 | 9.24 | -15 | 11.33 | 7 |
| policy_4 | Bryophytes | -1.10 | 7.65 | -15 | 8.00 | 7 |
| policy_4 | Lichens | -3.39 | 7.35 | -15 | 4.50 | 7 |
| policy_4 | Freshwater invertebrate assemblages | -1.71 | 7.21 | -15 | 5.00 | 7 |
#A2. Plot: which taxonomic groups are more uncertain within each scenario?
## ---- taxon-by-scenario-uncertainty-plot -------------------------------------
ggplot(
taxon_by_scenario_B,
aes(x = reorder(taxa, sd_signed), y = sd_signed)
) +
geom_col() +
coord_flip() +
facet_wrap(~ scenario, scales = "free_y") +
labs(
title = "Expert disagreement by taxonomic group within each scenario",
x = NULL,
y = "SD across experts"
) +
theme_minimal()
#A3. Heatmap view for fast comparison across scenarios
## ---- taxon-by-scenario-heatmap ----------------------------------------------
ggplot(
taxon_by_scenario_B,
aes(x = scenario, y = taxa, fill = sd_signed)
) +
geom_tile() +
geom_text(aes(label = round(sd_signed, 1)), size = 3) +
labs(
title = "Heatmap of expert disagreement by taxonomic group and scenario",
x = NULL,
y = NULL,
fill = "SD"
) +
theme_minimal()
Rank taxonomic groups within each scenario by disagreement
## ---- taxon-by-scenario-ranking ----------------------------------------------
taxon_by_scenario_ranks_B <- taxon_by_scenario_B %>%
group_by(scenario) %>%
mutate(
disagreement_rank = min_rank(desc(sd_signed))
) %>%
ungroup() %>%
arrange(scenario, disagreement_rank)
taxon_by_scenario_ranks_B %>%
mutate(across(where(is.numeric), \(x) round(x, 2))) %>%
knitr::kable(
caption = "Ranking of taxonomic groups by expert disagreement within each scenario (Assumption B)"
)
| scenario | taxa | mean_signed | sd_signed | min_signed | max_signed | n | disagreement_rank |
|---|---|---|---|---|---|---|---|
| counterfactual | Diptera | -12.39 | 12.15 | -30 | 1.67 | 7 | 1 |
| counterfactual | Vascular plants | -13.33 | 11.75 | -30 | -1.67 | 7 | 2 |
| counterfactual | Beetles | -10.48 | 11.50 | -30 | 1.67 | 7 | 3 |
| counterfactual | Spiders | -9.17 | 11.09 | -30 | 1.67 | 7 | 4 |
| counterfactual | Bryophytes | -12.62 | 10.04 | -30 | 0.00 | 7 | 5 |
| counterfactual | Lichens | -12.61 | 10.02 | -30 | -2.50 | 7 | 6 |
| counterfactual | Freshwater invertebrate assemblages | -14.86 | 8.79 | -30 | -2.33 | 7 | 7 |
| policy_1 | Spiders | -5.67 | 13.22 | -25 | 4.33 | 7 | 1 |
| policy_1 | Vascular plants | -7.05 | 12.76 | -25 | 8.33 | 7 | 2 |
| policy_1 | Diptera | -8.50 | 12.51 | -25 | 6.67 | 7 | 3 |
| policy_1 | Beetles | -5.71 | 12.32 | -25 | 8.33 | 7 | 4 |
| policy_1 | Bryophytes | -9.38 | 10.04 | -25 | 2.67 | 7 | 5 |
| policy_1 | Lichens | -9.54 | 9.18 | -25 | 2.50 | 7 | 6 |
| policy_1 | Freshwater invertebrate assemblages | -10.14 | 8.95 | -25 | -1.00 | 7 | 7 |
| policy_2 | Spiders | 1.78 | 11.49 | -20 | 13.33 | 7 | 1 |
| policy_2 | Beetles | 1.95 | 10.62 | -20 | 11.33 | 7 | 2 |
| policy_2 | Diptera | -1.94 | 10.04 | -20 | 9.67 | 7 | 3 |
| policy_2 | Vascular plants | -0.43 | 9.88 | -20 | 10.00 | 7 | 4 |
| policy_2 | Bryophytes | -2.67 | 7.75 | -20 | 2.67 | 7 | 5 |
| policy_2 | Lichens | -4.61 | 7.65 | -20 | 2.50 | 7 | 6 |
| policy_2 | Freshwater invertebrate assemblages | -4.19 | 7.26 | -20 | 0.33 | 7 | 7 |
| policy_3 | Diptera | 3.44 | 9.92 | -10 | 20.00 | 7 | 1 |
| policy_3 | Spiders | 7.11 | 9.38 | -10 | 16.67 | 7 | 2 |
| policy_3 | Beetles | 6.62 | 9.16 | -10 | 20.00 | 7 | 3 |
| policy_3 | Vascular plants | 5.52 | 8.49 | -10 | 15.00 | 7 | 4 |
| policy_3 | Freshwater invertebrate assemblages | 2.81 | 6.42 | -10 | 10.33 | 7 | 5 |
| policy_3 | Bryophytes | 3.48 | 6.29 | -10 | 10.00 | 7 | 6 |
| policy_3 | Lichens | 1.32 | 5.98 | -10 | 7.50 | 7 | 7 |
| policy_4 | Diptera | -0.83 | 10.80 | -15 | 17.00 | 7 | 1 |
| policy_4 | Beetles | 1.24 | 10.34 | -15 | 17.00 | 7 | 2 |
| policy_4 | Vascular plants | 0.67 | 9.77 | -15 | 13.00 | 7 | 3 |
| policy_4 | Spiders | 2.28 | 9.24 | -15 | 11.33 | 7 | 4 |
| policy_4 | Bryophytes | -1.10 | 7.65 | -15 | 8.00 | 7 | 5 |
| policy_4 | Lichens | -3.39 | 7.35 | -15 | 4.50 | 7 | 6 |
| policy_4 | Freshwater invertebrate assemblages | -1.71 | 7.21 | -15 | 5.00 | 7 | 7 |
Viewed scenario by scenario, there is a clear but not dramatic taxonomic structure to disagreement. Under the counterfactual, Diptera show the highest disagreement among experts (SD 12.15), followed closely by vascular plants (11.75) and beetles (11.50), whereas freshwater invertebrate assemblages, lichens and bryophytes sit at the lower end of the range. This suggests that even before policy is introduced, some taxa are already harder for the panel to judge consistently than others.
Across the policy scenarios, the same broad pattern persists. Beetles and Diptera tend to remain among the more uncertain groups, while bryophytes and lichens tend to remain among the more certain. At the same time, disagreement generally falls under the stronger policies for most taxa. For example, beetle disagreement declines from 12.32 under policy_1 to 9.16 under policy_3, while bryophytes decline from 10.04 to 6.29 over the same comparison. This implies that stronger interventions do not create taxon-specific confusion so much as reduce it, even though some groups remain harder to judge than others.
The ranking table is useful because it shows that this is not simply noise. Diptera are ranked most uncertain under the counterfactual, and vascular plants and beetles also repeatedly appear near the high-disagreement end of the ordering, whereas lichens and bryophytes tend to appear near the low-disagreement end. In substantive terms, this points to structured uncertainty: the expert panel is not equally certain across the taxonomic spectrum, and those differences are fairly stable across scenarios.
This section is based on Assumption B only throughout.
This approach is the probably a stronger-signal version.
## ---- taxon-overall-uncertainty-table ----------------------------------------
# Pool all scenarios together to test whether some taxonomic groups are
# consistently associated with more disagreement than others
taxon_overall_B <- taxon_B %>%
group_by(taxa) %>%
summarise(
mean_signed = mean(mean_signed_taxon, na.rm = TRUE),
sd_signed = sd(mean_signed_taxon, na.rm = TRUE),
median_signed = median(mean_signed_taxon, na.rm = TRUE),
iqr_signed = IQR(mean_signed_taxon, na.rm = TRUE),
min_signed = min(mean_signed_taxon, na.rm = TRUE),
max_signed = max(mean_signed_taxon, na.rm = TRUE),
n = n(),
.groups = "drop"
) %>%
arrange(desc(sd_signed))
taxon_overall_B %>%
mutate(across(where(is.numeric), \(x) round(x, 2))) %>%
knitr::kable(
caption = "Overall expert disagreement by taxonomic group across all scenarios combined (Assumption B)"
)
| taxa | mean_signed | sd_signed | median_signed | iqr_signed | min_signed | max_signed | n |
|---|---|---|---|---|---|---|---|
| Vascular plants | -2.92 | 12.01 | 0.00 | 15.33 | -30 | 15.00 | 35 |
| Beetles | -1.28 | 11.89 | 1.67 | 15.83 | -30 | 20.00 | 35 |
| Diptera | -4.04 | 11.84 | -1.17 | 13.33 | -30 | 20.00 | 35 |
| Spiders | -0.73 | 11.82 | 3.33 | 14.58 | -30 | 16.67 | 35 |
| Bryophytes | -4.46 | 9.91 | 0.00 | 12.67 | -30 | 10.00 | 35 |
| Freshwater invertebrate assemblages | -5.62 | 9.67 | -5.00 | 10.17 | -30 | 10.33 | 35 |
| Lichens | -5.76 | 9.12 | -3.00 | 10.88 | -30 | 7.50 | 35 |
#B2. Plot: overall disagreement by taxonomic group
## ---- taxon-overall-uncertainty-plot -----------------------------------------
ggplot(
taxon_overall_B,
aes(x = reorder(taxa, sd_signed), y = sd_signed)
) +
geom_col() +
coord_flip() +
labs(
title = "Overall expert disagreement by taxonomic group across all scenarios combined",
x = NULL,
y = "SD across experts"
) +
theme_minimal()
A Raw-distribution plot by taxonomic group across all scenarios is useful because the SD table can hide whether disagreement comes from one or two scenarios only, or is persistent across the whole set.
## ---- taxon-overall-distribution-plot ----------------------------------------
ggplot(
taxon_B,
aes(x = taxa, y = mean_signed_taxon)
) +
geom_boxplot() +
coord_flip() +
labs(
title = "Distribution of expert taxonomic-group outcomes across all scenarios combined",
x = NULL,
y = "Mean signed % outcome"
) +
theme_minimal()
## Warning: Removed 10 rows containing non-finite outside the scale range
## (`stat_boxplot()`).
When all scenarios are pooled, the taxonomic pattern becomes clearer. Vascular plants show the greatest overall disagreement across experts (SD 12.01), followed very closely by beetles (11.89). At the other end, lichens show the lowest disagreement (9.12), with freshwater invertebrate assemblages also relatively low (9.67). This pooled analysis therefore strengthens the impression from the scenario-by-scenario results: vascular plants and beetles are the most uncertain groups overall, whereas lichens and freshwater assemblages are the most consistently judged.
The pooled means also suggest that these differences in uncertainty are not simply tracking pessimism or optimism. For example, vascular plants have both the highest disagreement and a moderately negative overall mean, while lichens have the lowest disagreement despite also showing a clearly negative overall mean. This supports the interpretation that the analysis is picking up genuine differences in the consistency of expert judgement between taxa, rather than just differences in the average sign of the response.
The boxplot-style distribution across all scenarios is especially helpful here because it shows that some taxa have a wider overall spread in outcomes than others. In practice, this means that for groups such as vascular plants and beetles, expert disagreement is not confined to one single scenario but is more persistent across the elicitation as a whole. That makes them good candidates for carrying wider uncertainty bounds forward into any later aggregation or modelling step.
This tests whether the spread across experts differs systematically among taxa.
Setup: Per taxonomic group, calculate within-scenario SD first
## ---- taxon-sd-dataset-for-testing -------------------------------------------
# Create a compact dataset of one disagreement value per taxon per scenario
taxon_sd_for_test_B <- taxon_B %>%
group_by(taxa, scenario) %>%
summarise(
sd_signed = sd(mean_signed_taxon, na.rm = TRUE),
.groups = "drop"
)
taxon_sd_for_test_B %>%
mutate(sd_signed = round(sd_signed, 2)) %>%
knitr::kable(
caption = "Scenario-specific disagreement values used for taxonomic-group comparison tests (Assumption B)"
)
| taxa | scenario | sd_signed |
|---|---|---|
| Beetles | counterfactual | 11.50 |
| Beetles | policy_1 | 12.32 |
| Beetles | policy_2 | 10.62 |
| Beetles | policy_3 | 9.16 |
| Beetles | policy_4 | 10.34 |
| Bryophytes | counterfactual | 10.04 |
| Bryophytes | policy_1 | 10.04 |
| Bryophytes | policy_2 | 7.75 |
| Bryophytes | policy_3 | 6.29 |
| Bryophytes | policy_4 | 7.65 |
| Diptera | counterfactual | 12.15 |
| Diptera | policy_1 | 12.51 |
| Diptera | policy_2 | 10.04 |
| Diptera | policy_3 | 9.92 |
| Diptera | policy_4 | 10.80 |
| Freshwater invertebrate assemblages | counterfactual | 8.79 |
| Freshwater invertebrate assemblages | policy_1 | 8.95 |
| Freshwater invertebrate assemblages | policy_2 | 7.26 |
| Freshwater invertebrate assemblages | policy_3 | 6.42 |
| Freshwater invertebrate assemblages | policy_4 | 7.21 |
| Lichens | counterfactual | 10.02 |
| Lichens | policy_1 | 9.18 |
| Lichens | policy_2 | 7.65 |
| Lichens | policy_3 | 5.98 |
| Lichens | policy_4 | 7.35 |
| Spiders | counterfactual | 11.09 |
| Spiders | policy_1 | 13.22 |
| Spiders | policy_2 | 11.49 |
| Spiders | policy_3 | 9.38 |
| Spiders | policy_4 | 9.24 |
| Vascular plants | counterfactual | 11.75 |
| Vascular plants | policy_1 | 12.76 |
| Vascular plants | policy_2 | 9.88 |
| Vascular plants | policy_3 | 8.49 |
| Vascular plants | policy_4 | 9.77 |
Because there will be few observations per group, a non-parametric test is safer than leaning too hard on ANOVA assumptions.
## ---- taxon-kruskal-test -----------------------------------------------------
taxon_kw_B <- kruskal.test(sd_signed ~ taxa, data = taxon_sd_for_test_B)
taxon_kw_B
##
## Kruskal-Wallis rank sum test
##
## data: sd_signed by taxa
## Kruskal-Wallis chi-squared = 18.05, df = 6, p-value = 0.00611
nb: Only useful if the above suggests differences at all.
## ---- taxon-pairwise-tests ---------------------------------------------------
pairwise.wilcox.test(
x = taxon_sd_for_test_B$sd_signed,
g = taxon_sd_for_test_B$taxa,
p.adjust.method = "BH",
exact = FALSE
)
##
## Pairwise comparisons using Wilcoxon rank sum test with continuity correction
##
## data: taxon_sd_for_test_B$sd_signed and taxon_sd_for_test_B$taxa
##
## Beetles Bryophytes Diptera
## Bryophytes 0.096 - -
## Diptera 0.922 0.181 -
## Freshwater invertebrate assemblages 0.085 0.697 0.085
## Lichens 0.096 0.605 0.096
## Spiders 1.000 0.181 0.922
## Vascular plants 0.922 0.368 0.605
## Freshwater invertebrate assemblages Lichens
## Bryophytes - -
## Diptera - -
## Freshwater invertebrate assemblages - -
## Lichens 0.697 -
## Spiders 0.085 0.096
## Vascular plants 0.096 0.181
## Spiders
## Bryophytes -
## Diptera -
## Freshwater invertebrate assemblages -
## Lichens -
## Spiders -
## Vascular plants 1.000
##
## P value adjustment method: BH
The formal comparison supports the descriptive results. The Kruskal–Wallis test indicates that disagreement differs significantly among taxonomic groups overall (chi-squared = 18.05, df = 6, p = 0.006). This is useful because it shows that the taxonomic structure seen in the tables and plots is unlikely to be a chance feature of a small panel alone.
At the same time, the pairwise Wilcoxon results are mostly non-significant after multiple-testing correction. That suggests the taxonomic signal is real in aggregate, but that the sample is too small to cleanly separate most individual pairs of taxa from one another. In practical terms, this means it is safer to interpret the results as a broad gradient of higher- versus lower-certainty taxa, rather than making strong claims that one named group is definitively more uncertain than another specific group.
This is a sensible outcome given the structure of the analysis. The formal test uses only five scenario-level SD values per taxon, so statistical power is limited. The absence of strong pairwise separation therefore should not be read as absence of taxonomic pattern; rather, it indicates that the pattern is distributed across the whole set of taxa rather than being driven by one stark outlier pair.
This converts disagreement into an intuitive certainty-like metric for quick reading.
Here I define:
higher SD = lower certainty certainty score = inverse-rescaled SD between 0 and 1 within this analysis
So this is purely relative within the taxonomic-group comparison.
## ---- taxon-relative-certainty-score -----------------------------------------
taxon_certainty_B <- taxon_overall_B %>%
mutate(
certainty_score = 1 - (sd_signed - min(sd_signed)) / (max(sd_signed) - min(sd_signed))
) %>%
arrange(desc(certainty_score))
taxon_certainty_B %>%
mutate(across(where(is.numeric), \(x) round(x, 2))) %>%
knitr::kable(
caption = "Relative certainty score by taxonomic group across all scenarios combined (Assumption B only)"
)
| taxa | mean_signed | sd_signed | median_signed | iqr_signed | min_signed | max_signed | n | certainty_score |
|---|---|---|---|---|---|---|---|---|
| Lichens | -5.76 | 9.12 | -3.00 | 10.88 | -30 | 7.50 | 35 | 1.00 |
| Freshwater invertebrate assemblages | -5.62 | 9.67 | -5.00 | 10.17 | -30 | 10.33 | 35 | 0.81 |
| Bryophytes | -4.46 | 9.91 | 0.00 | 12.67 | -30 | 10.00 | 35 | 0.73 |
| Spiders | -0.73 | 11.82 | 3.33 | 14.58 | -30 | 16.67 | 35 | 0.07 |
| Diptera | -4.04 | 11.84 | -1.17 | 13.33 | -30 | 20.00 | 35 | 0.06 |
| Beetles | -1.28 | 11.89 | 1.67 | 15.83 | -30 | 20.00 | 35 | 0.04 |
| Vascular plants | -2.92 | 12.01 | 0.00 | 15.33 | -30 | 15.00 | 35 | 0.00 |
#Optional plot for certainty score
## ---- taxon-certainty-score-plot ---------------------------------------------
ggplot(
taxon_certainty_B,
aes(x = reorder(taxa, certainty_score), y = certainty_score)
) +
geom_col() +
coord_flip() +
labs(
title = "Relative certainty by taxonomic group across all scenarios combined",
x = NULL,
y = "Relative certainty score (0-1)"
) +
theme_minimal()
The relative certainty score provides a compact summary of the same pattern. Lichens emerge as the most certain taxonomic group overall, with a certainty score of 1.00, while freshwater invertebrate assemblages are also near the high-certainty end. At the opposite end, vascular plants sit at the lowest-certainty end of the scale, reflecting their position as the group with the highest pooled disagreement.
This is helpful for interpretation because it converts the spread measure into a more intuitive relative ranking. Read in that way, the expert panel appears comparatively confident about lichen responses and comparatively uncertain about vascular plant and beetle responses. Diptera also remain towards the more uncertain end, especially in the scenario-by-scenario outputs. The certainty score should still be treated as descriptive rather than absolute, but it is a clear way of communicating where uncertainty is concentrated taxonomically.
This section is based on Assumption B only throughout.
These additional taxa-level analyses show that expert disagreement is not evenly distributed across the taxonomic groups included in the elicitation. Instead, there is a structured taxonomic pattern: Diptera, beetles and especially vascular plants tend to attract higher disagreement, while lichens, bryophytes and freshwater invertebrate assemblages tend to attract lower disagreement. This pattern is visible within individual scenarios and becomes clearer when all scenarios are combined.
The scenario-by-scenario results also suggest that stronger interventions generally reduce disagreement within taxa rather than increase it. For several groups, including beetles and bryophytes, disagreement is lower under policy_3 than under policy_1 or the counterfactual. This indicates that stronger packages do not appear to create greater taxonomic ambiguity; instead, they often produce more coherent expectations, even if some groups remain intrinsically harder to judge than others.
The formal test reinforces this interpretation. There is evidence that disagreement differs significantly across taxa overall, but the pairwise comparisons are weak after adjustment, implying a broad taxonomic gradient in certainty rather than sharp separations between many individual taxon pairs. In practical terms, this means later interpretation should treat taxonomic uncertainty as real and structured, but avoid over-claiming precision about exactly which pairwise contrasts are most important.
Taken together, these findings suggest that uncertainty in the elicitation is partly ecological rather than merely stochastic. Some taxonomic groups appear consistently more difficult for experts to judge, and this should be carried forward into interpretation of the policy results. In particular, vascular plants and beetles appear to warrant more caution around magnitude estimates, whereas lichens and freshwater assemblages appear to support relatively tighter inference.
Section 8 shows that taxonomic-group uncertainty is structured rather than random. At the broad taxonomic-group level, disagreement is moderate under the counterfactual and weakest policy package, but tends to fall under the stronger scenarios, indicating that experts become more aligned once responses are averaged within taxa. The additional taxon-by-taxon analyses then show that this agreement is not evenly distributed: some taxa, particularly vascular plants, beetles and often Diptera, are consistently associated with greater disagreement, whereas lichens, bryophytes and freshwater invertebrate assemblages tend to be associated with tighter agreement.
The formal comparison confirms that these taxonomic patterns are meaningful overall, even if many individual pairwise contrasts remain weak after adjustment. In practical terms, Section 8 suggests that later interpretation should treat taxonomic uncertainty as real and structured, with wider caution around magnitude estimates for some groups than for others. It also reinforces the broader scenario conclusion that policy_3 remains the strongest package, while leaving uncertainty around the precise size of gains for the most uncertain taxa.
This subsection repeats the same diagnostics after aggregating outcomes within habitat groups. This shows whether expert agreement is tighter or looser when the elicitation is viewed through broad habitat structure rather than taxonomic structure.
practical note: these chunks assume df_AB and abs_policy_mag_signed already exist exactly as in Section 7.
## ---- habitat-level-setup ----------------------------------------------------
# Habitat group-level aggregation of absolute signed outcomes
# Mirrors the taxon-level chunk, but aggregates within habitat_group first.
habitat_AB <- df_AB %>%
filter(str_detect(scenario, "^policy_") | scenario == "counterfactual") %>%
group_by(assumption, id_gj, scenario, habitat_group) %>%
summarise(
mean_signed_habitat = mean(abs_policy_mag_signed, na.rm = TRUE),
.groups = "drop"
)
habitat_AB %>%
summarise(
n_rows = n(),
n_assumptions = n_distinct(assumption),
n_experts = n_distinct(id_gj),
n_scenarios = n_distinct(scenario),
n_habitat_groups = n_distinct(habitat_group)
) %>%
kable(caption = "Coverage check: habitat group-level uncertainty dataset") %>%
kable_styling(full_width = FALSE)
| n_rows | n_assumptions | n_experts | n_scenarios | n_habitat_groups |
|---|---|---|---|---|
| 210 | 2 | 7 | 5 | 3 |
## ---- habitat-dispersion -----------------------------------------------------
# 1) Dispersion in signed outcomes across experts, at habitat group level
habitat_consensus_var_AB <- habitat_AB %>%
group_by(assumption, habitat_group, scenario) %>%
summarise(
mean_signed = mean(mean_signed_habitat, na.rm = TRUE),
sd_signed = sd(mean_signed_habitat, na.rm = TRUE),
n = n(),
.groups = "drop"
)
habitat_consensus_summary_AB <- habitat_consensus_var_AB %>%
group_by(assumption, scenario) %>%
summarise(
mean_sd = mean(sd_signed, na.rm = TRUE),
median_sd = median(sd_signed, na.rm = TRUE),
.groups = "drop"
)
habitat_consensus_summary_AB %>%
mutate(across(where(is.numeric), \(x) round(x, 2))) %>%
kable(caption = "Mean dispersion (SD) in signed outcomes by scenario at habitat group level (Assumptions A and B)") %>%
kable_styling(full_width = FALSE)
| assumption | scenario | mean_sd | median_sd |
|---|---|---|---|
| A (rows = all survey_taxa) | counterfactual | 10.27 | 10.05 |
| A (rows = all survey_taxa) | policy_1 | 11.15 | 10.65 |
| A (rows = all survey_taxa) | policy_2 | 9.02 | 8.16 |
| A (rows = all survey_taxa) | policy_3 | 7.65 | 6.72 |
| A (rows = all survey_taxa) | policy_4 | 8.85 | 7.92 |
| B (collapsed = unique taxa:habitat_group) | counterfactual | 10.35 | 10.14 |
| B (collapsed = unique taxa:habitat_group) | policy_1 | 11.34 | 10.99 |
| B (collapsed = unique taxa:habitat_group) | policy_2 | 9.18 | 8.46 |
| B (collapsed = unique taxa:habitat_group) | policy_3 | 7.81 | 6.94 |
| B (collapsed = unique taxa:habitat_group) | policy_4 | 8.99 | 8.14 |
## ---- habitat-dispersion-plot ------------------------------------------------
ggplot(
habitat_consensus_var_AB,
aes(x = scenario, y = sd_signed)
) +
geom_boxplot() +
facet_wrap(~ assumption) +
labs(
title = "Expert disagreement by scenario at habitat group level",
x = NULL,
y = "SD across experts"
) +
theme_minimal()
At the habitat-group level, expert disagreement under Assumption B is moderate under the counterfactual and weakest policy package, but declines under the stronger interventions. Mean SD is 10.35 under the counterfactual, rises slightly to 11.34 under policy_1, then falls to 9.18 under policy_2, 7.81 under policy_3, and 8.99 under policy_4. Median SD shows the same pattern. This indicates that, once responses are averaged within the three broad habitat groups, stronger policy packages are associated with tighter agreement across the expert panel rather than greater divergence.
This is notable because it suggests that much of the panel disagreement is being smoothed out when ecological responses are viewed through broad habitat structure. In other words, experts may differ over particular taxa–habitat combinations, but they are more aligned about the overall direction and approximate scale of response at habitat-group level. The habitat-level analysis is based on three habitat groups, five scenarios and all seven experts under Assumption B, so it should be read as a broad structural view of agreement rather than a fine-grained ecological one.
These analyses examine whether expert disagreement differs systematically among the specific habitat groups represented in the elicitation. Results are shown first by habitat group within each scenario, and then across all scenarios combined in order to identify any more persistent habitat-level patterns in certainty or disagreement. Only Assumption B is considered.
## ---- habitat-mean-variance --------------------------------------------------
# 2) Mean–variance relationship at habitat group level
ggplot(
habitat_consensus_var_AB,
aes(x = mean_signed, y = sd_signed)
) +
geom_point(alpha = 0.7) +
facet_grid(assumption ~ scenario) +
labs(
title = "Mean–variance relationship across habitat groups",
x = "Mean signed % outcome",
y = "Standard deviation across experts"
) +
theme_minimal()
## ---- habitat-mean-variance-correlation --------------------------------------
habitat_mv_diag_AB <- habitat_consensus_var_AB %>%
mutate(abs_mean_signed = abs(mean_signed)) %>%
group_by(assumption, scenario) %>%
summarise(
cor_absmean_sd = cor(abs_mean_signed, sd_signed, use = "complete.obs"),
.groups = "drop"
)
habitat_mv_diag_AB %>%
mutate(across(where(is.numeric), \(x) round(x, 2))) %>%
kable(caption = "Correlation between absolute mean outcome and disagreement at habitat group level") %>%
kable_styling(full_width = FALSE)
| assumption | scenario | cor_absmean_sd |
|---|---|---|
| A (rows = all survey_taxa) | counterfactual | -0.97 |
| A (rows = all survey_taxa) | policy_1 | -0.96 |
| A (rows = all survey_taxa) | policy_2 | -0.83 |
| A (rows = all survey_taxa) | policy_3 | 1.00 |
| A (rows = all survey_taxa) | policy_4 | 0.63 |
| B (collapsed = unique taxa:habitat_group) | counterfactual | -0.96 |
| B (collapsed = unique taxa:habitat_group) | policy_1 | -0.96 |
| B (collapsed = unique taxa:habitat_group) | policy_2 | -0.14 |
| B (collapsed = unique taxa:habitat_group) | policy_3 | 1.00 |
| B (collapsed = unique taxa:habitat_group) | policy_4 | 0.84 |
The mean–variance diagnostic suggests that the structure of disagreement changes across scenarios. Under Assumption B, the correlation between absolute mean outcome and disagreement is strongly negative for the counterfactual (-0.96) and policy_1 (-0.96), close to zero for policy_2 (-0.14), and then strongly positive for policy_3 (+1.00) and policy_4 (+0.84). This implies that under the counterfactual and weakest policy package, habitat groups with larger mean responses are not the ones generating the greatest disagreement. By contrast, under the stronger interventions, the habitat groups with the largest projected responses are also the ones with the greatest dispersion among experts.
Substantively, this points to a shift from relatively uniform pessimism under the baseline towards magnitude uncertainty under stronger policy. Under policy_3 and policy_4, the panel appears broadly aligned that habitat groups improve, but less aligned on exactly how large those gains would be in the best-performing habitats. That interpretation is consistent with the wider habitat-group results, which show strong positive uplifts across all three habitat groups under the stronger scenarios, especially under policy_3.
## ---- habitat-resampling -----------------------------------------------------
# 3) Resampling / panel-size sensitivity at habitat group level
set.seed(123)
habitat_resample_means <- function(k, assumption_in, scenario_in, n_iter = 1000) {
dat <- habitat_AB %>%
filter(assumption == assumption_in, scenario == scenario_in)
map_dbl(seq_len(n_iter), ~{
sampled_ids <- sample(unique(dat$id_gj), size = k, replace = FALSE)
dat %>%
filter(id_gj %in% sampled_ids) %>%
group_by(habitat_group) %>%
summarise(
mean_signed_habitat = mean(mean_signed_habitat, na.rm = TRUE),
.groups = "drop"
) %>%
summarise(panel_mean = mean(mean_signed_habitat, na.rm = TRUE)) %>%
pull(panel_mean)
})
}
habitat_resampling_results_AB <- expand_grid(
assumption = unique(habitat_AB$assumption),
scenario = c("counterfactual", "policy_1", "policy_2", "policy_3", "policy_4")
) %>%
mutate(
k2 = map2_dbl(assumption, scenario, \(a, s) sd(habitat_resample_means(2, a, s))),
k3 = map2_dbl(assumption, scenario, \(a, s) sd(habitat_resample_means(3, a, s))),
k4 = map2_dbl(assumption, scenario, \(a, s) sd(habitat_resample_means(4, a, s))),
k5 = map2_dbl(assumption, scenario, \(a, s) sd(habitat_resample_means(5, a, s))),
k6 = map2_dbl(assumption, scenario, \(a, s) sd(habitat_resample_means(6, a, s))),
k7 = map2_dbl(assumption, scenario, \(a, s) sd(habitat_resample_means(7, a, s)))
)
habitat_resampling_results_AB %>%
mutate(across(where(is.numeric), \(x) round(x, 2))) %>%
kable(caption = "Resampling SD of mean signed outcome by panel size at habitat group level") %>%
kable_styling(full_width = FALSE)
| assumption | scenario | k2 | k3 | k4 | k5 | k6 | k7 |
|---|---|---|---|---|---|---|---|
| A (rows = all survey_taxa) | counterfactual | 6.04 | 4.29 | 3.26 | 2.39 | 1.56 | 0 |
| A (rows = all survey_taxa) | policy_1 | 6.32 | 4.65 | 3.51 | 2.61 | 1.64 | 0 |
| A (rows = all survey_taxa) | policy_2 | 5.00 | 3.69 | 2.83 | 1.99 | 1.37 | 0 |
| A (rows = all survey_taxa) | policy_3 | 4.16 | 2.99 | 2.30 | 1.70 | 1.08 | 0 |
| A (rows = all survey_taxa) | policy_4 | 4.87 | 3.58 | 2.69 | 1.93 | 1.26 | 0 |
| B (collapsed = unique taxa:habitat_group) | counterfactual | 6.01 | 4.44 | 3.29 | 2.35 | 1.58 | 0 |
| B (collapsed = unique taxa:habitat_group) | policy_1 | 6.57 | 4.86 | 3.54 | 2.66 | 1.68 | 0 |
| B (collapsed = unique taxa:habitat_group) | policy_2 | 5.26 | 3.80 | 2.87 | 2.07 | 1.33 | 0 |
| B (collapsed = unique taxa:habitat_group) | policy_3 | 4.34 | 3.20 | 2.40 | 1.74 | 1.03 | 0 |
| B (collapsed = unique taxa:habitat_group) | policy_4 | 4.98 | 3.64 | 2.70 | 1.99 | 1.27 | 0 |
## ---- habitat-resampling-plot ------------------------------------------------
habitat_resampling_long_AB <- habitat_resampling_results_AB %>%
pivot_longer(
cols = starts_with("k"),
names_to = "panel_size",
values_to = "sd_mean"
) %>%
mutate(panel_size = as.numeric(str_remove(panel_size, "^k")))
ggplot(
habitat_resampling_long_AB,
aes(x = panel_size, y = sd_mean)
) +
geom_line() +
geom_point() +
facet_grid(assumption ~ scenario) +
labs(
title = "Panel-size sensitivity of mean signed outcome at habitat group level",
x = "Number of experts sampled",
y = "SD of mean signed outcome"
) +
theme_minimal()
The resampling analysis indicates that the habitat-group results under Assumption B are fairly robust to panel composition. For all scenarios, the SD of the resampled mean declines steadily as more experts are included, showing that panel-level conclusions stabilise in the expected way with increasing panel size. Among the policy scenarios, policy_3 is the most stable at larger panel sizes, with SD falling from 4.34 at k = 2 to 1.03 at k = 6. Policy_4 and policy_2 also stabilise clearly, while policy_1 remains the least stable of the policy packages.
This strengthens confidence in the habitat-level ranking of scenarios. Although there remains some uncertainty over exact magnitude, there is little evidence that the broad conclusions are being driven by one or two particular respondents. The fact that policy_3 combines the lowest dispersion at habitat-group level with the greatest resampling stability suggests that its stronger performance is not only a feature of the central estimate, but is also comparatively robust to moderate changes in panel composition.
Under Assumption B, the habitat-group analysis suggests that expert agreement becomes stronger once outcomes are viewed through broad habitat structure, particularly under the stronger policy scenarios. Disagreement is moderate under the counterfactual and policy_1, but falls under policy_2 and is lowest under policy_3, before increasing slightly again under policy_4. This indicates that habitat-level responses are interpreted more coherently by the panel than the more disaggregated taxa × habitat results might imply.
The mean–variance diagnostic adds an important qualification. Under the baseline and weakest policy package, more extreme habitat-group means are not associated with greater disagreement, whereas under policy_3 and policy_4 the relationship turns strongly positive. This suggests that uncertainty under stronger intervention is increasingly about the size of gains in the best-performing habitat groups, rather than about whether those gains occur at all.
Finally, the resampling results suggest that the habitat-level conclusions are robust to the composition of the seven-person panel. Resampled variability declines smoothly as panel size increases across all scenarios, and policy_3 emerges as the most stable of the policy options at larger panel sizes. Taken together, these results support a clear interpretation under Assumption B: stronger policies, especially policy_3, generate both better and more coherent habitat-level outcomes, with remaining uncertainty concentrated primarily around magnitude rather than direction.
This section is based on Assumption B only throughout.
These additional analyses examine whether expert disagreement differs systematically among the specific habitat groups included in the elicitation. Results are shown first by habitat group within each scenario, and then across all scenarios combined in order to identify any more persistent habitat-level patterns in certainty or disagreement. Only Assumption B is considered.
Set-up
## ---- habitat-uncertainty-B-setup --------------------------------------------
# Restrict to Assumption B only for all additional habitat-group analyses
habitat_B <- habitat_AB %>%
filter(assumption == "B (collapsed = unique taxa:habitat_group)")
habitat_B %>%
summarise(
n_rows = n(),
n_experts = n_distinct(id_gj),
n_scenarios = n_distinct(scenario),
n_habitat_groups = n_distinct(habitat_group)
) %>%
knitr::kable(
caption = "Coverage check for additional habitat-group uncertainty analyses (Assumption B only)"
)
| n_rows | n_experts | n_scenarios | n_habitat_groups |
|---|---|---|---|
| 105 | 7 | 5 | 3 |
## ---- habitat-by-scenario-uncertainty-table ----------------------------------
# For each scenario, summarise expert disagreement separately for each habitat group
habitat_by_scenario_B <- habitat_B %>%
group_by(scenario, habitat_group) %>%
summarise(
mean_signed = mean(mean_signed_habitat, na.rm = TRUE),
sd_signed = sd(mean_signed_habitat, na.rm = TRUE),
min_signed = min(mean_signed_habitat, na.rm = TRUE),
max_signed = max(mean_signed_habitat, na.rm = TRUE),
n = n(),
.groups = "drop"
) %>%
arrange(scenario, desc(sd_signed))
habitat_by_scenario_B %>%
mutate(across(where(is.numeric), \(x) round(x, 2))) %>%
knitr::kable(
caption = "Expert disagreement by habitat group within each scenario (Assumption B)"
)
| scenario | habitat_group | mean_signed | sd_signed | min_signed | max_signed | n |
|---|---|---|---|---|---|---|
| counterfactual | Woodland | -8.45 | 11.47 | -30 | 2.50 | 7 |
| counterfactual | Open habitats | -13.20 | 10.14 | -30 | -1.50 | 7 |
| counterfactual | Freshwater/Wetland | -13.49 | 9.45 | -30 | -0.39 | 7 |
| policy_1 | Woodland | -4.17 | 13.16 | -25 | 12.50 | 7 |
| policy_1 | Open habitats | -8.62 | 10.99 | -25 | 1.83 | 7 |
| policy_1 | Freshwater/Wetland | -8.93 | 9.88 | -25 | 0.83 | 7 |
| policy_2 | Woodland | 2.21 | 11.25 | -20 | 15.00 | 7 |
| policy_2 | Open habitats | -1.80 | 8.46 | -20 | 3.67 | 7 |
| policy_2 | Freshwater/Wetland | -2.58 | 7.82 | -20 | 2.17 | 7 |
| policy_3 | Woodland | 6.99 | 9.84 | -10 | 18.75 | 7 |
| policy_3 | Freshwater/Wetland | 3.98 | 6.94 | -10 | 11.78 | 7 |
| policy_3 | Open habitats | 3.52 | 6.64 | -10 | 10.58 | 7 |
| policy_4 | Woodland | 1.99 | 10.75 | -15 | 15.33 | 7 |
| policy_4 | Freshwater/Wetland | -0.51 | 8.14 | -15 | 9.89 | 7 |
| policy_4 | Open habitats | -1.29 | 8.10 | -15 | 8.58 | 7 |
## ---- habitat-by-scenario-uncertainty-plot -----------------------------------
ggplot(
habitat_by_scenario_B,
aes(x = reorder(habitat_group, sd_signed), y = sd_signed)
) +
geom_col() +
coord_flip() +
facet_wrap(~ scenario, scales = "free_y") +
labs(
title = "Expert disagreement by habitat group within each scenario",
x = NULL,
y = "SD across experts"
) +
theme_minimal()
#### Heatmap view for fast comparison across scenarios
## ---- habitat-by-scenario-heatmap --------------------------------------------
ggplot(
habitat_by_scenario_B,
aes(x = scenario, y = habitat_group, fill = sd_signed)
) +
geom_tile() +
geom_text(aes(label = round(sd_signed, 1)), size = 3) +
labs(
title = "Heatmap of expert disagreement by habitat group and scenario",
x = NULL,
y = NULL,
fill = "SD"
) +
theme_minimal()
Rank habitat groups within each scenario by disagreement
## ---- habitat-by-scenario-ranking --------------------------------------------
habitat_by_scenario_ranks_B <- habitat_by_scenario_B %>%
group_by(scenario) %>%
mutate(
disagreement_rank = min_rank(desc(sd_signed))
) %>%
ungroup() %>%
arrange(scenario, disagreement_rank)
habitat_by_scenario_ranks_B %>%
mutate(across(where(is.numeric), \(x) round(x, 2))) %>%
knitr::kable(
caption = "Ranking of habitat groups by expert disagreement within each scenario (Assumption B)"
)
| scenario | habitat_group | mean_signed | sd_signed | min_signed | max_signed | n | disagreement_rank |
|---|---|---|---|---|---|---|---|
| counterfactual | Woodland | -8.45 | 11.47 | -30 | 2.50 | 7 | 1 |
| counterfactual | Open habitats | -13.20 | 10.14 | -30 | -1.50 | 7 | 2 |
| counterfactual | Freshwater/Wetland | -13.49 | 9.45 | -30 | -0.39 | 7 | 3 |
| policy_1 | Woodland | -4.17 | 13.16 | -25 | 12.50 | 7 | 1 |
| policy_1 | Open habitats | -8.62 | 10.99 | -25 | 1.83 | 7 | 2 |
| policy_1 | Freshwater/Wetland | -8.93 | 9.88 | -25 | 0.83 | 7 | 3 |
| policy_2 | Woodland | 2.21 | 11.25 | -20 | 15.00 | 7 | 1 |
| policy_2 | Open habitats | -1.80 | 8.46 | -20 | 3.67 | 7 | 2 |
| policy_2 | Freshwater/Wetland | -2.58 | 7.82 | -20 | 2.17 | 7 | 3 |
| policy_3 | Woodland | 6.99 | 9.84 | -10 | 18.75 | 7 | 1 |
| policy_3 | Freshwater/Wetland | 3.98 | 6.94 | -10 | 11.78 | 7 | 2 |
| policy_3 | Open habitats | 3.52 | 6.64 | -10 | 10.58 | 7 | 3 |
| policy_4 | Woodland | 1.99 | 10.75 | -15 | 15.33 | 7 | 1 |
| policy_4 | Freshwater/Wetland | -0.51 | 8.14 | -15 | 9.89 | 7 | 2 |
| policy_4 | Open habitats | -1.29 | 8.10 | -15 | 8.58 | 7 | 3 |
Below section text is based on Assumption B only throughout.
At the habitat-group level, disagreement is not evenly distributed across the three habitat groupings. Woodland is the most uncertain habitat group under both the counterfactual (SD 11.47) and the weakest policy package (SD 13.16), while Freshwater/Wetland and Open habitats are somewhat lower and fairly similar to one another under those same scenarios. Under the stronger scenarios, disagreement declines for all three habitat groups, but the ranking remains broadly similar: Woodland continues to sit at the high-disagreement end, while Open habitats and Freshwater/Wetland tend to be somewhat lower.
This suggests that uncertainty is not simply a property of the policy scenario itself. Some of it is habitat-specific. In particular, Woodland appears to generate the widest spread of expert judgement across several scenarios, implying greater uncertainty about the size or consistency of projected woodland responses. By contrast, the two non-woodland groups appear to elicit somewhat tighter expert agreement, especially under the stronger packages.
A second notable pattern is that disagreement generally falls under the stronger packages, particularly under policy_3. For Freshwater/Wetland, SD falls from 10.82 under the counterfactual to 6.94 under policy_3; for Open habitats it falls from 10.14 to 6.64; and for Woodland from 11.47 to 9.84. This implies that the strongest package does not create greater habitat-level ambiguity. Rather, it appears to produce a more coherent expert signal across all three habitat groups, even though Woodland remains the least certain of the three.
This section is based on Assumption B only throughout.
This approach is the probably a stronger-signal version.
## ---- habitat-overall-uncertainty-table --------------------------------------
# Pool all scenarios together to test whether some habitat groups are
# consistently associated with more disagreement than others
habitat_overall_B <- habitat_B %>%
group_by(habitat_group) %>%
summarise(
mean_signed = mean(mean_signed_habitat, na.rm = TRUE),
sd_signed = sd(mean_signed_habitat, na.rm = TRUE),
median_signed = median(mean_signed_habitat, na.rm = TRUE),
iqr_signed = IQR(mean_signed_habitat, na.rm = TRUE),
min_signed = min(mean_signed_habitat, na.rm = TRUE),
max_signed = max(mean_signed_habitat, na.rm = TRUE),
n = n(),
.groups = "drop"
) %>%
arrange(desc(sd_signed))
habitat_overall_B %>%
mutate(across(where(is.numeric), \(x) round(x, 2))) %>%
knitr::kable(
caption = "Overall expert disagreement by habitat group across all scenarios combined (Assumption B)"
)
| habitat_group | mean_signed | sd_signed | median_signed | iqr_signed | min_signed | max_signed | n |
|---|---|---|---|---|---|---|---|
| Woodland | -0.29 | 11.99 | 1.67 | 15.42 | -30 | 18.75 | 35 |
| Open habitats | -4.28 | 10.36 | 0.00 | 12.79 | -30 | 10.58 | 35 |
| Freshwater/Wetland | -4.31 | 10.17 | -1.67 | 11.71 | -30 | 11.78 | 35 |
## ---- habitat-overall-uncertainty-plot ---------------------------------------
ggplot(
habitat_overall_B,
aes(x = reorder(habitat_group, sd_signed), y = sd_signed)
) +
geom_col() +
coord_flip() +
labs(
title = "Overall expert disagreement by habitat group across all scenarios combined",
x = NULL,
y = "SD across experts"
) +
theme_minimal()
## ---- habitat-overall-distribution-plot --------------------------------------
ggplot(
habitat_B,
aes(x = habitat_group, y = mean_signed_habitat)
) +
geom_boxplot() +
coord_flip() +
labs(
title = "Distribution of expert habitat-group outcomes across all scenarios combined",
x = NULL,
y = "Mean signed % outcome"
) +
theme_minimal()
This tests whether the spread across experts differs systematically among habitat groups.
Seup: per habitat group, we calculate within-scenario SD first
## ---- habitat-sd-dataset-for-testing -----------------------------------------
# Create a compact dataset of one disagreement value per habitat group per scenario
habitat_sd_for_test_B <- habitat_B %>%
group_by(habitat_group, scenario) %>%
summarise(
sd_signed = sd(mean_signed_habitat, na.rm = TRUE),
.groups = "drop"
)
habitat_sd_for_test_B %>%
mutate(sd_signed = round(sd_signed, 2)) %>%
knitr::kable(
caption = "Scenario-specific disagreement values used for habitat-group comparison tests (Assumption B)"
)
| habitat_group | scenario | sd_signed |
|---|---|---|
| Freshwater/Wetland | counterfactual | 9.45 |
| Freshwater/Wetland | policy_1 | 9.88 |
| Freshwater/Wetland | policy_2 | 7.82 |
| Freshwater/Wetland | policy_3 | 6.94 |
| Freshwater/Wetland | policy_4 | 8.14 |
| Open habitats | counterfactual | 10.14 |
| Open habitats | policy_1 | 10.99 |
| Open habitats | policy_2 | 8.46 |
| Open habitats | policy_3 | 6.64 |
| Open habitats | policy_4 | 8.10 |
| Woodland | counterfactual | 11.47 |
| Woodland | policy_1 | 13.16 |
| Woodland | policy_2 | 11.25 |
| Woodland | policy_3 | 9.84 |
| Woodland | policy_4 | 10.75 |
## ---- habitat-kruskal-test ---------------------------------------------------
habitat_kw_B <- kruskal.test(sd_signed ~ habitat_group, data = habitat_sd_for_test_B)
habitat_kw_B
##
## Kruskal-Wallis rank sum test
##
## data: sd_signed by habitat_group
## Kruskal-Wallis chi-squared = 6.86, df = 2, p-value = 0.03239
## ---- habitat-pairwise-tests -------------------------------------------------
pairwise.wilcox.test(
x = habitat_sd_for_test_B$sd_signed,
g = habitat_sd_for_test_B$habitat_group,
p.adjust.method = "BH",
exact = FALSE
)
##
## Pairwise comparisons using Wilcoxon rank sum test with continuity correction
##
## data: habitat_sd_for_test_B$sd_signed and habitat_sd_for_test_B$habitat_group
##
## Freshwater/Wetland Open habitats
## Open habitats 0.676 -
## Woodland 0.065 0.090
##
## P value adjustment method: BH
This converts disagreement into a relative certainty-like metric for quick reading.
## ---- habitat-relative-certainty-score ---------------------------------------
habitat_certainty_B <- habitat_overall_B %>%
mutate(
certainty_score = 1 - (sd_signed - min(sd_signed)) / (max(sd_signed) - min(sd_signed))
) %>%
arrange(desc(certainty_score))
habitat_certainty_B %>%
mutate(across(where(is.numeric), \(x) round(x, 2))) %>%
knitr::kable(
caption = "Relative certainty score by habitat group across all scenarios combined (Assumption B only)"
)
| habitat_group | mean_signed | sd_signed | median_signed | iqr_signed | min_signed | max_signed | n | certainty_score |
|---|---|---|---|---|---|---|---|---|
| Freshwater/Wetland | -4.31 | 10.17 | -1.67 | 11.71 | -30 | 11.78 | 35 | 1.00 |
| Open habitats | -4.28 | 10.36 | 0.00 | 12.79 | -30 | 10.58 | 35 | 0.89 |
| Woodland | -0.29 | 11.99 | 1.67 | 15.42 | -30 | 18.75 | 35 | 0.00 |
## ---- habitat-certainty-score-plot -------------------------------------------
ggplot(
habitat_certainty_B,
aes(x = reorder(habitat_group, certainty_score), y = certainty_score)
) +
geom_col() +
coord_flip() +
labs(
title = "Relative certainty by habitat group across all scenarios combined",
x = NULL,
y = "Relative certainty score (0-1)"
) +
theme_minimal()
When all scenarios are considered together, the habitat-level pattern becomes clearer. Woodland shows the greatest overall disagreement across experts, while Freshwater/Wetland shows the lowest and therefore the highest relative certainty. Open habitats sit between the two. The relative certainty score puts Freshwater/Wetland at 1.00, with Open habitats only slightly lower, and Woodland clearly at the bottom of the scale.
This pooled result strengthens the interpretation from the scenario-by-scenario outputs. Woodland is not only somewhat more uncertain in individual scenarios, but appears to be persistently more uncertain across the elicitation as a whole. Freshwater/Wetland, by contrast, appears to be the habitat group for which the panel is most consistently aligned. That does not mean experts are optimistic about Freshwater/Wetland outcomes — its pooled mean remains negative — but rather that they are relatively consistent in that assessment.
The formal comparison supports treating this as a real habitat-level pattern. The Kruskal–Wallis test indicates that disagreement differs significantly across habitat groups overall (chi-squared = 6.86, df = 2, p = 0.032). At the same time, the pairwise Wilcoxon tests are weaker after adjustment, with Woodland tending towards higher disagreement relative to the other two groups but without strong pairwise separation at conventional thresholds. This suggests a broad habitat-level gradient in certainty, rather than sharply distinct and fully separable categories.
Below section text is based on Assumption B only throughout.
Taken together, these additional habitat-level analyses show that expert disagreement is structured not only by scenario, but also by habitat group. Woodland is consistently the most uncertain habitat grouping, while Freshwater/Wetland is the most certain and Open habitats generally intermediate. This pattern is visible both within individual scenarios and when all scenarios are pooled.
The scenario-specific results also suggest that stronger policy packages tend to reduce disagreement across all habitat groups, especially under policy_3. This indicates that the strongest package is not merely associated with larger projected effects, but also with a clearer shared signal among experts at habitat-group level. Remaining uncertainty is therefore better understood as uncertainty about the precise magnitude of gains, especially in Woodland, rather than as disagreement about the basic direction of response.
The formal tests reinforce this interpretation. There is evidence of a statistically meaningful habitat-group pattern in disagreement overall, but with limited power to separate individual pairs cleanly. In practical terms, this supports carrying forward habitat-structured uncertainty into later interpretation: woodland estimates should be treated with somewhat greater caution, whereas freshwater/wetland and open-habitat estimates appear to support somewhat tighter inference.
Section 9 shows that habitat-group uncertainty is also structured, but across a much smaller set of groups. At the broad habitat-group level, disagreement under Assumption B is moderate under the counterfactual and policy_1, but declines under the stronger scenarios, especially under policy_3. The more detailed habitat-group analyses then show that Woodland is consistently the most uncertain habitat grouping, while Freshwater/Wetland is the most certain and Open habitats is generally intermediate.
This means that habitat-level uncertainty is not just a reflection of scenario strength. Some habitat contexts appear intrinsically harder for the panel to judge. Even so, the stronger scenarios produce a clearer shared signal than the weaker ones, and the formal tests suggest that the habitat-group pattern is meaningful overall. In practical terms, Section 9 supports carrying forward habitat-structured uncertainty, with somewhat greater caution around woodland responses and somewhat greater confidence in the relative consistency of freshwater/wetland and open-habitat assessments.
Important: Below text refers to Assumption B only throughout. See Appendix A for an older version, which includes Assumption A reults.
The combined message from Sections 7–9 under Assumption B is clear. The expert panel gives a strongly pessimistic counterfactual to 2042, and all four policy packages are judged to improve on that baseline, with a stable ranking in which policy_3 performs best, policy_1 performs worst, and policy_2 and policy_4 occupy an intermediate position. However, a positive policy uplift is not the same thing as recovery: even under the strongest package, mean absolute outcomes remain below zero, indicating that policy improvement does not fully offset the expected background decline.
The uncertainty analysis sharpens rather than weakens that conclusion. At the overall level, disagreement is real but coherent. It is not random noise, nor is it concentrated in a way that undermines the central ranking of scenarios. Resampling shows that headline means stabilise as panel size increases, and supports confidence in the directional result and in the relative ranking of policy options, while leaving the precise distance from decline to stability under the strongest package more uncertain. In other words, the panel is much more certain that policy helps than it is about exactly how close the best package gets to zero net change.
Sections 8 and 9 then show that this uncertainty is ecologically structured. Some taxa are consistently harder to judge than others, with vascular plants, beetles and often Diptera generating wider spreads of opinion, whereas lichens, bryophytes and freshwater invertebrate assemblages tend to generate tighter agreement. Similarly, some habitat groups are more uncertain than others, with Woodland emerging as the least certain habitat grouping and Freshwater/Wetland the most certain. This means the panel is not equally confident across the ecological space represented in the elicitation. The strongest inferences can therefore be made about the broad scenario ordering and the existence of residual decline, while somewhat greater caution is needed when interpreting the magnitude of gains in the most uncertain taxa and habitats.
Taken together, Sections 7–9 imply three things for interpretation. First, the elicitation provides a robust signal that baseline pressures continue to drive decline by 2042. Second, it provides an equally robust signal that stronger policy packages improve outcomes, with policy_3 consistently emerging as the best-performing option. Third, it shows that the remaining uncertainty is concentrated mainly around how much improvement is achieved, especially where responses are closest to the stability threshold and in the ecological groupings where disagreement is intrinsically higher. This is exactly the pattern one would expect from a credible elicitation: strong agreement on direction and ordering, but less certainty about precise magnitude at the optimistic edge.
Under Assumption B, the central implication for the extinction-risk target is that the panel does not expect the system to recover to a positive mean state by 2042 under the currently defined packages, even though all packages improve on the counterfactual. The strongest scenario is judged to bring the system closest to stability, but not to eliminate decline in the mean. This matters because ERLI-relevant interpretation should distinguish clearly between beneficial intervention and sufficient intervention. The elicitation supports the former strongly, but only weakly supports the latter.
The results also suggest that any future modelling or policy interpretation should pay particular attention to the persistence of residual pockets of decline. Under the strongest package, many units improve and many are stable or positive, but a material minority remain negative. Those negative tails are what keep the mean below zero. For ERLI interpretation, that means progress could be real and substantial while still falling short of the level needed to reverse the overall extinction-risk trajectory.
At a more ecological level, the habitat results suggest that recovery constraints are not evenly distributed. Freshwater/Wetland and Open habitats remain especially important because they continue to show negative mean outcomes even under the strongest package, while Woodland comes closest to neutrality. The taxonomic results suggest an analogous pattern: some taxa contribute more heavily to uncertainty and may also be more likely to contain those remaining pockets of decline. This does not overturn the overall scenario ranking, but it does imply that achieving the ERLI target may require not only a strong overall package, but also sharper attention to the taxa and habitats where decline is hardest to arrest and expert uncertainty remains greatest.
Under Assumption B, the elicitation outputs are best used in three complementary ways.
This means the elicitation should not be summarised simply as “policy_3 solves the problem” or, conversely, as “uncertainty is too large to say anything useful.” Neither would be justified. A more accurate reading is that the panel gives a clear directional and comparative signal, but that the magnitude of remaining decline versus near-stability remains uncertain enough to warrant explicit uncertainty bounds in any downstream ERLI modelling. That is particularly important because the strongest package is the one that is both most promising and most policy-relevant, precisely because it is the package closest to the threshold between continued decline and stabilisation.
Under Assumption B, the expert elicitation indicates that the extinction-risk baseline to 2042 is strongly negative, that all policy packages improve on that baseline, and that policy_3 is the most effective package. However, the elicitation does not support a confident claim that even the strongest package is sufficient to produce positive mean outcomes by 2042. Instead, it supports a more cautious conclusion: the strongest package likely delivers substantial improvement and may bring some ecological groupings close to stability, but residual decline remains likely in the mean and is concentrated in particular taxa and habitats.
The uncertainty diagnostics add confidence to this overall interpretation. They suggest that the scenario ranking is robust, that the panel is broadly aligned on direction of change, and that the main remaining uncertainty concerns how large the gains are, not whether they occur. That makes the elicitation a useful input to ERLI trajectory modelling, provided that downstream work carries forward ecological heterogeneity and structured uncertainty rather than collapsing everything into a single over-precise point estimate.
Keeping this summary in here as I was rather happy with it and worry I might have forgotten something in the updated version.
This expert elicitation separates two components of expected change by 2042:
This structure is valuable because it prevents us from conflating “policies help” with “policies deliver recovery.” Q2 can be strongly positive while absolute outcomes remain negative if baseline decline is large.
All synthesis statements below are robust to the two aggregation assumptions used in the analysis:
Only a small subset of combinations are affected by the A→B collapse step (freshwater assemblages in Freshwater/Wetland; lichens in Open habitats), so A vs B is best seen as a sensitivity check for habitat-group interpretation rather than a competing “true” baseline.
Selective evidence to keep visible in this section: - Table: Counterfactual baseline (Q1 signed %): Assumptions A and B - Table + figure: Counterfactual baseline by habitat_group (Assumptions A and B)
Across both assumptions, the counterfactual baseline mean is around −12%, with a high proportion of units negative.
Habitat-group baselines show that:
This is a crucial policy framing point: the baseline “pull” is substantial, so “reasonable” policy uplifts may still be insufficient to deliver stability unless they are large, broad, and fast-acting.
Selective evidence to keep visible: - Table + figure: Counterfactual baseline by habitat_group (Assumptions A and B)
All policy scenarios show positive mean deltas relative to the counterfactual, with a consistent ranking:
The high proportion of positive deltas, especially under policy_3, indicates broad agreement that interventions improve outcomes relative to baseline. However, because these are deltas, this result should be communicated as “policies reduce decline relative to BAU,” not as “policies deliver recovery.”
Selective evidence to keep visible: - Table:
Incremental policy uplifts (Q2 deltas): overall by scenario
(Assumptions A and B)
- One figure: the scenario-level uplift bar chart (faceting by
assumption if shown)
The key sufficiency test is the absolute outcome:
Absolute outcome under policy = counterfactual + delta
This is the quantity that matters for target framing (stability vs recovery). Under Assumption A, the mean absolute outcomes remain negative for every scenario:
This means the policy packages, as elicited, are best characterised as decline-reduction pathways, not system-wide recovery pathways, at least in the mean.
At the same time, the distributions are heterogeneous: under policy_3, most units are stable/positive, yet a material minority remain negative, and these residual declines keep the mean below zero. This is exactly why it is helpful to report both the mean and the recovered/stabilised fractions.
Selective evidence to keep visible: - Table: Absolute signed % outcome under each policy (Assumptions A and B) - Figure: Mean signed % outcome: counterfactual vs policy
The recovered/stabilised/decline-remaining table provides the most accessible translation:
This reconciles the apparent tension between “many outcomes are positive” and “mean outcome is negative.” The minority of negative outcomes are often sufficiently negative to dominate the mean. From a policy perspective, this implies that:
Selective evidence to keep visible: - Table: recovered/stabilised/decline remaining under A and B
Habitat-group absolute outcomes indicate that:
Because these habitat-group results are reported under both A and B, one can interpret them as a robust signal rather than a survey-row artefact.
Selective evidence to keep visible: - Table/scroll
box: Absolute outcomes under policy by habitat_group (Assumptions A
and B)
- One figure: habitat-group mean outcome bars by scenario (faceted by
habitat_group and/or assumption)
Uncertainty diagnostics show:
This supports a credible communication stance:
Selective evidence to keep visible: - Table: Mean dispersion (SD) in signed magnitude by scenario (Assumptions A and B) - One figure: mean–variance scatter (faceted by scenario), OR resampling stability plot (pick one, not both)
The statutory extinction-risk target implies no net increase in extinction risk by 2042, and ideally improvement. We have not translated percent distribution-change expectations into explicit Red List category transitions (and therefore not directly into ERLI change), because that would require an additional mapping model and strong assumptions about how these percent changes relate to category thresholds.
However, the CF + delta results provide a strong sufficiency diagnostic:
Therefore, on the elicited evidence alone, it is difficult to argue that any of the tested policy packages is likely to deliver system-wide stability/recovery by 2042 in a way that would reliably support ERLI neutrality or improvement. The most defensible framing is: