--- title: "Getting started with freqTLS" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Getting started with freqTLS} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r, include = FALSE} knitr::opts_chunk$set( collapse = TRUE, comment = "#>", fig.width = 6, fig.height = 4, dpi = 96 ) ``` `freqTLS` fits single-stage four-parameter logistic (4PL) thermal-load-sensitivity (TLS) / thermal death-time models by maximum likelihood, parameterised **directly in `CTmax` and `z`** (thermal sensitivity — the temperature rise that cuts the tolerated exposure roughly tenfold), and returns prior-free **frequentist** confidence intervals — Wald, profile-likelihood (asymmetry-respecting, the default), and bootstrap. It is the likelihood complement to the Bayesian [`bayesTLS`](https://github.com/daniel1noble/bayesTLS) package; the modelling framework is theirs (see `vignette("comparing-to-bayesTLS")` for the relationship and credit). This vignette walks through the core loop: simulate → fit → confidence intervals → plot. It runs end to end with a small simulation and needs no Stan, no MCMC, and no internet. ```{r setup} library(freqTLS) ``` ## Simulate a dataset `simulate_tls()` draws survival counts from the locked data-generating process: a factorial grid of assay temperatures and exposure durations, with the 4PL evaluated at the supplied true `CTmax`, `z`, and shape parameters. We use the overdispersed beta-binomial family here; `phi` is its precision parameter — larger `phi` means *less* overdispersion, approaching the ordinary binomial as `phi` grows. ```{r simulate} set.seed(1) dat <- simulate_tls( temps = seq(30, 42, by = 2), times = c(0.5, 1, 2, 4, 8), reps = 3, n = 20, CTmax = 36, z = 4, phi = 50, family = "beta_binomial", seed = 1 ) head(dat) ``` Each row is one temperature-by-duration cell: `survived` out of `total` individuals survived the exposure. The true parameters used to generate the data are kept as an attribute, which is handy for checking recovery: ```{r truth} attr(dat, "truth")[c("CTmax", "z", "phi")] ``` ## Standardise and fit the model `freqTLS` mirrors the `bayesTLS` workflow. First `standardize_data()` names the temperature, duration, and survival-count columns and records the data contract; then `fit_4pl()` fits the 4PL by maximum likelihood and returns a `freq_tls` workflow object. `t_ref` is the reference time at which `CTmax` is defined: the twin facade `fit_4pl()` uses the `bayesTLS` spelling `t_ref` (default 60), while the lower-level engine `fit_tls()` (below) uses `tref` (default 1) — the same quantity, in the data's duration unit. The simulated durations here are already in reference units, so we pass `1`. ```{r standardize} std <- standardize_data( dat, temp = "temp", duration = "duration", n_total = "total", n_surv = "survived" ) ``` ```{r fit} fit <- fit_4pl(std, family = "beta_binomial", t_ref = 1) fit ``` ### Formula interface For the supported moderators, `fit_4pl()` takes a **similar direct interface** to `bayesTLS` — `ctmax =`, `z =`, or the `by =` shorthand (e.g. `by = "life_stage"` for a grouped fit). Under the hood the engine also exposes a `brms`/`drmTMB`-style grammar via `tls_bf()` — the left-hand side names the survival counts (`successes | trials(total)`), the right-hand side tags the two axes with the `time()` and `temp()` markers — which feeds the same likelihood, so the two fits are numerically identical: ```{r fit-formula} fit_f <- fit_tls( tls_bf(survived | trials(total) ~ time(duration) + temp(temp)), data = dat, family = "beta_binomial", tref = 1 ) all.equal(coef(fit_f), coef(fit)) ``` The grammar scales well beyond a single ungrouped fit: each sub-parameter can take a grouping factor, a continuous covariate, or an `lme4`-style random intercept. See **Going further** below. The print method reports the headline quantities (`CTmax`, `z`), the shape parameters (`low`, `up`, `k`), the overdispersion `phi`, and the convergence status. `summary()` adds standard errors and the log-likelihood: ```{r summary} summary(fit) ``` A tidy, broom-style table of all natural-scale parameters is available with `tidy_parameters()`, and the headline quantities have dedicated extractors: ```{r tidy} tidy_parameters(fit) get_ctmax(fit) get_z(fit) ``` ## Profile-likelihood confidence intervals The reason for the direct `CTmax`/`z` parameterisation is that both headline quantities are model *coordinates*, so they can be profiled directly. `confint()` with `method = "profile"` (the default) inverts the likelihood-ratio test: the interval is the set of values whose deviance from the maximum stays below the profile-$t$ cutoff (a squared Student-$t$ quantile on the residual degrees of freedom, not $\chi^2_1$; see `vignette("frequentist-and-bayesian")`), found by root-finding on each side of the MLE. ```{r confint} confint(fit, parm = "CTmax", method = "profile") confint(fit, parm = "z", method = "profile") ``` These are **confidence** intervals, not posteriors: they carry no prior, and they need not be symmetric about the estimate. The `conf.status` column reports `"ok"` when the profile closes on both sides; a weakly identified parameter is flagged rather than given a fabricated bound (see `vignette("profile-likelihood")`). For comparison, the first-order Wald interval is available with `method = "wald"`: ```{r wald} confint(fit, parm = c("CTmax", "z"), method = "wald") ``` ## Plot the fit `plot_survival_curves()` draws the fitted survival probability against exposure duration, one curve per temperature, with the observed proportions overlaid: ```{r survival-curves, fig.alt = "Fitted 4PL survival curves: survival probability declining with exposure duration, one curve per assay temperature, with observed proportions as points."} plot_survival_curves(fit) ``` The default uncertainty display is the **Confidence Eye**: a pale confidence lens with a hollow point estimate. It is freqTLS's visual identity and deliberately avoids posterior-density iconography, because these are likelihood intervals. Read the lens **width** as the confidence interval and the **hollow circle** as the maximum-likelihood estimate; two eyes that clearly fail to overlap are distinguishable at the confidence level, and a hollow point with *no* lens flags a profile that did not close (a weakly identified parameter). ```{r confidence-eye, fig.alt = "Confidence Eye plot: a pale lens spanning the profile-likelihood interval with a hollow point estimate, for CTmax and z."} plot_confidence_eye(fit, parm = c("CTmax", "z"), method = "profile") ``` ## Going further: groups, shape predictors, and random effects The same `tls_bf()` grammar scales from the single fit above to grouped, covariate, and hierarchical models. **Other families.** Besides `"beta_binomial"`, `fit_tls()` accepts `"binomial"` (no overdispersion) and `"beta"` (a continuous proportion in (0, 1) with no trials column; the runnable example below uses simulated beta data). **Grouped `CTmax` and `z`.** Put a grouping factor on a sub-parameter to estimate a separate, directly profile-able `CTmax` and `z` per group, with one shared curve shape (the `bayesTLS` constant-shape configuration). In the formula, `z` enters on its internal log scale as `log_z` (and steepness as `log_k`), so the grouped term is written `log_z ~ group`. Intervals are still requested by the natural-scale name (`z:cool`, not `log_z:cool`) — `confint()` converts internally: ```{r grouped} dat_grp <- simulate_tls( temps = seq(30, 42, by = 2), times = c(0.5, 1, 2, 4, 8), reps = 3, n = 20, group = c("cool", "warm"), CTmax = c(34, 38), z = c(3, 5), phi = 50, family = "beta_binomial", seed = 2 ) fit_grp <- fit_tls( tls_bf( survived | trials(total) ~ time(duration) + temp(temp), CTmax ~ group, log_z ~ group ), data = dat_grp, family = "beta_binomial", tref = 1 ) confint(fit_grp, c("CTmax:cool", "CTmax:warm", "z:cool", "z:warm"), method = "profile") ``` **Shape predictors.** The shape parameters are no longer intercept-only: `low`, `up`, and `log_k` each take their own grouping factor or continuous covariate (for example `low ~ group` or `log_k ~ body_size`). See `?tls_bf`. **A random intercept.** Add an `lme4`-style `(1 | group)` term to let a sub-parameter vary across groups drawn from a population — here `CTmax` varies by `colony`: ```{r random-effect} dat_re <- simulate_tls( temps = seq(30, 42, by = 2), times = c(0.5, 1, 2, 4, 8), reps = 2, n = 20, CTmax = 36, z = 4, phi = 50, family = "beta_binomial", re_sd = 1.2, n_re_groups = 10, seed = 3 ) fit_re <- fit_tls( tls_bf( survived | trials(total) ~ time(duration) + temp(temp), CTmax ~ (1 | colony) ), data = dat_re, family = "beta_binomial", tref = 1 ) fit_re head(ranef(fit_re)) ``` The intercept is integrated out by a Laplace approximation; `sigma_CTmax` is its standard deviation and `ranef()` returns the per-colony BLUPs. The population `CTmax` interval is profiled under the random effect (`confint(fit_re, "CTmax")`, the default), which propagates the variance-component uncertainty that a Wald interval would understate. Single random intercepts are supported on `CTmax`, `log_z`, `low`, and `log_k`; see `vignette("random-effects")` for the full hierarchical workflow and the few-groups caveats (the variance components are biased low when fewer than about eight groups are sampled). ## The core workflow at a glance `freqTLS` deliberately mirrors the `bayesTLS` workflow, so the same **standardise → fit → quantities → plot** path runs by maximum likelihood. The map below shows the main workflow and how each box twins a `bayesTLS` function. The **"freqTLS extras"** box (gold, upper right) is the frequentist-only addition — Wald / profile / bootstrap intervals and the Confidence Eye — and the dashed **"trace & repair helpers — planned"** box is the one piece not yet ported. ```{r function-map, echo = FALSE, results = "asis"} # Inline the SVG rather than include_graphics() so the figure does not depend on # pkgdown copying a co-located static asset. The Pandoc raw-HTML fence is # essential: without it, wildcard labels such as get_*_summary() are parsed as # Markdown emphasis, which breaks the SVG namespace. Accessibility is provided # by the SVG's