Accessible version · How to use this site with a screen reader · Course home
Day 5 · Plain-language summary

Hierarchical Bayes, latent states, and management quantities — in normal English

Read this before the lab. It explains every Day 5 idea with as little math as possible, and tells you what to focus on while reading the code.

What we did today, in one paragraph

Plain-language summary

We took the Bayesian models from Day 4 (one likelihood, a small handful of parameters, vague priors) and added the three things that show up in almost every real ecological dataset: multiple levels of grouping (sites within regions, fish within streams), latent biological states that we never directly observe (true occupancy, true abundance, true survival), and derived quantities that managers actually care about (equilibrium population size, harvest at maximum sustainable yield, year-to-year occupancy). The whole day was about fitting these models in NIMBLE, getting them to converge, and reading the posteriors back as biology.

Hierarchical models in plain English

A hierarchical model is a model in which some of the parameters themselves have parameters. The textbook example: you measured something at 30 sites, and you want a separate intercept $\alpha_j$ for each site, but you also think the sites are not totally unrelated — they all come from the same regional population. You write:

$\alpha_j \sim \mathcal{N}(\mu_\alpha, \sigma_\alpha^2)$

and then you estimate both the individual $\alpha_j$'s and the higher-level $\mu_\alpha$ and $\sigma_\alpha$ from the data. That's it. You have just acknowledged that sites are different but related, and the model will use that fact wisely.

Key idea

Three things to remember about hierarchical models:

  1. They are the principled middle ground between complete pooling (everyone is the same) and no pooling (everyone is independent).
  2. They share information across groups, which means small or noisy groups borrow strength from larger or cleaner groups.
  3. They produce shrinkage — extreme group estimates get pulled toward the overall mean, especially for groups with little data.

The same logic applies to slopes, variances, intercepts on log scales, intercepts on logit scales, dispersion parameters — anything that varies across some grouping. The hierarchical machinery is reusable.

What "shrinkage" really means

Imagine you sampled 50 streams for steelhead presence. In 49 streams you have 30 observations each. In one stream you have a single observation, which happened to be a detection. If you treat that one stream as its own universe (no pooling), your estimated probability of presence in stream 50 is $1/1 = 1.0$.

If you pool partially, the model says: "Across all the streams I've seen, presence rates are roughly $\mu = 0.4$ with SD $\sigma = 0.2$. Stream 50 has one observation that says 'present.' Both pieces of information are valid. Let me weight them by their precision."

The result: stream 50's estimated occupancy gets pulled from 1.0 toward 0.4, ending up somewhere around 0.5 or 0.6. That is shrinkage. It is the same logic a human expert would apply: don't take one observation at face value when you have a population of similar streams to compare against.

Biological intuition

Shrinkage is not a flaw in hierarchical models; it is their main feature. It corrects the over-confidence of extreme single-group estimates by recognising that those groups are part of a population of similar groups. In small-sample work it is often the difference between making good decisions and getting fooled by noise.

Latent state models: separating what's happening from what you saw

In ecology, we almost never measure the quantity we care about. We don't see "true occupancy" — we see whether a camera or eDNA sample flagged a species during one visit. We don't see "true abundance" — we see a count that is a noisy fraction of the truth. The actual biological state is latent (hidden).

A latent-state model has two layers:

Key idea

Process layer: What the biology is doing. Sites are occupied or not ($z[i, t]$). Patches have some true abundance ($N_i$). Tagged animals are alive or dead ($z_i$).

Observation layer: What your survey records. You might detect the species with probability $p$ given it's actually there. You might count $y[i, j]$ animals, drawn binomially from the true $N_i$ with detection $p$.

Estimating both layers at once is the whole point. If you ignored the observation layer and treated raw counts as truth, you would underestimate abundance, overestimate trend rates, and miss extinctions that are really just non-detections. The Bayesian latent-state framework gives you a clean place to write down both layers.

The two canonical examples

  • Dynamic occupancy: $z[i, t]$ is the true occupancy of site $i$ in year $t$, evolving with persistence $\phi$ and colonization $\gamma$. The data are repeated detections $y[i, j, t]$ given $z$ and detection $p$. We met this in section E of the lab.
  • N-mixture: $N_i$ is the true abundance at site $i$, drawn from Poisson($\lambda$). The data are repeated counts $y[i, j]$ drawn from Binomial($N_i, p$). We met this in section F.

Both models look complicated when you first see them, but each is just a process layer (one line) plus an observation layer (one line) plus priors. Once you internalise that split, dozens of other models in the literature suddenly become recognisable as variations on the same theme: capture-recapture, hidden Markov, dynamic N-mixture, false-positive occupancy, multistate models, etc.

Derived quantities — the Bayesian superpower for managers

A derived quantity is anything you compute from your model's parameters that you care about. Examples:

  • $S_\text{msy} = b\sqrt{1/a} - b/a$ in a Beverton-Holt fit — spawners at maximum sustainable yield.
  • The equilibrium occupancy $\gamma / (\gamma + (1 - \phi))$ in a dynamic occupancy model.
  • The total population $\sum_i N_i$ across all sites in an N-mixture model.
  • The probability that population $A$ has higher abundance than population $B$.

In maximum-likelihood land, you fit $(a, b)$, get point estimates and standard errors, and use the delta method or bootstrap to put a CI on $S_\text{msy}$. It works, but it's an approximation.

In Bayes, you compute $S_\text{msy}$ at every MCMC iteration, using that iteration's draw of $(a, b)$. The resulting set of values is the posterior distribution of $S_\text{msy}$ — with all the correlation between $a$ and $b$ correctly accounted for, no delta method, no bootstrap. You just write the derived-quantity line inside your nimbleCode block and ask MCMCsummary for it.

Biological intuition

This is the answer to "why bother with Bayes" for many applied scientists. You get fully propagated uncertainty in any quantity you can compute from parameters — for the same effort as defining the parameter.

Why MCMC works (intuitively)

Bayes' rule tells you what the posterior is:

$p(\theta \mid y) \propto p(y \mid \theta) \cdot p(\theta)$

It does not tell you what shape the posterior has, or how to compute its mean, or how to sample from it. For all but a few toy problems, the right-hand side is so messy that no formula will hand you the answer.

Markov Chain Monte Carlo (MCMC) is a clever way to sidestep the messy denominator. The idea: build a random walk through parameter space that, in the long run, visits each region with probability proportional to the posterior. Tally up where the walk spent its time, and you have an approximation of the posterior.

The two most common engines are:

  • Gibbs sampling: update each parameter from its full conditional distribution. Works great when those conditionals have known forms. JAGS and BUGS built their reputations on Gibbs.
  • Metropolis-Hastings: propose a small random step, accept or reject based on the ratio of posterior densities. Works on essentially any model.

NIMBLE uses a mix of both (and more — slice samplers, block samplers, Hamiltonian Monte Carlo via add-ons) and lets you customise.

Watch out

Two things to always check after running MCMC.

  1. Convergence. Did all chains end up in the same place? Use R-hat (gelman.diag), and look at the traceplots (MCMCtrace) — they should look like horizontal fuzzy caterpillars.
  2. Effective sample size. A correlated sample of 10,000 might only be worth 200 independent samples. Use effectiveSize(). Aim for at least a few hundred per parameter for any parameter you care about.

Why we switched JAGS → NIMBLE for the course

The model code is nearly identical — same distributions, same precision parameterisation, same logit/ilogit idiom. The reasons to switch are practical:

  • NIMBLE is actively developed in R, with the maintainers responsive to bugs and feature requests.
  • It compiles models to C++ for speed, then runs them as MCMC by default. You can also write your own samplers in R.
  • It generalises: you can run MCMC, but also Sequential Monte Carlo, MCEM, Laplace approximation, or your own custom algorithms, on the same model object.
  • The split between constants (sizes, indices, fixed covariates) and data (observed random variables) forces a clarity that JAGS' single-list approach often obscures.

The cheat sheet you need:

JAGSNIMBLE
cat("model{...}", file="x.txt")myCode <- nimbleCode({ ... })
jags.model() / update() / coda.samples()nimbleMCMC()
single data= listsplit into constants= + data=
argument order dbinom(p, n)same: dbinom(prob, size)
precision: dnorm(mu, tau)same: dnorm(mu, tau)

Common pitfalls (and how to avoid them)

Watch out

Vague priors on link scales. A $\mathcal{N}(0, 100)$ prior on a log-link slope is not noninformative; it puts most weight on impossible parameter values. Fix: standardise predictors and use $\mathcal{N}(0, \sigma \approx 2)$ priors.

Watch out

Bad starting values in latent-state models. If your initial latent state implies a $y$ that contradicts the data, the log-density is $-\infty$ and the chain won't start. Fix: in dynamic occupancy, initialise all $z = 1$; in N-mixture, initialise $N_i = \max_j y_{i,j}$.

Watch out

Wrong binomial argument order. R: rbinom(n, size, prob). NIMBLE/JAGS: dbinom(prob, size). Silent bug if you copy-paste from one to the other. Fix: always use named arguments.

Watch out

Wrong split between constants and data. If you put a covariate vector in data= but never assign it with ~, NIMBLE will treat it as missing (NA) and try to estimate it. Fix: covariates and indices go in constants=. Only random variables observed with ~ go in data=.

Watch out

Not checking convergence. A bad posterior summary will happily print to your screen even if R-hat is 1.5 and the chains never mixed. Fix: always run gelman.diag() and MCMCtrace() before believing any output.