Skip to contents

Why extend epidist?

epidist handles the parts of delay estimation that are tedious and easy to get wrong. Censoring, truncation, the brms formula interface, priors, and the Stan code that ties them together.

If you are building a model that shares those problems but has a different likelihood, you might want to do so by extending epidist.

Two packages already do this, in two different ways.

Adding a data source

Write a method that turns your class into one epidist understands.

Code
#' @importFrom epidist as_epidist_linelist_data
#' @export
as_epidist_linelist_data.my_class <- function(data, ...) {
  return(as_epidist_linelist_data(
    as.data.frame(data),
    pdate_lwr = "onset_date",
    sdate_lwr = "report_date"
  ))
}
Code
my_data |>
  as_epidist_linelist_data() |>
  as_epidist_marginal_model() |>
  epidist(formula = mu ~ 1)

Adding a model type

A model type is an S3 class plus methods for six generics.

Three you have to supply.

Generic What you supply If you skip it
assert_epidist() What columns your data must have and what must be true of them Errors
epidist_family_model() The brms custom family, including the likelihood name and its vars Returns the family unchanged
epidist_stancode() The Stan code injected through brms stanvars NULL, so no likelihood

Three are optional.

Generic What you supply If you skip it
epidist_formula_model() How the user’s formula is rewritten, usually adding vreal() terms Returns the formula unchanged
epidist_transform_data_model() Reshaping before fitting Returns the data unchanged
epidist_model_prior() Model specific priors NULL

You also write a constructor that sets your class, by convention as_epidist_<name>_model().

The skeleton looks like this.

Code
as_epidist_my_model <- function(data) {
  class(data) <- c("epidist_my_model", class(data))
  assert_epidist(data)
  return(data)
}

#' @importFrom epidist assert_epidist
#' @export
assert_epidist.epidist_my_model <- function(data, ...) {
  checkmate::assert_names(names(data), must.include = c("delay", "n"))
  return(invisible(NULL))
}

#' @importFrom epidist epidist_family_model
#' @export
epidist_family_model.epidist_my_model <- function(data, family, ...) {
  return(brms::custom_family(
    paste0("my_model_", family$family),
    dpars = family$dpars,
    vars = "vreal1",
    loop = FALSE
  ))
}

#' @importFrom epidist epidist_formula_model
#' @export
epidist_formula_model.epidist_my_model <- function(data, formula, ...) {
  return(stats::update(formula, delay | vreal(n) ~ .))
}

#' @importFrom epidist epidist_stancode
#' @export
epidist_stancode.epidist_my_model <- function(data, family, formula, ...) {
  return(brms::stanvar(block = "functions", scode = "// your lpdf here"))
}

Register the S3 methods in your NAMESPACE and import the generics from epidist, as cfrnow does:

importFrom(epidist,assert_epidist)
importFrom(epidist,epidist_family_model)
importFrom(epidist,epidist_formula_model)
importFrom(epidist,epidist_stancode)
S3method(assert_epidist,epidist_my_model)
S3method(epidist_family_model,epidist_my_model)
S3method(epidist_formula_model,epidist_my_model)
S3method(epidist_stancode,epidist_my_model)

Users then call epidist() exactly as they would for a built-in model.

Code
data |>
  as_epidist_my_model() |>
  epidist(formula = mu ~ 1)

Exploring how cfrnow extends epidist

cfrnow fits a mixture-cure survival model to estimate a real-time case fatality ratio. Every method it needs is in R/cure_model.R.

Method
as_epidist_cure_model() Sets the class
assert_epidist() Checks the columns its likelihood needs
epidist_family_model() Declares the custom family and its vars
epidist_formula_model() Adds the vreal() terms
epidist_transform_data_model() Reshapes before fitting
epidist_model_prior() Returns NULL
epidist_stancode() Injects the mixture-cure likelihood

cfrnow also exports its own frontend. Its users call prepare_cfr_data(), fit_cfr() and pp_check_cfr() rather than epidist().

Getting help

If you are building an extension and something in this interface is awkward, please open an issue.