---
title: "Climate change vulnerability assessment with ccva"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{Climate change vulnerability assessment with ccva}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r, include = FALSE}
knitr::opts_chunk$set(collapse = TRUE, comment = "#>", fig.width = 7,
                      fig.height = 4.4, dpi = 90, out.width = "100%")
set.seed(2026)
```

A climate change vulnerability assessment (CCVA) asks which species are
most at risk from climate change. Almost every CCVA rests on the same
framework:

$$\text{Vulnerability} = f(\text{Exposure}, \text{Sensitivity}, \text{Adaptive capacity}).$$

In practice $f$ is usually a sum of expert-scored traits with arbitrary
weights, never tested for internal consistency, never validated against an
observable outcome, and reported without uncertainty. **ccva** keeps the
framework but puts it on a statistical footing:

1. **Data** -- any dataset; each column is tagged to a component.
2. **Components** -- each computed from its own predictors by a chosen method.
3. **Vulnerability** -- from the conventional weighted index (Ofori et al.
   2017) to a latent-variable model in which vulnerability is *inferred*
   from observable proxies.
4. **Constraints** -- ecological knowledge imposed explicitly on predictors
   and on each component's role.

Every step has a plot. All data in this vignette are simulated and have no
biological meaning.

```{r}
library(ccva)
```

## 1. Data

`ccva_sim` holds 300 simulated species generated from a known latent
structure: correlated latent exposure, sensitivity and adaptive capacity
drive a latent vulnerability $V^*$. Each component is observed only through
its indicators, and vulnerability only through three proxies -- an
IUCN-like status, a population trend and range change. Because the truth is
known, we can check which method recovers it.

`as_ccva()` is the data contract. It tags each column to a component and
records its **direction**: whether a higher value means *more* of the
component (or, for an outcome, *more* vulnerable). A declining population
trend and a range loss are bad news, so both get direction `-1`:

```{r}
d <- as_ccva(ccva_sim,
  exposure    = c("E1_risk", "E2_risk", "E3_risk"),
  sensitivity = grep("^s[0-9]+_", names(ccva_sim), value = TRUE),
  adaptive    = grep("^a[0-9]+_", names(ccva_sim), value = TRUE),
  outcomes    = c("iucn", "pop_trend", "range_change"),
  direction   = c(pop_trend = -1, range_change = -1),
  id = "species")
d$truth <- ccva_sim[c("E_star", "S_star", "A_star", "V_star")]
d
```

(`ccva_sim_data()` does exactly this in one call.) With real data, the
exposure indicators usually come from a species distribution model.
`ccva_sdm_exposure()` turns the cell counts of thresholded current and
future habitat maps into Ofori's three exposure indices, already recoded so
that higher means more exposed:

```{r}
ccva_sdm_exposure(n_current = c(120, 80), n_future = c(60, 95),
                  n_overlap = c(50, 70), n_protected = c(30, 10))
```

```{r fig.height = 7.5}
ccva_plot(d, "indicators")
```

### Before combining anything: diagnostics

Summing indicators assumes they measure one construct; adding components
assumes they are not redundant. `ccva_check()` tests both.

```{r}
chk <- ccva_check(d)
chk
```

```{r fig.height = 3}
ccva_plot(chk, "alpha")
```

## 2. The conventional baseline: Ofori et al. (2017)

Ofori and colleagues scored every indicator 1--3, summed the scores within
each component, and combined the components. `component_method =
"ordinal"` reproduces the scoring; the weighted method uses the corrected
normalisation $V = (E + S - A + 1)/3$, which maps the full range of
$E + S - A$ onto $[0, 1]$ without clipping.

```{r}
ofori <- ccva_combine(d, "weighted", component_method = "ordinal")
ofori
```

```{r fig.height = 3.6}
ccva_plot(ofori, "structure")
```

```{r fig.height = 3}
ccva_plot(ofori, "components")
```

The weighted index makes it easy to see what drives each species' score:

```{r fig.height = 5.5}
ccva_plot(ofori, "contributions", n = 8)
```

## 3. Vulnerability as a latent variable

Vulnerability is never observed directly; we observe its consequences. The
latent model treats it that way: $V^*$ is measured by the three outcome
proxies and caused by latent exposure, sensitivity and adaptive capacity,
each measured by its own indicators.

The three exposure indices share a denominator (current habitat area), so
their errors are correlated. We declare that in `shared_error`; it is
modelled by an orthogonal method factor.

```{r}
latent <- ccva_combine(d, "latent",
  shared_error = list(c("E1_risk", "E2_risk", "E3_risk")))
latent
```

```{r fig.height = 3.6}
ccva_plot(latent, "structure")
```

The measurement model shows how strongly each indicator reflects its
component -- a data-driven replacement for expert weights:

```{r fig.height = 7.5}
ccva_plot(latent, "effects")
```

### Which method recovers the truth?

```{r}
cmp <- ccva_compare(ofori = ofori, latent = latent)
cmp
```

```{r fig.height = 4}
ccva_plot(cmp, "truth")
```

The latent model recovers the true vulnerability better than the Ofori
index, because it uses the outcome proxies to infer $V^*$ instead of
computing it from discretised scores. Rankings change between methods:

```{r fig.height = 5}
ccva_plot(cmp, "ranks")
```

## 4. Constraints: imposing ecological knowledge

Free estimation ignores what ecologists already know. In ccva that
knowledge is imposed explicitly, at two levels:

* **predictor level** -- the sign of each indicator's loading, bounds,
  equality between indicators, or a fixed value (0 drops it);
* **component level** -- exposure and sensitivity raise vulnerability,
  adaptive capacity lowers it; bounds on the effects; or adaptive capacity
  acting only as a moderator.

The default constraints encode the standard ecological directions:

```{r}
ccva_default_constraints()
```

Rules are added on top of the defaults:

```{r}
cons <- ccva_constraints(
  constrain_equal(c("a1_genetic", "a2_bodysize")),
  constrain_fixed("s7_repromode", 0),
  constrain_bounds("exposure", lower = 0, upper = 2))
cons
fit_c <- ccva_combine(d, "latent", constraints = cons,
  shared_error = list(c("E1_risk", "E2_risk", "E3_risk")))
fit_c$details$effects
```

### When do constraints matter?

With 300 species the free estimates already have the ecological signs, so
constraints change little. CCVAs, however, are usually run on a few dozen
species. Here we fit the latent model with and without constraints to 20
simulated datasets of 50 species each:

```{r}
small_n <- do.call(rbind, lapply(1:20, function(i) {
  s <- ccva_simulate(n = 50, seed = 100 + i)
  run <- function(cons) {
    f <- tryCatch(suppressWarnings(ccva_combine(s, "latent",
           constraints = cons,
           shared_error = list(c("E1_risk", "E2_risk", "E3_risk")))),
           error = function(e) NULL)
    if (is.null(f)) return(c(converged = 0, rho = NA))
    c(converged = f$details$converged,
      rho = cor(f$score, s$truth$V_star, method = "spearman"))
  }
  rbind(data.frame(model = "constrained", t(run(NULL))),
        data.frame(model = "free", t(run(ccva_unconstrained()))))
}))
aggregate(cbind(converged, rho) ~ model, small_n, mean, na.action = na.pass)
```

Imposing the ecological directions is what lets the latent model converge
reliably at realistic sample sizes. Sign constraints on loadings have a
second benefit: the sign of a latent factor is otherwise arbitrary, and an
unconstrained fit can return vulnerability upside down. With every loading
pinned to its ecological direction that cannot happen.

### Adaptive capacity as a moderator

Biologically, adaptive capacity does nothing on its own: it modulates the
impact of exposure and sensitivity. `constrain_moderator()` removes its main
effect. In the shape-constrained model this fits monotone interaction
surfaces $f_{EA}(E, A) + f_{SA}(S, A)$; in the weighted index it gives
$V \propto E(1 - \gamma_E A) + S(1 - \gamma_S A)$.

```{r fig.height = 3.6}
mod <- ccva_combine(d, "shape",
                    constraints = ccva_constraints(constrain_moderator()))
ccva_plot(mod, "structure")
```

Without the moderator constraint, the shape-constrained model learns the
*shape* of each component's effect from data while the *direction* is fixed:

```{r fig.height = 3.2}
shape <- ccva_combine(d, "shape")
ccva_plot(shape, "effects")
```

## 5. High-dimensional predictors

Some components arrive with more predictors than species -- genomic markers
for adaptive capacity are the obvious case. The penalised two-stage method
estimates latent vulnerability from the outcomes, then regresses it on every
predictor with a LASSO whose coefficient signs follow the constraints.

```{r}
g <- ccva_simulate(n = 150, n_genomic = 300, n_causal = 10, seed = 3)
length(unlist(g$indicators))   # predictors
pen <- ccva_combine(g, "penalised")
pen
```

```{r fig.height = 5.5}
ccva_plot(pen, "effects")
```

## 6. How certain is the ranking?

A sorted list of point estimates implies a precision the data rarely
support. `ccva_uncertainty()` refits the whole pipeline on bootstrap
resamples; for the latent model each species' factor-score error is added
as well.

```{r}
unc <- ccva_uncertainty(latent, "bootstrap", R = 40, seed = 1,
                        progress = FALSE)
unc
```

```{r fig.height = 6}
ccva_plot(unc, "intervals", n = 25)
```

For the weighted index, a second question is how much the ranking depends
on the arbitrary choice of weights:

```{r}
ccva_uncertainty(ofori, "weights", R = 1000, seed = 1)
```

## 7. Classification and outputs

Cut-offs at 1/3 and 2/3 assume uniformly distributed scores. Alternatives
calibrate them to an observed outcome or to natural clusters:

```{r}
sapply(c("uniform", "quantile", "mixture", "roc"), function(m)
  table(ccva_classify(latent$score, m, data = d)))
```

```{r fig.height = 6}
ccva_plot(latent, "ranking", n = 8)
```

```{r}
head(ccva_rank(latent, unc))
```

`ccva_report(latent, "report.html", uncertainty = unc, comparison = cmp)`
writes all of this into one reproducible HTML report.

## 8. Extending the framework

New component or combination methods register with `ccva_register()` and
are then used exactly like built-in ones:

```{r}
ccva_register("median", function(X, anchor, signs, ...) {
  z <- scale(X)
  ctr <- attr(z, "scaled:center"); scl <- attr(z, "scaled:scale")
  list(score = apply(z, 1, median),
       predict = function(M) apply(scale(M, ctr, scl), 1, median))
}, type = "component", description = "Median of standardised indicators")

ccva_sensitivity(d, method = "median")
ccva_unregister("median")
```

## References

Ofori, B. Y., Stow, A. J., Baumgartner, J. B. and Beaumont, L. J. (2017).
Influence of adaptive capacity on the outcome of climate change
vulnerability assessment. *Scientific Reports* 7, 12979.

Bollen, K. A. (1989). *Structural Equations with Latent Variables*. Wiley.

Pya, N. and Wood, S. N. (2015). Shape constrained additive models.
*Statistics and Computing* 25, 543--559.

Rosseel, Y. (2012). lavaan: an R package for structural equation modeling.
*Journal of Statistical Software* 48(2), 1--36.
