--- title: "Case study: brown-shrimp lethal TDT" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Case study: brown-shrimp lethal TDT} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r, include = FALSE} knitr::opts_chunk$set( collapse = TRUE, comment = "#>", fig.width = 6, fig.height = 4, dpi = 96 ) ``` This article is a focused walk-through of one dataset: the brown shrimp (*Crangon crangon*) lethal thermal-death-time (TDT) assay vendored from [`bayesTLS`](https://github.com/daniel1noble/bayesTLS). It is written for an applied thermal-biology reader who wants to see the whole `freqTLS` workflow end to end on a single, well-behaved lethal dataset — fit, visualise, derive critical temperatures, and place the result beside the Bayesian and classical two-stage estimates. `shrimp_lethal` has **148 rows** at seven nominal assay temperatures (30–33 °C in 0.5 °C steps) crossed with exposure durations from about five minutes to six hours. The survival counts are **reconstructed** from the source CSV mortality proportions: `freqTLS` multiplies each proportion by the trial size and rounds, so `deaths = round(mortality_prop * total)` and `survived = total - deaths` (the "R-SHRIMP" reconstruction; see `?shrimp_lethal`). The benchmark configuration is fixed throughout: reference time `tref = 1` hour, the **relative** mortality threshold, a `beta_binomial` family, and a constant 4PL shape (`low`, `up`, `k` shared across temperatures). This is the matched configuration for the two model-based fits. The classical two-stage estimate is an absolute-LT50 approximation for this near-0/near-1 lethal curve (see `vignette("comparing-to-bayesTLS")`). **This article builds without Stan and renders fast.** The `freqTLS` fit runs live; the Bayesian and classical two-stage numbers are read from a version-stamped cache shipped with the package. The two chunks copied from `vignette("comparing-to-bayesTLS")` — the live shrimp fit and the three-way table — render Stan-free using that cache. ```{r setup} library(freqTLS) ``` ## The live `freqTLS` fit The `freqTLS` fit needs nothing beyond this package. We standardise the raw assay table with `standardize_data()`, fit the ungrouped shrimp 4PL by maximum likelihood with `fit_4pl()`, and read profile-likelihood confidence intervals for `CTmax` and `z` with `tls()`. ```{r profile-shrimp} data(shrimp_lethal) shrimp_std <- standardize_data( shrimp_lethal, temp = "Temperature_assay", duration = "Duration_exposure_hours", n_total = "N_individuals_after_trial", mortality = "Mortality_after_trial", duration_unit = "hours" ) shrimp_fit <- fit_4pl(shrimp_std, t_ref = 1, family = "beta_binomial", quiet = TRUE) tls(shrimp_fit, method = "profile")$summary ``` `CTmax` lands near 31.8 °C and `z` near 2.2 °C, each with a narrow asymmetry- respecting profile interval. Biologically, `z` ≈ 2.2 °C is the temperature change that scales the tolerated exposure tenfold: an exposure that is lethal in about an hour at 31.8 °C would be lethal in ~6 minutes at 34.0 °C, or take ~10 hours at 29.6 °C. A larger `z` would mean tolerance declines more gradually with temperature; a smaller `z`, more steeply. ## Seeing the fit ### The Confidence Eye The default `freqTLS` uncertainty visual is the **Confidence Eye**: a confidence lens with a hollow point estimate. It carries no prior and makes no probability statement about the parameter — it is a likelihood confidence display, not a posterior density. ```{r eye, fig.alt = "Confidence Eye for the shrimp CTmax and z: pale confidence lenses with hollow point estimates, the freqTLS uncertainty display. Both profiles close, so each eye is a closed lens."} plot_confidence_eye(shrimp_fit, parm = c("CTmax", "z"), method = "profile") ``` ### Survival curves The fitted survival surface, drawn as one curve per assay temperature against exposure duration: ```{r survival, fig.alt = "Fitted shrimp survival curves: probability of survival declining with exposure duration, one curve per assay temperature, hotter temperatures dropping faster."} plot_survival_curves(shrimp_fit) ``` ### The thermal-death-time line Collapsing the survival surface to the threshold gives the classical TDT line — log exposure time at the mortality threshold against temperature. Its slope is `z` and its position fixes `CTmax`: ```{r tdt, fig.alt = "Shrimp thermal-death-time line: log exposure time at the mortality threshold falling linearly with assay temperature, the slope encoding z."} plot_tdt_curve(shrimp_fit) ``` ## Deriving critical temperatures Because shrimp lethal TDT measures death, both critical-temperature derivations are meaningful here. `derive_ctmax()` reads the absolute temperature giving 50% survival at a one-hour exposure; `derive_tcrit()` returns the rate-multiplier `T_crit` (valid because this is a lethal endpoint). ```{r derive} c( CTmax_50pct_1h = round(derive_ctmax(shrimp_fit, surv = 0.5, duration = 1), 2), T_crit_rate1 = round(derive_tcrit(shrimp_fit, rate = 1), 2) ) ``` The 50%-survival critical temperature at one hour is about 31.7 °C; the rate-multiplier `T_crit` is about 27.4 °C. These are deterministic transforms of the fitted `CTmax` / `z`, not new fits. (`derive_tcrit()` prints an explicit lethal-endpoint caveat: on a sublethal endpoint the `z` is estimated from a functional decline rather than death, so feeding it into a lethal-damage accumulator can drive `T_crit` to implausible values. Shrimp lethal TDT is a lethal endpoint, so the value stands.) ## The three-way comparison, with real numbers The cache holds the maintainer-built `bayesTLS` (posterior) and classical two-stage summaries; the `freqTLS` column is computed live as this page renders. The two model fits use the matched beta-binomial, relative-threshold, constant-shape configuration at `tref = 1` hour. The classical two-stage column uses absolute LT50 and is an approximate comparator because the fitted lethal asymptotes are near zero and one. ```{r three-way, echo = FALSE} cache_path <- system.file("extdata", "bayesTLS_benchmark_cache.rds", package = "freqTLS") has_cache <- nzchar(cache_path) && file.exists(cache_path) fmt <- function(est, lo, hi) { ifelse(is.na(est), "--", sprintf("%.2f [%.2f, %.2f]", est, lo, hi)) } if (has_cache) { cache <- readRDS(cache_path) pro <- confint(shrimp_fit$fit, c("CTmax", "z"), method = "profile") # engine fit from above pick <- function(df, parm, cols) { r <- df[df$dataset == "shrimp" & df$parameter == parm, ] stats::setNames(as.numeric(r[cols]), c("est", "lo", "hi")) } ts_ct <- pick(cache$two_stage, "CTmax", c("estimate", "lower", "upper")) ts_z <- pick(cache$two_stage, "z", c("estimate", "lower", "upper")) by_ct <- pick(cache$bayesian, "CTmax", c("median", "lower", "upper")) by_z <- pick(cache$bayesian, "z", c("median", "lower", "upper")) pr_ct <- pro[pro$parameter == "CTmax", ] pr_z <- pro[pro$parameter == "z", ] knitr::kable(data.frame( Quantity = c("CTmax (°C)", "z (°C / decade)"), `Two-stage (delta CI)` = c(fmt(ts_ct["est"], ts_ct["lo"], ts_ct["hi"]), fmt(ts_z["est"], ts_z["lo"], ts_z["hi"])), `bayesTLS (95% CrI)` = c(fmt(by_ct["est"], by_ct["lo"], by_ct["hi"]), fmt(by_z["est"], by_z["lo"], by_z["hi"])), `freqTLS (profile CI)` = c(fmt(pr_ct$estimate, pr_ct$conf.low, pr_ct$conf.high), fmt(pr_z$estimate, pr_z$conf.low, pr_z$conf.high)), check.names = FALSE ), caption = "Shrimp CTmax and z shown side by side: the two model fits use the relative midpoint; the classical estimator uses absolute LT50.") } else { stop("The shipped bayesTLS benchmark cache is missing; reinstall freqTLS from a complete source tarball.") } ``` The headline is the agreement between the two interval-bearing model fits: the `freqTLS` profile **confidence** interval and the `bayesTLS` posterior credible interval nearly coincide — and the `freqTLS` side is produced live, in milliseconds, with no Stan. That is the complementary framing made concrete on one dataset: under the matched configuration the likelihood and the posterior summarise the *same* fitted curve, one prior-free and by optimisation, the other with a prior and MCMC. ## Boundary: what this case study does not cover The shrimp assay in `bayesTLS` also includes a **sublethal time-to-knockdown** endpoint — time until loss of righting response. That is a time-to-event quantity with a different likelihood, and it is a deliberate **non-goal** for `freqTLS`, which fits the single binomial / beta-binomial survival-count 4PL. For the sublethal knockdown analysis, see `bayesTLS`. **Where to next:** for a grouped fit, see the zebrafish or aphid case study; to derive heat-injury / `T_crit` from a fit, see `vignette("heat-injury")`.