Skip to contents

tinyAM (tiny assessment model; aka TAM) is an R package for fitting age-structured stock assessment models using RTMB.

The package is designed to be flexible, modular, and lightweight, with an emphasis on making alternative model structures and process assumptions easy to specify, inspect, simulate, and compare.

The development version can be installed from GitHub:

# install.packages("pak")
pak::pak("PaulRegular/tinyAM")

Dashboard R packages install automatically. Rendering requires Pandoc, which is included with RStudio.

See the package website and cheatsheet for guidance and examples.

Philosophy

Assessment models express hypotheses about where unexplained variation arises: recruitment, abundance deviations, fishing mortality, natural mortality, catchability or observations. tinyAM lets you change one or more assumptions and examine their consequences using the same diagnostic, simulation and forecasting tools.

“Tiny” refers to a small modelling vocabulary: complexity comes from combining understandable components, rather than choosing among many special-purpose models.

1. Set up tidy inputs

The required tables are catch, index, weight and maturity; pop_covariates is optional. The Northern cod example shows all five:

library(tinyAM)
print(cod_obs, n = 2)

── tinyAM data ─────────────────────────────────────────────────────────────────────────────────────

── catch: 1512 rows, 6 columns ──

Years 1954–2025; ages 0–20
     year age obs          unit F_y_block F_a_block
1    1954   0  NA thousand fish 1954-1991       0-1
2    1955   0  NA thousand fish 1954-1991       0-1
⋮       ⋮   ⋮   ⋮             ⋮         ⋮         ⋮
1511 2024  20   0 thousand fish 2023-2025     18-20
1512 2025  20   0 thousand fish 2023-2025     18-20

── index: 1512 rows, 7 columns ──

Years 1954–2025; ages 0–20
     year age obs     survey samp_time q_block          unit
1    1954   0  NA Fall 2J3KL       0.8       0 thousand fish
2    1954   1  NA Fall 2J3KL       0.8       1 thousand fish
⋮       ⋮   ⋮   ⋮          ⋮         ⋮       ⋮             ⋮
1511 2025  19   0 Fall 2J3KL       0.8    6-20 thousand fish
1512 2025  20   0 Fall 2J3KL       0.8    6-20 thousand fish

── weight: 1512 rows, 4 columns ──

Years 1954–2025; ages 0–20
     year age          obs unit
1    1954   0 2.965967e-04   kg
24   1955   0 2.965967e-04   kg
⋮       ⋮   ⋮            ⋮    ⋮
1631 2024  20 1.409146e+01   kg
1654 2025  20 1.369174e+01   kg

── maturity: 1512 rows, 3 columns ──

Years 1954–2025; ages 0–20
     year age obs
1    1954   0   0
2    1955   0   0
⋮       ⋮   ⋮   ⋮
1711 2024  20   1
1712 2025  20   1

── pop_covariates: 1512 rows, 4 columns ──

Years 1954–2025; ages 0–20
     year age allometric_M_assumption M_a_block
1    1954   0               4.9990199       0-1
2    1955   0               4.9990199       0-1
⋮       ⋮   ⋮                       ⋮         ⋮
1511 2024  20               0.1872682      8-20
1512 2025  20               0.1889190      8-20

The four required tables have numeric year, age and obs columns. obs contains catch numbers, survey indices, individual weights or proportions mature. Index rows also identify survey and samp_time: 0.8 means sampling eight-tenths of the way through the year. q_block groups ages sharing catchability in the example.

Catch covariates explain F means, F process SDs/correlation groups and catch observation SDs; index covariates explain q and index SDs. The optional pop_covariates table supplies recruitment, N and M predictors, including process-SD groups and supplied M; it needs a complete year–age grid without obs. Keep weights in weight, and maturity and optional spawn_time in maturity. An optional weight$type column separates complete stock and catch weights: stock weights determine biomass/SSB, catch weights determine yield and catch targets. Untyped or stock-only weights serve both purposes. Each type must cover the same biological year-age grid. Optional catch$fleet adds separate fisheries on one shared population: supply a complete year–age grid per fleet, with missing observations as NA. Catch weights may be shared or fully fleet-specific using weight$fleet; stock weights have no fleet label. See input help.

dat <- cod_obs                    # replace with your own named list of tables
check_tam_data(dat)               # validate before fitting
# dat <- as_tam_data(dat)         # optional class for compact printing

Catch in thousand fish and weight in kg give biomass and SSB in tonnes. Optional unit columns label values; they do not convert them. Survey units can differ. Catch/index NA values and zeros are excluded from the lognormal likelihood; weights and maturities must be complete. No missing-observation imputation or zero-count model is provided.

See ?check_tam_data for input requirements and ?cod_obs for details on the sources and preparation of the supplied Northern cod data.

2. Specify competing models

These are illustrative hypotheses, not package defaults or recommended stock settings. The baseline lets recruitment and fishing mortality vary while holding natural mortality at 0.3 per year:

base <- fit_tam(
  dat, years = 1983:2024, ages = 2:14,
  N_settings = list(process = "off", init = "exp"),
  rec_settings = list(process = "rw"),
  F_settings = list(process = "ar1", mu_form = ~ 1),
  M_settings = list(process = "off", mu_supplied = ~ I(0.3)),
  catch_settings = list(sd_form = ~ 1),
  index_settings = list(sd_form = ~ 1, q_form = ~ mono(q_block))
)

N_iid <- update(base, N_settings = list(process = "iid"))
M_ar1 <- update(base, M_settings = list(
  process = "ar1", age_breaks = c(2, 4, 6, 8, 14)
))

Partial updates retain omitted settings. Here the alternatives add independent cohort-survival variation or temporally correlated M shared within age blocks. The age range ages = 2:14 uses age 14 as the terminal plus group.

Other possibilities include:

Component Example options Guidance
Recruitment rec_settings: "rw"; "iid" with mu_form = ~ bh(ssb); "off" with ~ factor(year) Recruitment: Beverton–Holt/Ricker curves support IID/AR1 residuals or deterministic recruitment, not RW residuals.
Cohort survival N_settings: "off", "iid", "rw", "ar1"; mu_form = ~ temperature Population model: apparent gains/losses cannot separate migration from mortality.
Fishing mortality F_settings: "off", "iid", "rw", "ar1", "cor_rw"; sd_form = ~ age_group Settings: deterministic F requires an explicit mean. Optional age_breaks share actual F and can leave younger ages unfished; "cor_rw" correlates annual changes across states.
Natural mortality M_settings: "off", "iid", "rw", "ar1"; sd_form = ~ age_group Settings: M can be supplied or estimated; age blocks share actual M.
Mean F and catchability ~ age_group, ~ logistic(age), ~ mono(age); q also supports iid(), rw(), ar1() effects Curves and formula effects: supported combinations have identifiability safeguards; q_link = "logit" bounds q between zero and one.
Observation SD sd_form = ~ age_group Formula effects: formulas modify log-SD, not observation covariance.

Recruitment boundary states are fixed by default; rec_settings$init = "latent" integrates them without an initial density. For abundance-dependent catchability, use power(N, by = q_key) in q_form with the log link.

With fleet catches, F is estimated separately for each fleet and summed for population survival. A single formula can give fleets different selectivity curves and surveys different q relationships:

# Settings for your fleet/survey names and complete explanatory grids
F_settings <- list(process = "off", mu_form = ~ 0 +
  for_fleet("Trawl", ~ factor(year) + logistic(age)) +
  for_fleet("Gillnet", ~ factor(year) + double_logistic(age)))
index_settings <- list(q_form = ~ 0 +
  for_survey("RV", ~ logistic(age)) +
  for_survey("Sentinel", ~ s(age, k = 4)))

Scopes also support ordinary interactions. Double-logistic curves follow WHAM’s four-parameter product; smooths use jointly estimated mgcv penalties. Flexible F means can be deterministic or have IID/AR1 residuals. Their separation can be weak; intrinsic F random walks cannot identify these mean shapes. Shared correlation parameters do not correlate innovations between fleets; observation covariance and time-varying structural closures remain unsupported.

3. Diagnose, visualize and compare

check_tam(base, N_iid, M_ar1)
vis_tam(base, N_iid, M_ar1)

check_tam() separates numerical checks from structural concerns and statistical advisories. Convergence alone does not establish that the data identify every component. Inspect each check before interpreting or ranking an alternative.

Use plot_model(base, N_iid, M_ar1) or the dashboard’s opening Model structure page to inspect process switches and formulas. Its second tab shows function calls. The structure guide links contrasting diagrams to the model equations.

In the dashboard, start with Fisheries and Surveys to inspect fits and residuals. Plots start with retained OSA residuals when available, otherwise standardized residuals; the dropdown selects either type explicitly. Fisheries lists each fleet, then Aggregate fishery; without fleets, the aggregate page contains the single-fishery diagnostics. Review Tables → Diagnostics and Parameters for checks, uncertainty and process deviations before interpreting Trends for abundance and mortality. Residuals use fitted states and can be affected by shrinkage.

Test forecasting skill by repeatedly withholding terminal years:

hind <- fit_hindcast(base, N_iid, M_ar1, folds = 5)
hind
vis_tam(hind, model = "base")

The summary shows Mohn’s rho (retrospective change; ideally near zero), survey log-RMSE (lower is better), MASE (below one beats holding the last observation constant), and scored/failed-fold counts. Compare validation coverage before comparing scores; failed fits are retained in hind$fold_diagnostics, not scored.

By default, each one-year forecast conditions on actual aggregate catch. Held-out surveys and catch compositions test prediction; matching the imposed catch total is not forecast skill. Use proj_settings = list(F_mult = 1) for status-quo F instead. See hindcast definitions.

AIC() and cAIC() complement predictive checks for numerically acceptable models fitted to the same observations. Intrinsic random walks and unpenalized initial states need particular care; read the information-criterion assumptions before interpreting rankings.

Further tools

  • Tidy results: tidy_tam(base, N_iid, M_ar1) collects population, parameter and observation tables. Confidence limits use the reported scale; se_scale identifies the SE scale.
  • Residuals: residuals(base) extracts standardized residuals. To calculate and retain OSAs for dashboards, use base <- add_osa_residuals(base, method = "oneStepGeneric"). Calculation can be expensive and does not refit the model.
  • Population shortcuts: get_ssb(base), get_recruitment(base), get_fbar(base) and get_mbar(base) select annual summaries; get_f(base) and get_m(base) select age-specific tables.
  • Simulation and projections explore model behavior and future scenarios.
  • Model equations explain initialization, process scales, boundaries and spawning-time SSB.