Demography and population dynamics

Multistate and multievent capture-recapture (Arnason-Schwarz / Pradel multievent)

"How likely are my animals to survive in each state, how often do they move or switch between states, and is switching associated with a covariate?" Typical examples:

module id: multistate✓ Example verdict: DONE

Is this the right analysis?

Use it when

Individually marked animals are caught or resighted on repeated occasions and each capture records a state (site or wetland, breeding vs non-breeding, infected vs uninfected, size class), possibly with some captures where the state could not be determined; the question is survival in each state, how often animals move or switch between states, and what is associated with those transitions.

Not for

Plain survival without states (use cjs); abundance or recruitment (jolly-seber, scr-open); states that can be misclassified as another state (only "state recorded correctly or coded unknown" is modelled); unobservable states (e.g. temporary emigration to an unsampled site, skipped breeders that cannot be seen); dead recoveries; individual random effects; memory (second-order) transitions; animals with continuous position (use scr / movement-hmm).

The data you need

Capture records with animal id, occasion and recorded state (long CSV, wide table or capture-history strings; 0 = not seen, a state name, or an "unknown state" code), optional individual covariates (id + columns) and occasion covariates (occasion + columns).

What the example's main data file looks like:

First 5 of 516 rows of frog_captures.csv.
frog_idyearbd_status
RL-00012015U
RL-00012016uninfected
RL-00012021infected
RL-00022015infected
RL-00022016uninfected

Not sure? Use the finder to compare modules.

Try it

The worked example

Chytrid (Bd) infection, survival and clearance in a PIT-tagged frog population (multievent capture-recapture, simulated data with known truth)

A pond-breeding frog population is surveyed once a year, at the peak of the breeding season, from 2015 to 2022. Each frog caught gets a PIT tag (ids RL-0001 to RL-0280) and has its snout-vent length measured. Each capture also gets a skin swab, which qPCR tests for the amphibian chytrid fungus Batrachochytrium dendrobatidis (Bd). The result is uninfected or infected. Some swabs are lost, contaminated or inconclusive. Those captures are coded U: the frog was seen, but its infection status is unknown.

The question: Do infected frogs survive less well than uninfected frogs, how often do frogs become infected or clear Bd infection each year, is water temperature associated with infection and clearance, and do larger frogs survive better, when some swabs were lost or inconclusive?

The data are simulated, so the true answer is known and the analysis can be checked against it.

What the researcher typed

I have eight years (2015-2022) of capture data for a PIT-tagged frog population in three files: frog_captures.csv (each capture with the frog's id, year and Bd swab result: uninfected, infected, or U when the swab was lost or inconclusive), frog_individuals.csv (snout-vent length of each frog) and survey_years.csv (mean water temperature and survey effort in person-hours for each year). I want to know whether infected frogs survive less well, how often frogs become infected or clear the infection each year, whether water temperature is related to infection and clearance, and whether bigger frogs survive better. Please analyse this and give me a report with figures I can use in a paper.

Run it yourself

  1. Download the example data below and unzip it.

  2. Open the unzipped folder in Claude Code (see Install).

  3. Paste the prompt above. The agent checks the files, writes a plan, runs planted truth, fits the model and writes the report. You can compare your results with the ones below.

Contains frog_captures.csv, frog_individuals.csv, survey_years.csv, the settings file (config.yaml), a description of the data and a step-by-step walkthrough.

Study design details
statesuninfected, infected
modelmultievent (Pradel 2005); U = swab lost/inconclusive
n occasions8
years2015-2022
n intervals7
n frogs280
new frogs per year40 in 2015-2021
capture rows516
records uninfected258
records infected144
records unknown114
frogs recaptured141
survival covariatesvl_mm (individual)
transition covariatewater_temp_C (occasion; 7 values used)
detection covariatesurvey_hours (occasion)
engineNIMBLE + nimbleEcology dDHMMo, 3 chains x 6000 iterations (2000 burn-in), slice samplers
What the report says

Results from the example data

Figure: Probability of switching state over one year (for survivors) against Mean water temperature (°C), one curve per transition; dark band = 50% and light band = 95% credible intervals.
Probability of switching state over one year (for survivors) against Mean water temperature (°C), one curve per transition; dark band = 50% and light band = 95% credible intervals. Rug = observed values.

What this means

  • Survival over one year for an animal with average covariate values was 0.84 (95% credible interval 0.75-0.96) in state 'uninfected'; 0.50 (95% credible interval 0.37-0.63) in state 'infected'.
  • The difference in survival (uninfected minus infected) was 0.34 (95% interval 0.18 to 0.50); posterior probability that survival is higher in 'uninfected' = 1.00.
  • Transitions are probabilities of being in a new state at the next occasion, for animals that survive the year (at average transition covariates): from 'uninfected' to 'infected' 0.33 (95% credible interval 0.19-0.52); from 'infected' to 'uninfected' 0.34 (95% credible interval 0.18-0.54).
  • Expected number of years a surviving animal stays in a state before switching (1 / switching probability, median and 95% interval): 'uninfected' 3.1 (1.9-5.4); 'infected' 3.0 (1.8-5.5).
  • Survival and svl_mm: odds ratio per 1 SD (= 6) 1.77 (95% interval 1.34-2.50); P(effect > 0) = 1.00. Going from the 10th to the 90th percentile of svl_mm (44.5 to 59), predicted survival changes in 'uninfected' from 0.72 to 0.91, in 'infected' from 0.33 to 0.65.
  • Transitions and water_temp_C (from 14.6 to 19.1, 10th to 90th percentile): 'uninfected' to 'infected': 0.48 at the 10th percentile vs 0.23 at the 90th (difference -0.24, 95% interval -0.41 to -0.06); 'infected' to 'uninfected': 0.16 at the 10th percentile vs 0.56 at the 90th (difference 0.40, 95% interval 0.10 to 0.69). Associations only.
  • Detection (the chance that a live animal is caught or resighted at an occasion, at average detection covariates) was 0.61 (95% credible interval 0.49-0.75) in 'uninfected'; 0.39 (95% credible interval 0.23-0.63) in 'infected'. Low detection in a state means fewer data on that state, not fewer animals.
  • State uncertainty (multievent model): when a detected animal was in a given state, the probability that its state was recorded (rather than coded unknown) was 0.74 (95% credible interval 0.67-0.81) in 'uninfected'; 0.86 (95% credible interval 0.73-0.98) in 'infected'.
  • Goodness of fit: posterior predictive checks showed no clear misfit (Bayesian p = 0.43 sightings per occasion, 0.64 state switches, 0.60 animals never seen again).

More figures

Figure: State-specific survival (per year), detection, probability that a detected animal's state was recorded, and the state distribution at first capture, with posterior means and 95% credible intervals (at average covariate values).
State-specific survival (per year), detection, probability that a detected animal's state was recorded, and the state distribution at first capture, with posterior means and 95% credible intervals (at average covariate values).
Figure: Transition probabilities between states over one year for animals that survive it: posterior mean (95% credible interval).
Transition probabilities between states over one year for animals that survive it: posterior mean (95% credible interval). Rows sum to 1; off-diagonal cells are movement/switching, diagonal cells are staying. Evaluated at average transition covariates.
Figure: Estimated transition probabilities (points) with 95% credible intervals.
Estimated transition probabilities (points) with 95% credible intervals.

Figures are the module's default publication figures, reduced for the web. The report also saves them as PDF and full-resolution PNG.

How we know it works

Planted truth at the example's design

Before trusting the example's results, the module was run on many simulated data sets with the same design and known true values. Each row is one quantity: did the method get it back? (What the thresholds are.)

Verdict for this example: ✓ DONE

Every quantity checked passed planted truth at this design.

The example's design compared with the validated design: same as validated
This example8 annual occasions, 280 frogs (40 newly marked per year), 2 infection states with some swabs inconclusive
Design the module was validated at8 occasions, 280 animals (40 newly marked per occasion), 2-state multievent model

The example matches the validated default design: a 2-state model with uncertain states, 8 occasions and 40 new animals per occasion. All 16 estimands passed there and 100/100 fits converged. The weakest quantities were the transition slopes on the yearly covariate (B-to-A slope bias -15%, power 0.40), so read the water-temperature effects on infection transitions through their intervals.

≈ Borderline results

These calls sit close to the pass/fail line. With another 100 simulated data sets they could come out the other way, so read them as “about at the limit” rather than as a clear pass or fail.

  • survival_beta_svl_mm (✓ PASS): bias +9.1% against an allowance of ±10.0% (set by the 10% rule), so it used 91% of the allowance.

16 of 16 rows pass; 1 is borderline.

Show the full recovery table (16 rows)
Bias: average estimate minus truth, as a percentage of the truth. Allowed bias: the largest bias that still passes, the bigger of 10% of the truth and 3 Monte Carlo standard errors († = set by that simulation-noise floor, so the allowance is wider than 10%). Coverage: share of 95% intervals that contained the true value, with the minimum needed in brackets. ≈ borderline: the bias used 80–125% of its allowance, or coverage was within 0.015 of the minimum; such a result could flip between PASS and FAIL with another set of simulations. Quantity names are as they appear in the report.
QuantityTrue valueAverage estimateBiasAllowed biasCoverage (needed)Result
assignment_infected0.850.836-1.7%±10.0%0.95 (0.885)✓ PASS
assignment_uninfected0.750.758+1.1%±10.0%0.94 (0.885)✓ PASS
detection_beta_survey_hours0.40.425+6.2%±12.2%†0.96 (0.885)✓ PASS
detection_infected0.50.521+4.2%±10.0%0.96 (0.885)✓ PASS
detection_uninfected0.60.615+2.5%±10.0%0.93 (0.885)✓ PASS
initial_infected0.350.359+2.5%±10.0%0.93 (0.885)✓ PASS
initial_uninfected0.650.641-1.4%±10.0%0.93 (0.885)✓ PASS
stationary_infected0.4620.483+4.7%±10.0%0.96 (0.885)✓ PASS
stationary_uninfected0.5380.517-4.1%±10.0%0.96 (0.885)✓ PASS
survival_beta_svl_mm0.40.436+9.1%±10.0%0.91 (0.885)✓ PASS ≈ borderline
survival_infected0.550.551+0.1%±10.0%0.94 (0.885)✓ PASS
survival_uninfected0.750.759+1.2%±10.0%0.93 (0.885)✓ PASS
transition_beta_water_temp_C_infected_to_uninfected0.50.548+9.5%±23.1%†0.97 (0.885)✓ PASS
transition_beta_water_temp_C_uninfected_to_infected-0.7-0.674+3.7%±12.2%†0.94 (0.885)✓ PASS
transition_infected_to_uninfected0.350.347-0.9%±10.0%0.97 (0.885)✓ PASS
transition_uninfected_to_infected0.30.321+7.1%±10.0%0.94 (0.885)✓ PASS

Limits that matter

  • Transition slopes on a yearly covariate rest on only 7 temperature values: power at this design was 0.76 for infection and 0.26 for clearance, and detected slopes tend to be inflated (example clearance slope 0.92 vs true 0.50). Associations only; confounded with any other year effect.
  • False-positive rate for transition slopes was slightly above nominal (0.10 for one slope in the null validation run).
  • Misclassification (e.g. false-negative qPCR recorded as uninfected) is not modelled; only lost/inconclusive swabs are treated as uncertain.
  • Assignment (swab success) is assumed constant over time and the same at first capture and recaptures.
  • Survival is apparent survival (death and permanent emigration are confounded).
  • U-CARE tests are approximate for multievent data (unknown sightings treated as not seen).
  • Covariate slopes on survival and detection are shared by both states; body length is fixed at first capture.
  • The stationary distribution describes switching only and is not a prevalence forecast.
All known limits from the module's validation record
  1. Rare transitions are biased upward in the posterior mean. At true 0.05-0.10 with 3 states (section 3) the bias is +12-17% (~+0.01 absolute). Coverage is fine. Report medians/CIs, and be cautious when calling a rare transition "higher" than another. The independent 3-state multievent run showed the same upward pull (+21-26%, +0.03-0.04) for transitions out of a poorly observed state (assignment 0.5, detection 0.4), again with nominal coverage.
  2. Transition slopes on occasion covariates are weakly estimated.

    • With 8 occasions they have small-sample bias away from zero: -15% (multievent) and -19% (AS) for a -0.5 slope.
    • The AS variant fails the bias gate for that one estimand.
    • Power is low for moderate effects (section 7).
    • The interpretation calls them associations.
  3. High state uncertainty. With assignment around 0.3 and fewer than about 200 animals (section 4), transition and assignment point estimates are prior-influenced (+11-25% bias). The intervals keep nominal coverage. Use the power/recovery commands at the researcher's own design before trusting transitions.
  4. Misclassification is not modelled. Only "state recorded correctly or coded unknown" is supported (Conn & Cooch 2009 partial observation). If states are sometimes recorded wrongly, transitions are biased (compare the negative control).
  5. Not supported:

    • unobservable states;
    • memory (non-Markov) transitions;
    • individual random effects;
    • fully time-varying parameters (only occasion covariates);
    • dead recoveries;
    • state-specific covariate slopes on survival/detection (the slope is shared across states).
  6. Goodness of fit.

    • The U-CARE tests (R2ucare) assume the Arnason-Schwarz model. For multievent data they are run with unknown sightings treated as not seen, so they are approximate.
    • The PPCs (Freeman-Tukey, state switches, never-recaptured) were checked on the example only. Their power to detect specific violations was not quantified by simulation.
  7. The false-positive rate for transition slopes was 0.10 for one of two slopes (n = 99). That is slightly above nominal (0.05); the other slope was 0.05.
  8. Compute. About 5 min per fit including compilation. Recovery at nsim = 100 takes about 30-40 min on 16 cores.

How this module was validated

  • 100 simulated datasets per design, fitted with Nimble (3 chains x 2,000 iterations, 700 burn-in, slice samplers). To pass, bias had to be within 10% (or 3 Monte Carlo SE) and coverage (how often the 95% interval contained the truth) at least 0.885.
  • Default design: 16 of 16 estimands passed. Other designs passed in part. The variant where state is always recorded passed 11 of 12 (B-to-A slope -18.8%). Three sites passed 12 of 15 (rare transitions +12 to +17%). High state uncertainty passed 12 of 16 (transitions +15 to +25%).
  • Negative control: when unknown states were forced into the commonest state, the B-to-A transition was +32% with coverage 0.66. The gate failed, as it should.
  • False-positive rate with no true effects: 0.05-0.10.
  • Cross-checks: the hand-written likelihood and the marked package reached the same maximum (-2 log-likelihood 2097.24), and the Nimble posterior means were within 0.41 posterior SD of it.
  • Power: survival and detection effects needed about 20 new animals per occasion. A large transition effect (0.8 per SD) needed 40-80. A moderate one (-0.5 per SD) was not reliably detected even with 560 animals.
  • A separate verifier agent with its own simulator got 16/16 on one replicate set and 13/16 on another (the misses did not replicate), and 21/23 on a 3-state uncertain-state design. It found and fixed a bug in the maximum-likelihood initial-state output for 3 or more states, which did not affect any Nimble result.

Every run uses the acceptance gate described on the validation page. The full record follows, section by section, exactly as kept in the module's VALIDATION.md.

Settings and pass/fail rule used for every run

All runs used bin/Rscript core/run.R recover|power multistate on njobvu (16 cores; the power run used 14), with fast_settings: Nimble, 3 chains, 2000 iterations, 700 burn-in, slice samplers, no PPC and no U-CARE. The gate thresholds are the fixed core ones (recovery_gate):

  • bias<= max(10% of truth, 3 MC SE); for truth = 0, <= max(0.1 emp. SD, 3 MC SE)
  • coverage of the 95% CI >= 0.95 - 3 MC SE (0.885 at n = 100)
  • >= 90% of replicates converged (max R-hat < 1.05)

The raw outputs are in results/multistate/validation/<run>/ (*_reps.csv, *_summary.csv, HTML report). The configs are in modules/multistate/validation/*.yaml. To reproduce a run, use bash dev/multistate/run_validation.sh <run>. The tables below were produced by dev/multistate/summarise_validation.R. "rel_bias" is bias / truth. "power" is the share of replicates whose 95% CI excludes 0.

Median time per replicate was 250-380 s (including Nimble compilation). A default real-data fit (6000 iterations x 3 chains, PPC and U-CARE) of the 280-frog example took about 5 min.

0. Engine correctness and cross-checks

Cross-check 1: Arnason-Schwarz data (420 animals, 8 occasions, no covariates). Three implementations were compared:

  • Nimble (dDHMMo, module default settings);
  • the module's hand-written forward-algorithm likelihood maximised with optim (engine: ml);
  • the independent marked package (Laake et al. 2013; crm, model hmmMSCJS).
paramtruthNimble mean (SD)hand MLmarked MLE (SE)(Nimble - marked)/SD
survival_A0.800.790 (0.034)0.78380.7838 (0.044)+0.17
survival_B0.650.600 (0.042)0.59310.5931 (0.072)+0.16
detection_A0.600.697 (0.125)0.64610.6461 (0.209)+0.41
detection_B0.500.569 (0.181)0.57340.5734 (0.397)-0.02
transition_A_to_B0.300.374 (0.109)0.33800.3380 (0.215)+0.33
transition_B_to_A0.400.504 (0.111)0.52400.5240 (0.236)-0.18
  • The hand-written likelihood and marked give the same maximum: -2lnL = 2097.24 for both. Their estimates agree to < 3e-5.
  • The Nimble posterior means lie within 0.41 posterior SD of the MLE. The differences are the expected mean-vs-mode gap for skewed posteriors.
  • This dataset has weakly identified detection (large marked SEs), and that is where the gaps are largest.
  • Nimble max R-hat 1.009, min ESS 208.

Cross-check 2: multievent data with all three covariates (default design, seed 11). Nimble vs the hand-written multievent forward-algorithm ML:

  • All 16 estimands agree within 0.30 posterior SD. Examples:

    • survival_A 0.794 vs 0.792;
    • assignment_A 0.690 vs 0.683;
    • transition_beta_temp_A_to_B 1.29 vs 1.30;
    • detection_beta_effort 0.503 vs 0.494.
  • Every Nimble posterior mean lies inside the ML 95% interval.
  • Nimble max R-hat 1.005, min ESS 1536.
1. Default design (multievent, 2 states, 8 occasions, 40 newly marked per occasion = 280 animals): PASS 16/16
estimandtruthmean_estrel_biasbiasmcsecoverageboundci_widthpowergate
assignment_A0.70.712+1.7%0.0120.00440.980.8850.195PASS
assignment_B0.80.793-0.9%-0.00690.00710.950.8850.281PASS
detection_A0.60.61+1.7%0.010.00680.940.8850.261PASS
detection_B0.50.526+5.2%0.0260.00910.940.8850.353PASS
detection_beta_effort0.50.547+9.4%0.0470.0130.940.8850.5220.99PASS
initial_A0.60.589-1.9%-0.0110.0050.950.8850.187PASS
initial_B0.40.411+2.8%0.0110.0050.950.8850.187PASS
stationary_A0.5710.571+0.0%5.5e-050.00810.970.8850.324PASS
stationary_B0.4290.429-0.0%-5.5e-050.00810.970.8850.324PASS
survival_A0.80.802+0.3%0.00220.00380.950.8850.143PASS
survival_B0.650.641-1.3%-0.00860.0050.930.8850.183PASS
survival_beta_size0.50.485-3.0%-0.0150.0110.940.8850.4710.99PASS
transition_A_to_B0.30.316+5.2%0.0160.00690.950.8850.28PASS
transition_B_to_A0.40.424+6.1%0.0240.00850.980.8850.333PASS
transition_beta_temp_A_to_B0.80.826+3.2%0.0260.0290.930.8851.140.87PASS
transition_beta_temp_B_to_A-0.5-0.574-14.8%-0.0740.0370.950.8851.40.40PASS

All 100 replicates converged. Every estimand passes. The weakest quantities are the transition slopes on the occasion covariate (7 values):

  • B->A: rel. bias -15%, inside 3 MC SE; power 0.40.
  • A->B: power 0.87.
2. Arnason-Schwarz variant (state always recorded), same design: 11/12 PASS
estimandtruthmean_estrel_biasbiasmcsecoverageboundci_widthpowergate
detection_A0.60.613+2.1%0.0130.0060.960.8850.247PASS
detection_B0.50.519+3.8%0.0190.00880.940.8850.332PASS
detection_beta_effort0.50.544+8.8%0.0440.0130.930.8850.5180.99PASS
stationary_A0.5710.57-0.3%-0.00170.00720.930.8850.278PASS
stationary_B0.4290.43+0.4%0.00170.00720.930.8850.278PASS
survival_A0.80.8-0.0%-0.000230.00330.960.8850.131PASS
survival_B0.650.644-0.9%-0.00620.0050.920.8850.17PASS
survival_beta_size0.50.484-3.2%-0.0160.0110.960.8850.4680.99PASS
transition_A_to_B0.30.31+3.3%0.010.00580.940.8850.229PASS
transition_B_to_A0.40.414+3.4%0.0140.00720.970.8850.275PASS
transition_beta_temp_A_to_B0.80.786-1.8%-0.0140.0220.960.8850.9090.96PASS
transition_beta_temp_B_to_A-0.5-0.594-18.8%-0.0940.0290.940.8851.120.61FAIL(bias )

The single failure is transition_beta_temp_B_to_A:

  • bias -0.094, which is 3.2 MC SE and 19% of the truth -0.5, away from zero; coverage is 0.94;
  • the same estimand gave -15% (2.0 MC SE) in the multievent default run.

This is the known small-sample inflation of logistic-type slopes away from zero. It shrinks with more data: at n_marked = 80 in the power run the relative bias was +3%. See Known limits.

3. Three-site multistate (AS, 3 states, 50 marked per occasion, no covariates): 12/15 PASS
estimandtruthmean_estrel_biasbiasmcsecoverageboundci_widthpowergate
detection_W10.60.602+0.3%0.00180.00570.920.8850.203PASS
detection_W20.50.516+3.2%0.0160.00730.940.8850.299PASS
detection_W30.40.425+6.2%0.0250.00921.000.8850.412PASS
stationary_W10.4570.45-1.7%-0.00760.00750.890.8850.253PASS
stationary_W20.3710.372+0.1%0.000220.00710.940.8850.253PASS
stationary_W30.1710.179+4.3%0.00740.00540.970.8850.23PASS
survival_W10.80.803+0.4%0.0030.00330.930.8850.133PASS
survival_W20.70.695-0.8%-0.00540.00410.950.8850.165PASS
survival_W30.60.597-0.6%-0.00340.0050.950.8850.216PASS
transition_W1_to_W20.150.157+5.0%0.00740.0030.990.8850.142PASS
transition_W1_to_W30.050.0587+17.3%0.00870.00240.940.8850.0991FAIL(bias )
transition_W2_to_W10.20.209+4.3%0.00860.00470.950.8850.162PASS
transition_W2_to_W30.10.112+12.0%0.0120.00380.980.8850.167FAIL(bias )
transition_W3_to_W10.10.116+15.8%0.0160.0040.950.8850.153FAIL(bias )
transition_W3_to_W20.250.271+8.5%0.0210.00690.940.8850.271PASS

Survival, detection and the stationary distribution all pass.

The three rarest transitions (true 0.05-0.10) are biased upward by +0.009 to +0.016 in absolute terms (12-17%). Their coverage stays fine (0.94-0.98).

The cause is the posterior mean of a small probability under the logistic(0,1) priors on the multinomial-logit intercepts. With 3 states, those priors put prior mass around 1/3 per destination, which pulls rare transitions up. Report the posterior median or the CI for rare transitions, or use more animals. See Known limits.

4. Harder design: high state uncertainty (assignment 0.30/0.35, 25 marked per occasion = 175 animals): 12/16 PASS
estimandtruthmean_estrel_biasbiasmcsecoverageboundci_widthpowergate
assignment_A0.30.335+11.5%0.0350.0070.980.8840.344FAIL(bias )
assignment_B0.350.383+9.3%0.0330.010.980.8840.483PASS
detection_A0.60.601+0.1%0.000810.00740.960.8840.389PASS
detection_B0.50.525+4.9%0.0250.0110.960.8840.492PASS
detection_beta_effort0.50.516+3.1%0.0160.020.940.8840.680.84PASS
initial_A0.60.58-3.3%-0.020.010.980.8840.461PASS
initial_B0.40.42+4.9%0.020.010.980.8840.461PASS
stationary_A0.5710.548-4.1%-0.0230.0120.990.8840.567PASS
stationary_B0.4290.452+5.4%0.0230.0120.990.8840.567PASS
survival_A0.80.792-1.0%-0.00810.00530.950.8840.243PASS
survival_B0.650.665+2.4%0.0150.0080.950.8840.308PASS
survival_beta_size0.50.508+1.7%0.00830.0140.990.8840.6310.95PASS
transition_A_to_B0.30.375+24.9%0.0750.0140.960.8840.581FAIL(bias )
transition_B_to_A0.40.462+15.4%0.0620.0150.990.8840.645FAIL(bias )
transition_beta_temp_A_to_B0.80.722-9.7%-0.0780.0590.980.8842.860.20PASS
transition_beta_temp_B_to_A-0.5-0.792-58.5%-0.290.0850.960.8843.910.16FAIL(bias )
  • 98/100 replicates converged (max R-hat < 1.05).
  • Survival, detection, initial state, stationary distribution and the survival/detection slopes pass. Coverage is >= 0.96 for every estimand.
  • Failures, all toward the prior:

    • the transitions are biased upward (+25% and +15%);
    • assignment is biased upward (+11.5%);
    • the B->A temperature slope is biased away from zero (posterior SD ~1).
  • Reading: when the state is recorded for only about 1 in 3 detections, the data say little about transitions. The intervals are honest (wide, correct coverage) but the posterior means are prior-influenced.
  • This design is outside the validated range for point estimates of transitions. See Known limits.
5. Negative control: unknown states forced into state A: FAILS (as intended)

The data are the default multievent data, but every "seen, state unknown" record was recoded as state A. This is a common shortcut ("assume the commonest state").

estimandtruthmean_estrel_biasbiasmcsecoverageboundci_widthpowergate
detection_A0.60.605+0.9%0.00530.00590.960.8850.242PASS
detection_B0.50.521+4.1%0.0210.010.980.8850.416PASS
detection_beta_effort0.50.544+8.8%0.0440.0130.940.8850.5210.99PASS
stationary_A0.5710.657+14.9%0.0850.00710.730.8850.28FAIL(bias cov )
stationary_B0.4290.343-19.9%-0.0850.00710.730.8850.28FAIL(bias cov )
survival_A0.80.785-1.9%-0.0150.00340.900.8850.127PASS
survival_B0.650.637-2.0%-0.0130.00530.930.8850.192PASS
survival_beta_size0.50.48-4.0%-0.020.0120.930.8850.4680.99PASS
transition_A_to_B0.30.273-9.2%-0.0270.00580.920.8850.237PASS
transition_B_to_A0.40.527+31.7%0.130.00840.660.8850.321FAIL(bias cov )
transition_beta_temp_A_to_B0.80.67-16.2%-0.130.0240.830.8850.8670.89FAIL(bias cov )
transition_beta_temp_B_to_A-0.5-0.479+4.2%0.0210.0350.950.8851.250.27PASS

The gate catches it:

  • transition_B_to_A is biased +32% with coverage 0.66;
  • the stationary distribution is biased by 0.085 (coverage 0.73);
  • the A->B temperature slope is biased -16% (coverage 0.83).

Survival and detection are only mildly affected. This is why the module models state uncertainty instead of reassigning records. The gate CAN fail.

6. False-positive control: every covariate effect = 0: PASS 16/16
estimandtruthmean_estrel_biasbiasmcsecoverageboundci_widthpowergate
assignment_A0.70.725+3.5%0.0250.00470.970.8840.231PASS
assignment_B0.80.771-3.7%-0.0290.00680.960.8840.325PASS
detection_A0.60.608+1.4%0.00810.00760.980.8840.346PASS
detection_B0.50.548+9.5%0.0480.0110.960.8840.498PASS
detection_beta_effort0-0.00769-0.00770.0150.940.8840.4790.06PASS
initial_A0.60.576-3.9%-0.0240.00490.940.8840.207PASS
initial_B0.40.424+5.9%0.0240.00490.940.8840.207PASS
stationary_A0.5710.572+0.1%0.000620.00880.980.8840.387PASS
stationary_B0.4290.428-0.1%-0.000620.00880.980.8840.387PASS
survival_A0.80.804+0.5%0.0040.00340.960.8840.146PASS
survival_B0.650.656+0.9%0.00590.00460.950.8840.188PASS
survival_beta_size0-0.00228-0.00230.0120.930.8840.4290.07PASS
transition_A_to_B0.30.32+6.8%0.020.0080.980.8840.331PASS
transition_B_to_A0.40.428+6.9%0.0280.00780.980.8840.375PASS
transition_beta_temp_A_to_B00.03170.0320.0270.900.8840.9750.10PASS
transition_beta_temp_B_to_A0-0.0662-0.0660.030.950.8841.260.05PASS

False-positive rate (95% CI excludes 0 when the true effect is 0), nominal 0.05:

  • survival slope 0.07;
  • detection slope 0.06;
  • A->B transition slope 0.10;
  • B->A transition slope 0.05.

The 0.10 for the A->B transition slope (10 of 99) is just above the central 95% binomial range for a true rate of 0.05 at n = 99 (about 0.01-0.09). Its coverage, 0.90, still passes the gate. Treat a lone transition-covariate "effect" with an interval barely excluding 0 with caution.

7. Power over n_marked (newly marked per occasion; 8 occasions; nsim = 50 per level)

core/run.R power multistate --vary n_marked=10,20,40,80 --nsim 50 (results/multistate/validation/power/). Default multievent truths.

n_marked (total animals)survival_beta_size (0.5)detection_beta_effort (0.5)transition_beta_temp_A_to_B (0.8)transition_beta_temp_B_to_A (-0.5)coverage range (covariate slopes)
10 (70)0.640.640.400.160.90-0.96
20 (140)0.940.820.440.240.94-0.96
40 (280)1.000.980.900.320.82-0.96
80 (560)1.000.980.980.550.90-0.98
  • Survival and detection effects of this size need about 20 new animals per occasion.
  • Transition effects of an occasion covariate need 40-80 per occasion for a large effect (0.8 per SD).
  • A moderate effect (-0.5 per SD) is not reliably detectable with 8 occasions even at 560 animals.
  • Relative bias of the slopes at n_marked = 10 was up to +21% (away from zero), falling to < 4% at 80.
  • One cell has coverage 0.82: A->B slope at n_marked = 40. That is 50 replicates, so the MC SE is 0.03. The default nsim = 100 run at the same design gave 0.93.
Independent verification (2026-10-20, separate verifier agent)

Scripts and outputs are in dev/multistate/verify/ (out/ holds the summaries and recovery .rds files; the logs are logs/multistate_verify_*.log). All model fits went through bin/ejob on njobvu, with the module's fast_settings for recovery and the core recovery_gate() thresholds, unchanged.

Literature and code check.

  • The model was checked line by line against Pradel (2005) and the nimbleEcology dDHMMo documentation:

    • init is the state distribution at the first modelled occasion (f+1);
    • probTrans[, , t] maps t to t+1 and has len-1 slices;
    • probObs has len slices;
    • the two padded identity occasions are inert;
    • the first-event term pi_s * delta_s / sum pi_s (1 - delta_s) matches Pradel's initial-state x first-capture event structure;
    • survival acts before transition (AS order);
    • the multinomial logit uses staying as the reference.
  • The simulator's covariate indexing matches the model:

    • occasion covariate at t drives the interval t to t+1 for survival and transitions;
    • occasion covariate at t drives detection at t.
  • Shared code between simulate() and fit() is limited to ms_stationary() (truth and derived). The verifier's own truths use eigen() and power iteration instead.
  • Implicit assumption (now explicit): the same delta_s applies at first capture and at recaptures. Pradel (2005) allows a separate first-capture event matrix.

Independent likelihood (xcheck_indep.R): the verifier wrote the Pradel matrix-product likelihood without module code and maximised it with optim.

  • 2-state multievent with covariates on all three processes (survival on an occasion covariate, transitions on an individual covariate):

    • -lnL 1097.8760 for both the verifier's code and the module ml;
    • all estimands agree within 1.1e-7;
    • Nimble posterior means are within 0.36 posterior SD of the MLE.
  • 3-state multievent: -lnL 1426.0208 for both, and all survival, transition, detection and assignment estimands agree within 1.7e-6.

    • BUG found: initial_* from engine ml were wrong (0.337/0.337/0.169 vs the correct 0.507/0.255/0.238).
    • Cause: with a single row of draws, ms_derive() built the initial-state logit matrix with sapply + cbind, which mangles it for S >= 3.
    • The bug affected only the ML point estimate of initial_* with 3+ states. Nimble fits and 2-state models were unaffected, so no recovery result changes.
    • Fixed in R/derive.R. After the fix all 18 estimands agree within 1.7e-6, and single-row vs multi-row derivation agree exactly.
  • Nimble vs MLE on the 3-state data: within 0.89 posterior SD. The largest gap is for the weakly identified detection_centre (skewed posterior: MLE 0.82, mean 0.67, SD 0.17).

Rerun of the builder's default recovery (out/rerun_default/): identical to section 1 (PASS 16/16, 100/100 converged). The engine is deterministic given the seed.

Planted truth with the verifier's own simulator, scenario 1 (verify_sim.R main). The design and values differ from the builder's:

  • 2 sites, 10 occasions, Poisson(30) releases per occasion (~270 animals);
  • survival 0.70/0.85 with an OCCASION covariate (winter, slope -0.5);
  • transitions 0.15/0.25 with an INDIVIDUAL covariate (mass, slopes +0.6/-0.7);
  • detection 0.45/0.70 with effort (+0.4);
  • assignment 0.85/0.60, initial 0.35/0.65.
runresultnotes
seed 777, nsim 10013/16initial_* coverage 0.87 (bias +0.012, 2.9 MC SE); transition_beta_mass_pond1_to_pond2 coverage 0.88 (bound 0.885)
seed 99, nsim 10016/16all coverage 0.91-0.98, every relative bias < 8%, 100/100 converged
pooled 200passinitial coverage 0.925 and the pond1->pond2 slope 0.92, vs bound 0.904 at n = 200

The seed-777 misses did not replicate. They are consistent with chance: 16 estimands x 2 runs, each with about a 5% chance of a borderline miss.

Scenario 2: 3-state MULTIEVENT (verify_sim3.R), never run by the builder (whose 3-state run was AS only):

  • 7 occasions, 60 per occasion (360 animals);
  • assignment 0.9/0.7/0.5, initial 0.5/0.3/0.2;
  • a binary individual covariate (sex) on survival;
  • a negative detection slope (rain).

Result: 21/23 PASS, 98/100 converged.

  • All survival, detection, assignment, initial and stationary estimands and both slopes pass (bias <= 8%, coverage 0.93-0.99).
  • Failures are the transitions out of the least-observed state ('south': assignment 0.5, detection 0.4):

    • south->centre +21% (+0.042);
    • south->north +26% (+0.026);
    • coverage 0.95/0.97.
  • This is the same prior-pull pattern as Known limits 1 and 3. It is not confined to rare transitions: it hits transitions out of any poorly observed state.

Controls (verifier's simulator, nsim = 100 each):

  • Null (all four slopes = 0): PASS 16/16. False-positive rates:

    • survival slope 0.04;
    • detection slope 0.03;
    • transition slopes 0.05 and 0.05.
  • Negative control (unknowns forced into pond1, unknown_handling: assign): FAILS 9/12. For example:

    • transition_pond2_to_pond1 0.63 vs 0.25 (coverage 0.00);
    • stationary coverage 0.35;
    • pond2->pond1 mass slope coverage 0.42.
  • The gate can fail.

Usability.

  • fit on example/config.yaml reproduces the saved estimates exactly. Every one of the 16 example truths (example/truth.rds) lies inside its 95% CrI.
  • Every interpretation sentence was checked against the estimates table, including:

    • odds ratios exp(0.569) = 1.77 and exp(0.434) = 1.54;
    • survival difference 0.34;
    • residence time 1/0.334 ≈ 3.0;
    • 10th/90th-percentile transition values from the logit arithmetic.
  • The wording is associational, and detection is kept separate from ecology.
  • 14 messy-data cases were run (messy_test.R): NA covariate, misspelled state, misspelled id, wrong column names in two files, NA occasion covariates at used and unused occasions, typo year, conflicting states, empty state, capitalised states. All give actionable messages.

    • Fixed: a typo year ("2O18") used to give only "cannot tell the time order of the occasions". ms_occ_order() now names the non-numeric value(s) and suggests a typo.
  • The figures meet docs/FIGURES.md for multistate (survival_<cov>, detection_<cov> + detection_by_occasion, transitions, gof, plus trace/posterior). They use original units, and their plotted values agree with fit$estimates.

    • Fixed: coefficients.png had its plotting panel squeezed into about 20% of the width by unwrapped labels plus a redundant legend. Labels are now wrapped at 30 characters and the legend is dropped (the process is named in each label).
    • Example figures were redrawn (core/run.R figures).
  • README: corrected the marked model name (hmmMSCJS, not Mscjs).

Changes to the module:

  • R/derive.R: initial-state bug fix;
  • R/io.R: typo-year message;
  • R/output.R: coefficients figure layout;
  • README.md: marked model name.

No core changes. Recovery results are unaffected: all recoveries use Nimble, and the derive fix does not change multi-row results (verified: max diff 0).

Verdict: VERIFIED_WITH_LIMITS.

  • The default design passes, with independent replication at a different design.
  • Limits:

    • prior-pulled transition means for rare transitions, transitions out of poorly observed states, and high state uncertainty;
    • weak occasion-covariate transition slopes.

Engine, references and full details

Engine: nimble + nimbleEcology (dDHMMo, forward algorithm); option engine 'ml' = hand-written forward-algorithm likelihood maximised with optim (also used as a cross-check); marked (crm 'hmmMSCJS') used as an independent cross-check.

References

  • Arnason AN (1973) The estimation of population size, migration rates and survival in a stratified population. Researches on Population Ecology 15:1-8.
  • Schwarz CJ, Schweigert JF, Arnason AN (1993) Estimating migration rates using tag-recovery data. Biometrics 49:177-193.
  • Brownie C, Hines JE, Nichols JD, Pollock KH, Hestbeck JB (1993) Capture-recapture studies for multiple strata including non-Markovian transitions. Biometrics 49:1173-1187.
  • Lebreton J-D, Nichols JD, Barker RJ, Pradel R, Spendelow JA (2009) Modeling individual animal histories with multistate capture-recapture models. Advances in Ecological Research 41:87-173.
  • Pradel R (2005) Multievent: an extension of multistate capture-recapture models to uncertain states. Biometrics 61:442-447. doi:10.1111/j.1541-0420.2005.00318.x
  • Conn PB, Cooch EG (2009) Multistate capture-recapture analysis under imperfect state observation: an application to disease models. Journal of Applied Ecology 46:486-492.
  • Kendall WL (2009) One size does not fit all: adapting mark-recapture and occupancy models for state uncertainty. In Thomson DL et al. (eds) Modeling Demographic Processes in Marked Populations, Springer, pp 765-780.
  • Kéry M, Schaub M (2012) Bayesian Population Analysis using WinBUGS. Academic Press. Chapter 9.
  • Turek D, de Valpine P, Paciorek CJ (2016) Efficient Markov chain Monte Carlo sampling for hierarchical hidden Markov models. Environmental and Ecological Statistics 23:549-564.
  • Goldstein B, Turek D, Ponisio L, de Valpine P (2024) nimbleEcology: Distributions for Ecological Models in nimble. R package.
  • de Valpine P, Turek D, Paciorek CJ, Anderson-Bergman C, Temple Lang D, Bodik R (2017) Programming with models: writing statistical algorithms for general model structures with NIMBLE. Journal of Computational and Graphical Statistics 26:403-413.
  • Zucchini W, MacDonald IL, Langrock R (2016) Hidden Markov Models for Time Series, 2nd ed. CRC Press.
  • Pradel R, Wintrebert CMA, Gimenez O (2003) A proposal for a goodness-of-fit test to the Arnason-Schwarz multisite capture-recapture model. Biometrics 59:43-53.
  • Gimenez O, Lebreton J-D, Choquet R, Pradel R (2018) R2ucare: An R package to perform goodness-of-fit tests for capture-recapture models. Methods in Ecology and Evolution 9:1749-1754.
  • Laake JL, Johnson DS, Conn PB (2013) marked: an R package for maximum likelihood and Markov Chain Monte Carlo analysis of capture-recapture data. Methods in Ecology and Evolution 4:885-890.

CascadiaTools is made by Taal Levi, Cascadia Wildlife Lab, Oregon State University. Source code on GitHub · How to cite

Based at
Oregon State University