Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion DESCRIPTION
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Package: cfrnow
Title: Real-Time Case Fatality Ratio via a Mixture-Cure Survival Model
Version: 0.1.0.9000
Version: 0.2.0
Authors@R:
person("Sebastian", "Funk", , "sebastian.funk@lshtm.ac.uk",
role = c("aut", "cre"))
Expand Down
8 changes: 7 additions & 1 deletion NEWS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# cfrnow (development version)
# cfrnow 0.2.0

* `fit_cfr()` and `simulate_linelist()` support a `Weibull()` onset-to-death (and
recovery) delay, alongside `LogNormal()` and `Gamma()`.
* Delay parameterisation now uses distspec's exported `natural_params()` in place
of an internal helper, tracking the distspec API.
* Added a "Stratified and partially-pooled CFR" vignette covering no-, complete-
and partial-pooling CFR fits and per-group `summary()` output.
* `pp_check_cfr()` runs a posterior-predictive check on a fit: it draws replicate
line-list outcomes from the posterior, replays the real-time truncation, and
compares the observed death counts (plus recoveries in a two-outcome fit) and
Expand Down
6 changes: 3 additions & 3 deletions R/cure_model.R
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
#'
#' The delay distribution's location is `mu` (as `epidist` expects); the cure
#' probability `cfr` is an additional dpar with a logit link. Supported delay
#' families are `lognormal()` and `Gamma()`.
#' families are `lognormal()`, `Gamma()` and `Weibull()`.
#'
#' @name cfrnow-cure-model
#' @keywords internal
Expand Down Expand Up @@ -81,8 +81,8 @@ assert_epidist.epidist_cure_model <- function(data, ...) {
}

.assert_delay_family <- function(family, what = "delay") {
if (!family$family %in% c("lognormal", "gamma")) {
stop("cfrnow supports lognormal() and Gamma() ", what, "s only.",
if (!family$family %in% c("lognormal", "gamma", "weibull")) {
stop("cfrnow supports lognormal(), Gamma() and Weibull() ", what, "s only.",
call. = FALSE
)
}
Expand Down
9 changes: 5 additions & 4 deletions R/fit_cfr.R
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@
#'
#' The delay's native parameters may each be a fixed number (held fixed; fixing
#' the whole delay gives the Ghani/Nishiura estimator) or a `Normal()` prior
#' (co-estimated). The family (`LogNormal()` or `Gamma()`) sets the delay
#' distribution. `cfr_prior` is a `Beta()`; it matters because the CFR is weakly
#' (co-estimated). The family (`LogNormal()`, `Gamma()` or `Weibull()`) sets the
#' delay distribution. `cfr_prior` is a `Beta()`; it matters because the CFR is
#' weakly
#' identified early on (`Beta(1, 1)` is uniform, `Beta(1, 9)` favours a low CFR,
#' `Beta(6.6, 13.4)` suits a high-fatality pathogen).
#'
Expand All @@ -22,8 +23,8 @@
#' @param data Output of [prepare_cfr_data()], or an `epidist_cure_model` /
#' data frame with `y`, `outcome`, `pwindow`, `swindow`.
#' @param delay Onset-to-death delay as a \pkg{distspec} distribution
#' ([distspec::LogNormal()] or [distspec::Gamma()]) whose native parameters
#' are fixed numbers or `Normal()` priors.
#' ([distspec::LogNormal()], [distspec::Gamma()] or [distspec::Weibull()])
#' whose native parameters are fixed numbers or `Normal()` priors.
#' @param cfr_prior CFR prior as a [distspec::Beta()]. Defaults to `Beta(1, 1)`.
#' @param recovery_delay Optional onset-to-recovery delay (same form as `delay`)
#' for the two-outcome fit; may use a different family from `delay`.
Expand Down
20 changes: 16 additions & 4 deletions R/simulate_linelist.R
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@
#' @param n Number of cases.
#' @param cfr True case fatality ratio.
#' @param delay Onset-to-death delay: a distspec distribution
#' ([distspec::LogNormal()] or [distspec::Gamma()]) with fixed parameters.
#' ([distspec::LogNormal()], [distspec::Gamma()] or [distspec::Weibull()])
#' with fixed parameters.
#' @param recovery Optional onset-to-recovery delay (same form as `delay`); when
#' given, non-fatal cases get a `recovery_date`.
#' @param onset_start First possible onset date.
Expand Down Expand Up @@ -74,15 +75,26 @@ simulate_linelist <- function(n = 200, cfr = 0.5, delay, recovery = NULL,
#' @noRd
sample_delay <- function(n, delay) {
fam <- get_distribution(delay)
pars <- get_parameters(delay)[delay_native_order(fam)]
pars <- get_parameters(delay)[natural_params(delay)]
if (!all(vapply(pars, is.numeric, logical(1)))) {
stop("simulate_linelist() needs a delay with fixed parameters (numbers), ",
"not priors.",
call. = FALSE
)
}
switch(fam,
out <- switch(fam,
lognormal = stats::rlnorm(n, pars[["meanlog"]], pars[["sdlog"]]),
gamma = stats::rgamma(n, shape = pars[["shape"]], rate = pars[["rate"]])
gamma = stats::rgamma(n, shape = pars[["shape"]], rate = pars[["rate"]]),
weibull = stats::rweibull(
n,
shape = pars[["shape"]], scale = pars[["scale"]]
)
)
if (is.null(out)) {
stop("simulate_linelist() supports LogNormal(), Gamma() and Weibull() ",
"delays only.",
call. = FALSE
)
}
out
}
7 changes: 6 additions & 1 deletion R/summarise_cfr.R
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,19 @@ naive_cfr <- function(n_deaths, n_cases) {
#'
#' `loc_var` is the location intercept and `scale_var` the second parameter's
#' intercept; the second parameter is log-linked. Handles lognormal (loc is
#' meanlog) and gamma (loc is log-mean, second parameter is shape).
#' meanlog), gamma and weibull (loc is log-mean, second parameter is the shape).
#' @noRd
.delay_moments <- function(dr, loc_var, scale_var, family) {
loc <- dr[[loc_var]]
sc <- exp(dr[[scale_var]])
if (family == "lognormal") {
dmean <- exp(loc + sc^2 / 2)
list(mean = dmean, sd = sqrt(exp(sc^2) - 1) * dmean)
} else if (family == "weibull") {
# brms weibull: mu is the mean (log link), sc is the shape k
dmean <- exp(loc)
cv <- sqrt(gamma(1 + 2 / sc) / gamma(1 + 1 / sc)^2 - 1)
list(mean = dmean, sd = dmean * cv)
} else {
dmean <- exp(loc)
list(mean = dmean, sd = dmean / sqrt(sc))
Expand Down
79 changes: 62 additions & 17 deletions R/translate.R
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,37 @@
)
}

# distspec Weibull() uses (shape, scale); brms weibull uses (mu = mean, shape),
# both log-linked. Build the mu prior from mean = scale * gamma(1 + 1 / shape),
# propagating shape/scale uncertainty to log(mu) by the delta method.
.weibull_mu_prior <- function(shape_spec, scale_spec, dpar) {
mu_of <- function(k, lambda) lambda * gamma(1 + 1 / k)
if (!is.null(shape_spec$fixed) && !is.null(scale_spec$fixed)) {
return(.prior_row(
list(fixed = mu_of(shape_spec$fixed, scale_spec$fixed)), dpar, TRUE
))
}
kc <- shape_spec$fixed %||% shape_spec$mean
lc <- scale_spec$fixed %||% scale_spec$mean
# d log(mu)/d shape uses digamma(1 + 1/k); sign drops on squaring
d_shape <- digamma(1 + 1 / kc) / kc^2
vlog <- sqrt(
((scale_spec$sd %||% 0) / lc)^2 + (d_shape * (shape_spec$sd %||% 0))^2
)
prior_args <- if (nzchar(dpar)) {
list(class = "Intercept", dpar = dpar)
} else {
list(class = "Intercept")
}
do.call(
brms::set_prior,
c(
list(sprintf("normal(%.6f, %.6f)", log(lc) + lgamma(1 + 1 / kc), vlog)),
prior_args
)
)
}

# A distspec delay -> list(family, prior). `main = TRUE` targets the death delay
# (mu is the main dpar); `main = FALSE` the recovery delay (r-prefixed dpars).
.delay_family_prior <- function(delay, main = TRUE) {
Expand All @@ -78,25 +109,39 @@
fam <- get_distribution(delay)
pars <- get_parameters(delay)
loc_dpar <- if (main) "" else "rmu"
if (fam == "lognormal") {
scale_dpar <- if (main) "sigma" else "rsigma"
prior <- c(
.prior_row(.param_spec(pars$meanlog), loc_dpar, FALSE),
.prior_row(.param_spec(pars$sdlog), scale_dpar, TRUE)
)
list(family = brms::lognormal(), prior = prior)
} else if (fam == "gamma") {
scale_dpar <- if (main) "shape" else "rshape"
sh <- .param_spec(pars$shape)
rate_spec <- .param_spec(pars$rate)
prior <- c(
.gamma_mu_prior(sh, rate_spec, loc_dpar),
.prior_row(sh, scale_dpar, TRUE)
scale_dpar <- if (main) "sigma" else "rsigma"
shape_dpar <- if (main) "shape" else "rshape"
out <- switch(fam,
lognormal = {
prior <- c(
.prior_row(.param_spec(pars$meanlog), loc_dpar, FALSE),
.prior_row(.param_spec(pars$sdlog), scale_dpar, TRUE)
)
list(family = brms::lognormal(), prior = prior)
},
gamma = {
sh <- .param_spec(pars$shape)
prior <- c(
.gamma_mu_prior(sh, .param_spec(pars$rate), loc_dpar),
.prior_row(sh, shape_dpar, TRUE)
)
list(family = stats::Gamma(link = "log"), prior = prior)
},
weibull = {
sh <- .param_spec(pars$shape)
prior <- c(
.weibull_mu_prior(sh, .param_spec(pars$scale), loc_dpar),
.prior_row(sh, shape_dpar, TRUE)
)
list(family = brms::weibull(), prior = prior)
}
)
if (is.null(out)) {
stop("cfrnow supports LogNormal(), Gamma() and Weibull() delays only.",
call. = FALSE
)
list(family = stats::Gamma(link = "log"), prior = prior)
} else {
stop("cfrnow supports LogNormal() and Gamma() delays only.", call. = FALSE)
}
out
}

# A Normal(m, s) on logit(cfr) whose induced mean/sd on the [0, 1] CFR scale
Expand Down
20 changes: 0 additions & 20 deletions R/utils.R

This file was deleted.

2 changes: 1 addition & 1 deletion man/cfrnow-cure-model.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 6 additions & 5 deletions man/fit_cfr.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion man/simulate_linelist.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions tests/testthat/test-families.R
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ test_that("simulated delays match the requested mean and sd", {
set.seed(1)
specs <- list(
LogNormal(mean = 12.75, sd = 7),
Gamma(mean = 12.75, sd = 7)
Gamma(mean = 12.75, sd = 7),
Weibull(mean = 12.75, sd = 7)
)
for (delay in specs) {
ll <- simulate_linelist(n = 20000, cfr = 1, delay = delay)
Expand All @@ -28,7 +29,7 @@ test_that("a recovery delay adds recoveries for non-fatal cases only", {
})

test_that("unsupported families and prior-parameter delays are rejected", {
expect_error(delay_native_order("weibull"), "unsupported")
expect_error(sample_delay(5, Exp(rate = 1)), "supports") # exp not supported
expect_error(sample_delay(5, LogNormal(
meanlog = Normal(2, 0.1), sdlog = Normal(0.5, 0.1)
)), "fixed")
Expand Down
18 changes: 18 additions & 0 deletions tests/testthat/test-fit_cfr.R
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,24 @@ test_that("a gamma delay recovers the CFR and delay moments", {
expect_equal(s[s$quantity == "delay_mean", "q50"], 8, tolerance = 0.8)
})

test_that("a weibull delay recovers the CFR and delay moments", {
Comment thread
sbfnk marked this conversation as resolved.
skip_if_no_cmdstan()
set.seed(3)
ll <- simulate_linelist(
n = 2000, cfr = 0.4, onset_days = 40,
delay = Weibull(shape = 1.5, scale = 13)
)
d <- prepare_cfr_data(ll, obs_time = max(ll$onset_date) - 2)
s <- summary(fit_quick(d,
delay = Weibull(shape = Normal(1.5, 0.4), scale = Normal(13, 3)),
cfr_prior = Beta(1, 1)
))
true_mean <- 13 * gamma(1 + 1 / 1.5) # Weibull mean = scale * Gamma(1 + 1/shape)
expect_lt(s[s$quantity == "cfr", "q2.5"], 0.4)
expect_gt(s[s$quantity == "cfr", "q97.5"], 0.4)
expect_equal(s[s$quantity == "delay_mean", "q50"], true_mean, tolerance = 1.5)
})

test_that("a fixed delay runs the Ghani/Nishiura estimator (delay held constant)", {
skip_if_no_cmdstan()
set.seed(4)
Expand Down
Loading