--- title: "Case study: Drosophila suzukii lethal thermal death time across sexes" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Case study: Drosophila suzukii lethal thermal death time across sexes} %\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 fits a lethal thermal-death-time (TDT) model to the vinegar fly *Drosophila suzukii*, asking the question Ørsted, Hoffmann, Sgrò and colleagues posed in their 2024 study: **do the two sexes differ in their thermal limits?** It mirrors the lethal subset of Case Study 4 in the `bayesTLS` supplement, but it is a `freqTLS`-only treatment: the fit runs live by maximum likelihood (TMB, **no Stan**), uncertainty is summarised by profile-likelihood **confidence intervals**, and the sex difference is a **frequentist contrast**, not a posterior. **This vignette builds without Stan.** The `freqTLS` side runs live by maximum likelihood (TMB); the Bayesian (`bayesTLS`) and classical two-stage columns of the three-way comparison are read from the maintainer-built benchmark cache (`inst/extdata/bayesTLS_benchmark_cache.rds`), so the full per-sex table renders without a Stan toolchain. ```{r setup} library(freqTLS) ``` ```{r load-data, include = FALSE} data(dsuzukii) # Per-individual records -> survival counts per (temp, time, sex) cell. .nd <- aggregate(list(n_dead = dsuzukii$dead), dsuzukii[c("temp", "time", "sex")], sum) .nt <- aggregate(list(n_total = dsuzukii$dead), dsuzukii[c("temp", "time", "sex")], length) mort <- merge(.nd, .nt) ``` ## The dataset `dsuzukii` is the per-individual mortality data from the Ørsted *D. suzukii* assays, vendored from [`bayesTLS`](https://github.com/daniel1noble/bayesTLS) under CC BY 4.0 (primary deposit [10.5281/zenodo.10602268](https://doi.org/10.5281/zenodo.10602268); cite `@orsted_suzukii_2024` and `citation("freqTLS")`). We aggregate it to survival counts per `(temp, time, sex)` cell — `n_dead` of `n_total` died — exactly as the `bayesTLS` case study does. ```{r data-shape} str(mort) table(mort$sex) ``` There are `r nrow(mort)` cells (a temperature × duration × sex grid), `r sum(mort$sex == "F")` for females and `r sum(mort$sex == "M")` for males, totalling `r sum(mort$n_total)` flies. Two columns deserve emphasis: * **`time` is in minutes.** The reference exposure for this study is **4 hours = 240 minutes**, so we set `tref = 240`. * We interpret the threshold as **absolute** (a fixed 50% survival), following Ørsted. The model itself is still parameterised by the **relative midpoint** (`CTmax` is the 4PL midpoint, the `(low + up)/2` survival level). The relative and absolute thresholds coincide only when the asymptotes sit near 0 and 1 — which they do for these data (`low` ≈ 0, `up` ≈ 1, confirmed in the fit below), so `derive_*` at `surv = 0.5` recovers the absolute LT50 with no extra argument. If your own data have asymptotes far from 0/1, the two thresholds diverge and you must pass the `surv` you want explicitly. ## Fitting the model: one fit, grouped by sex A single grouped call fits a beta-binomial 4PL with sex-specific thermal location (`CTmax`, `z`) and a shared curve shape (`low`, `up`, `k`) — the constant-shape configuration that matches the `bayesTLS` "sex on the midpoint only" model. The temperature effect runs through the midpoint, so each sex gets its own `CTmax` and `z` while the asymptotes and steepness are pooled. ```{r fit} std <- standardize_data( mort, temp = "temp", duration = "time", n_total = "n_total", n_dead = "n_dead", duration_unit = "minutes" ) # fit_4pl(by = "sex") gives a per-sex CTmax and z (the "sex on the midpoint" # model); $fit is the engine fit the extractors below read directly. fit <- fit_4pl(std, by = "sex", t_ref = 240, family = "beta_binomial", quiet = TRUE)$fit fit$convergence$code # 0 = converged ``` The optimiser converged (code `0`, positive-definite Hessian). The per-sex thermal limits, with **profile-likelihood confidence intervals**: ```{r per-sex} confint(fit, c("CTmax:F", "CTmax:M", "z:F", "z:M"), method = "profile")[ , c("parameter", "estimate", "conf.low", "conf.high", "method")] ``` So the maximum-likelihood estimates are: | Quantity | Female | Male | | --- | --- | --- | | `CTmax` at 4 h (°C) | 35.23 [35.13, 35.32] | 35.25 [35.16, 35.34] | | `z` (°C / decade) | 3.01 [2.86, 3.18] | 3.18 [3.01, 3.36] | Both sexes have an upper thermal limit near 35.2 °C at a four-hour exposure, and a thermal sensitivity `z` of about 3 °C per tenfold change in survival time. The intervals are confidence intervals from the profile likelihood — the range of parameter values the data do not reject at the 95% level — not credible intervals and not posterior summaries. ### The Confidence Eye The `freqTLS` uncertainty display is the **Confidence Eye**: a confidence lens with a hollow point estimate, carrying no prior and making no probability statement about the parameter. Here it shows `CTmax` and `z` for both sexes side by side. ```{r eye, fig.height = 5, fig.alt = "Confidence Eye for Drosophila suzukii CTmax and z, one lens per sex (female and male). The lenses for the two sexes overlap almost completely for both CTmax (near 35.2 C) and z (near 3 C per decade), each drawn as a pale confidence lens with a hollow point estimate."} plot_confidence_eye(fit, parm = c("CTmax", "z"), method = "profile") ``` The female and male lenses overlap heavily for both quantities — the first visual hint that the sexes are not strongly separated. Each lens is a closed confidence interval (the profiles closed on both sides), so the eyes are drawn closed rather than hollow-and-open. ### Survival curves The fitted survival curves, with the observed cell proportions overlaid, show the 4PL decline of survival with exposure time at each temperature, per sex. ```{r survival, fig.height = 4.5, fig.alt = "Fitted Drosophila suzukii survival curves by temperature (34 to 38 C) and sex, with observed cell proportions overlaid. Survival falls from near one to near zero as exposure time increases, and the curves shift to shorter survival times at higher temperatures; the female and male curves are nearly indistinguishable at each temperature."} plot_survival_curves(fit) ``` ### A lower thermal threshold: T_crit Because this is a **lethal** endpoint, the rate-multiplier critical temperature `T_crit` is meaningful here (it is a damage-accumulation concept and would be misleading for sublethal endpoints such as heat-coma). `derive_tcrit()` returns the temperature at which the thermal-damage rate falls to a chosen low floor; at the `bayesTLS` default floor of 1% of the lethal dose per hour: ```{r tcrit} c( T_crit_F = round(derive_tcrit(fit, rate = 1, group = "F"), 2), T_crit_M = round(derive_tcrit(fit, rate = 1, group = "M"), 2) ) ``` `T_crit` sits near **29 °C** for both sexes (about 6 °C below `CTmax`), the lower thermal threshold a heat-injury model would treat as the onset of negligible damage. It is a deterministic transform of the fitted `CTmax` and `z`, not a new fit. ## Do the sexes differ? A frequentist contrast The biological question is whether the small female-minus-male gaps in `CTmax` and `z` are distinguishable from zero. `freqTLS` answers this with a profile or bootstrap confidence interval **on the difference itself** — a frequentist contrast, *not* a posterior probability statement. The contrast is requested by name: `dCTmax:F-M` (the `CTmax` difference, female minus male) and `dz:F-M` (the sensitivity difference, taken on the log-`z` scale so the `z` ratio is its exponential). ```{r contrast} sex_contrasts <- confint( fit, c("dCTmax:F-M", "dz:F-M"), method = "profile", fallback = TRUE, nboot = 1000L, boot_seed = 20260713L ) sex_contrasts[, c("parameter", "estimate", "conf.low", "conf.high", "method")] ``` Both intervals **span zero**: * `dCTmax:F-M` = **-0.024 °C, 95% CI [-0.154, 0.092]** — the sexes' four-hour `CTmax` values are indistinguishable. * `dz:F-M` = **-0.054 [-0.133, 0.026]** on the log-`z` scale (a `z` ratio of `exp(-0.054)` ≈ 0.95, a point estimate of about a 5% difference in `z` between the sexes), with a confidence interval that includes 0 (ratio 1). This agrees with the published finding: Ørsted reported a sex difference in `z` whose interval spans zero, i.e. **no clear sex difference in thermal sensitivity or limit**. The likelihood path reaches the same conclusion as the Bayesian path did, by a contrast that makes no posterior claim. The `method` column above records when the requested profile used the asymmetry-respecting parametric- bootstrap fallback without a prior. ## Validation: does ML recover the published values? A useful cross-check is whether the maximum-likelihood fit lands on the same parameter values Ørsted (2024) reported in their Table 1, which were obtained by a different route. The published values are `z` ≈ 3.03 (F) / 3.28 (M) and a four-hour `CTmax` ≈ 35.2 °C for both sexes. ```{r validation} ml <- confint(fit, c("CTmax:F", "CTmax:M", "z:F", "z:M"), method = "profile") data.frame( parameter = ml$parameter, ML_estimate = round(ml$estimate, 2), published = c(35.2, 35.2, 3.03, 3.28) ) ``` The match is close: `CTmax` ≈ 35.23 / 35.25 °C against a published ≈ 35.2 °C, and `z` ≈ 3.01 / 3.18 against a published 3.03 / 3.28. The likelihood path reproduces the published thermal-limit estimates on the same data — a validation success that the `freqTLS` engine targets the same model and the same fitted curve as the analyses that established these numbers. ## The three-way comparison, per sex The cache holds the maintainer-built `bayesTLS` (posterior) and classical two-stage summaries; the `freqTLS` column is computed live as this page renders. All three use the matched configuration (beta-binomial, constant shape, `tref = 240` minutes). The `CTmax` compared is the relative-midpoint parameter; for these near-0/near-1 lethal asymptotes it coincides with the absolute LT50 the study reports above, so the benchmark and the headline per-sex table agree. ```{r three-way-suzukii, 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)) } sex_levels <- fit$group_levels sex_label <- c(F = "Female", M = "Male") # Live freqTLS per-sex CTmax and z as profile-likelihood confidence # intervals (with explicit open-profile status; no Stan). pro <- suppressMessages(confint( fit, c(paste0("CTmax:", sex_levels), paste0("z:", sex_levels)), method = "profile" )) pr_of <- function(parm, g) { r <- pro[pro$parameter == paste0(parm, ":", g), , drop = FALSE] if (nrow(r) == 0L) return(stats::setNames(rep(NA_real_, 3), c("est", "lo", "hi"))) stats::setNames(c(r$estimate[1L], r$conf.low[1L], r$conf.high[1L]), c("est", "lo", "hi")) } # The cache keys each sex as "dsuzukii:". cache_has_suzukii <- FALSE if (has_cache) { cache <- readRDS(cache_path) cache_has_suzukii <- !is.null(cache$bayesian) && "dataset" %in% names(cache$bayesian) && all(paste0("dsuzukii:", sex_levels) %in% cache$bayesian$dataset) } if (cache_has_suzukii) { pick <- function(df, parm, g, cols) { r <- df[df$dataset == paste0("dsuzukii:", g) & df$parameter == parm, , drop = FALSE] if (nrow(r) == 0L) return(stats::setNames(rep(NA_real_, 3), c("est", "lo", "hi"))) stats::setNames(as.numeric(r[1L, cols]), c("est", "lo", "hi")) } one_row <- function(g, parm, quantity) { ts <- pick(cache$two_stage, parm, g, c("estimate", "lower", "upper")) by <- pick(cache$bayesian, parm, g, c("median", "lower", "upper")) pr <- pr_of(parm, g) data.frame( Sex = sex_label[[g]], Quantity = quantity, `Two-stage (delta CI)` = fmt(ts["est"], ts["lo"], ts["hi"]), `bayesTLS (95% CrI)` = fmt(by["est"], by["lo"], by["hi"]), `freqTLS (profile CI)` = fmt(pr["est"], pr["lo"], pr["hi"]), check.names = FALSE ) } rows <- do.call(rbind, lapply(sex_levels, function(g) rbind( one_row(g, "CTmax", "CTmax (°C)"), one_row(g, "z", "z (°C / decade)") ))) knitr::kable(rows, row.names = FALSE, caption = "D. suzukii CTmax and z per sex from three estimators (matched constant-shape configuration, tref = 240 min).") } else { prof_tbl <- do.call(rbind, lapply(sex_levels, function(g) { ct <- pr_of("CTmax", g); z <- pr_of("z", g) data.frame( Sex = sex_label[[g]], `CTmax (°C), profile CI` = fmt(ct["est"], ct["lo"], ct["hi"]), `z (°C / decade), profile CI` = fmt(z["est"], z["lo"], z["hi"]), check.names = FALSE ) })) knitr::kable(prof_tbl, row.names = FALSE, caption = "Live freqTLS per-sex estimates (no Stan). The bayesTLS and two-stage columns appear once the maintainer-built cache carries the D. suzukii sexes.") } ``` Under the matched configuration the `freqTLS` profile-likelihood confidence intervals and the `bayesTLS` credible intervals summarise the *same* fitted curve per sex — one prior-free and by optimisation, the other with a prior and MCMC — and the two model-based fits agree tightly, both reproducing the published thermal limits (`CTmax` ≈ 35.2 °C; `z` ≈ 3.0 for females and 3.2 for males). The classical two-stage estimate sits a few tenths of a degree lower on `CTmax` (≈ 34.8–35.0 °C) with wider, less precise intervals — the expected behaviour of the cruder per-temperature method, and a reminder that the headline equivalence here is between the two model-based fits (see `vignette("comparing-to-bayesTLS")` and `vignette("model-math")`). ## Boundary: lethal only `freqTLS` is purpose-built for the 4PL survival/proportion model class. The Ørsted study measured two further *D. suzukii* endpoints that are **non-goals** here: * **Heat-coma (knockdown)** is a right-censored time-to-event endpoint (`brms::bf(... | cens(cens) ~ ...)`), a model class outside the 4PL count engine. Its rate-multiplier `T_crit` is also endpoint-conditional (knockdown `z` < lethal `z`), so it must not be reported as a lethal threshold. For the heat-coma analysis, **see `bayesTLS`**. * **Productivity (fertility)** is modelled with a hurdle-Gamma (a zero-inflation process plus a positive clutch-size process), two response processes, neither a 4PL survival curve, and the magnitude component has no LT50 or `z` at all. For productivity, **see `bayesTLS`**. This article covers the **lethal-by-sex** subset only, which is the part of the *D. suzukii* analysis that fits the `freqTLS` model class. **Where to next:** `vignette("heat-injury")` walks through the `T_crit` / heat-injury workflow that this absolute-threshold fit feeds. ## Session info ```{r session} sessionInfo() ```