Prior choice and specification guide

1 Introduction

This vignette provides practical guidance on specifying and choosing priors for {EpiNow2} models. While the package provides sensible defaults for all priors, understanding when and how to modify them can improve model performance for your specific application.

This guide covers priors for the three main modelling functions:

What this guide covers:

What this guide does NOT cover:

Key principle: All defaults in {EpiNow2} aim to provide sensible defaults that work across a range of pathogens and settings. For a specific application with domain knowledge, you may benefit from more informative priors.

Important default: The default generation time is Fixed(1), meaning all transmission occurs after 1 day. With this default, \(R_t\) represents the daily exponential growth rate rather than the traditional reproduction number. For epidemiologically meaningful \(R_t\) estimates, you should always specify a realistic generation time distribution from the literature (see Generation time distribution).

2 Set up

We load the EpiNow2 package and the posterior and data.table packages which we will use for convergence diagnostics and data wrangling, respectively.

Code
library(EpiNow2)
#> 
#> Attaching package: 'EpiNow2'
#> The following object is masked from 'package:stats':
#> 
#>     Gamma
library(posterior)
#> This is posterior version 1.6.1.9000
#> 
#> Attaching package: 'posterior'
#> The following objects are masked from 'package:stats':
#> 
#>     mad, sd, var
#> The following objects are masked from 'package:base':
#> 
#>     %in%, match
library(data.table)

We also set up example data and parameters that will be used in code examples throughout this vignette.

Code
# Example data for estimate_infections
reported_cases <- example_confirmed[1:60]

# Example data for estimate_secondary
secondary_data <- copy(example_confirmed)[1:60]
setnames(secondary_data, "confirm", "primary")
secondary_data[, scaling := 0.4]
secondary_data[, meanlog := 1.8][, sdlog := 0.5]
secondary_data <- convolve_and_scale(secondary_data, type = "incidence")

# Example data for estimate_truncation
truncation_data <- example_truncated

# Example delays
reporting_delay <- LogNormal(mean = 2, sd = 1, max = 10)
delay <- example_incubation_period + reporting_delay

# Fast Stan settings for vignette building
# (Use default settings for actual analyses)
stan <- stan_opts(
  samples = 100,
  warmup = 100,
  chains = 2,
  control = list(adapt_delta = 0.9)
)

3 Overview of all priors

The following tables list all priors available in {EpiNow2}, organised by modelling function.

3.1 estimate_infections() priors

estimate_infections() supports two models: renewal equation (default) and non-mechanistic/deconvolution.

Component Model Function Parameter Default Notes
Reproduction number (Rt)
Initial R₀ / mean Rt Renewal only rt_opts() prior LogNormal(mean = 1, sd = 1) When choosing gp_on = "R0", reverts to this when no data
Random walk
Step size SD Renewal only rt_opts() rw HalfNormal(0, 0.1) Set step size via rw argument
Gaussian Process
Length scale Both gp_opts() ls LogNormal(mean = 21, sd = 7, max = 60) Controls smoothness over time
Magnitude Both gp_opts() alpha Normal(mean = 0, sd = 0.01) Controls amplitude of variations
Back-calculation
Prior source Non-mechanistic only backcalc_opts() prior "reports" Options: “reports”, “infections”, “none”
Observation model
Overdispersion Both obs_opts() dispersion Normal(mean = 0, sd = 0.25) 1/√φ parameterisation
Scaling Both obs_opts() scale Fixed(1) Ascertainment rate
Day of week Both (internal) (auto) Dirichlet(1, ..., 1) Turn off via week_effect = FALSE
Generation time
Parameters Renewal only gt_opts() dist Fixed(1) Default: Rt is daily exponential growth rate
Delays
Parameters Both delay_opts() dist Fixed(0) No delay by default
Truncation
Distribution Both trunc_opts() dist Fixed(0) No truncation by default

3.2 estimate_secondary() priors

Component Function Parameter Default Notes
Delays (primary to secondary)
Parameters delay_opts() dist LogNormal(meanlog = Normal(2.5, 0.5), sdlog = Normal(0.47, 0.25), max = 30) Time from primary to secondary event
Observation model
Scaling obs_opts() scale Fixed(1) Fraction of primary events leading to secondary
Overdispersion obs_opts() dispersion Normal(mean = 0, sd = 0.25) 1/√φ parameterisation
Day of week (internal) (auto) Dirichlet(1, ..., 1) Turn off via week_effect = FALSE

3.3 estimate_truncation() priors

Component Function Parameter Default Notes
Truncation distribution
Parameters trunc_opts() dist LogNormal(meanlog = Normal(0, 1), sdlog = Normal(1, 1), max = 10) Describes reporting delays
Observation model
Overdispersion (internal) (auto) HalfNormal(0, 1) 1/√φ parameterisation; not directly adjustable
Noise term (internal) (auto) HalfNormal(0, 1) Not directly adjustable

4 Prior impacts and choice guidance

This section explains what effects you can expect from modifying each prior, and when you might want to do so.

4.1 Reproduction number

What it controls: The prior distribution for the reproduction number. Its role depends on the Gaussian Process specification:

Default: LogNormal(mean = 1, sd = 1)

This default corresponds to a median of 1 (range 0.4 to 2.7 at 95% prior probability), centred on the epidemic threshold.

Expected impact of changes:

When to modify:

Example:

Code
# For a measles outbreak with strong prior knowledge
rt_opts(prior = LogNormal(mean = 15, sd = 2))
#> $use_rt
#> [1] TRUE
#> 
#> $rw
#> [1] 0
#> 
#> $use_breakpoints
#> [1] TRUE
#> 
#> $future
#> [1] "latest"
#> 
#> $gp_on
#> [1] "R_t-1"
#> 
#> $pop_period
#> [1] "forecast"
#> 
#> $pop_floor
#> [1] 1
#> 
#> $growth_method
#> [1] "infections"
#> 
#> $pop
#> - fixed value:
#>   0
#> 
#> $prior
#> - lognormal distribution:
#>   meanlog:
#>     2.7
#>   sdlog:
#>     0.13
#> 
#> attr(,"class")
#> [1] "rt_opts" "list"

# For uncertain but likely growing outbreak
rt_opts(prior = LogNormal(mean = 2, sd = 1))
#> $use_rt
#> [1] TRUE
#> 
#> $rw
#> [1] 0
#> 
#> $use_breakpoints
#> [1] TRUE
#> 
#> $future
#> [1] "latest"
#> 
#> $gp_on
#> [1] "R_t-1"
#> 
#> $pop_period
#> [1] "forecast"
#> 
#> $pop_floor
#> [1] 1
#> 
#> $growth_method
#> [1] "infections"
#> 
#> $pop
#> - fixed value:
#>   0
#> 
#> $prior
#> - lognormal distribution:
#>   meanlog:
#>     0.58
#>   sdlog:
#>     0.47
#> 
#> attr(,"class")
#> [1] "rt_opts" "list"

# Stationary GP reverting to R_t = 2 when no data
rt_opts(
  prior = LogNormal(mean = 2, sd = 0.5),
  gp_on = "R0"
)
#> $use_rt
#> [1] TRUE
#> 
#> $rw
#> [1] 0
#> 
#> $use_breakpoints
#> [1] TRUE
#> 
#> $future
#> [1] "latest"
#> 
#> $gp_on
#> [1] "R0"
#> 
#> $pop_period
#> [1] "forecast"
#> 
#> $pop_floor
#> [1] 1
#> 
#> $growth_method
#> [1] "infections"
#> 
#> $pop
#> - fixed value:
#>   0
#> 
#> $prior
#> - lognormal distribution:
#>   meanlog:
#>     0.66
#>   sdlog:
#>     0.25
#> 
#> attr(,"class")
#> [1] "rt_opts" "list"

4.2 Gaussian Process length scale

What it controls: The smoothness of changes in the reproduction number over time, measured in days. Larger values = smoother changes; smaller values = more rapid fluctuations.

Default: LogNormal(mean = 21, sd = 7, max = 60)

This corresponds to a median of 21 days (range 9 to 36 days at 95% prior probability), reflecting gradual changes typical of epidemics with interventions happening over weeks.

Expected impact of changes:

When to modify:

Example:

Code
# For outbreak with weekly policy changes
gp_opts(ls = LogNormal(mean = 7, sd = 3, max = 30))
#> $basis_prop
#> [1] 0.2
#> 
#> $boundary_scale
#> [1] 1.5
#> 
#> $ls
#> - lognormal distribution (max: 30):
#>   meanlog:
#>     1.9
#>   sdlog:
#>     0.41
#> 
#> $alpha
#> - normal distribution:
#>   mean:
#>     0
#>   sd:
#>     0.01
#> 
#> $kernel
#> [1] "matern"
#> 
#> $matern_order
#> [1] 1.5
#> 
#> $w0
#> [1] 1
#> 
#> attr(,"class")
#> [1] "gp_opts" "list"

# For gradually evolving endemic disease
gp_opts(ls = LogNormal(mean = 28, sd = 10, max = 90))
#> $basis_prop
#> [1] 0.2
#> 
#> $boundary_scale
#> [1] 1.5
#> 
#> $ls
#> - lognormal distribution (max: 90):
#>   meanlog:
#>     3.3
#>   sdlog:
#>     0.35
#> 
#> $alpha
#> - normal distribution:
#>   mean:
#>     0
#>   sd:
#>     0.01
#> 
#> $kernel
#> [1] "matern"
#> 
#> $matern_order
#> [1] 1.5
#> 
#> $w0
#> [1] 1
#> 
#> attr(,"class")
#> [1] "gp_opts" "list"

4.3 Gaussian Process magnitude

What it controls: The amplitude of variations in the Gaussian Process.

Default: Normal(mean = 0, sd = 0.01)

For the renewal equation model, this corresponds to a half-normal (since negative values are truncated) with 95% of prior mass below 0.02, reflecting small changes in \(\log(R_t)\).

Expected impact of changes:

When to modify:

Example:

Code
# For outbreak with major interventions
gp_opts(alpha = Normal(mean = 0, sd = 0.05))
#> $basis_prop
#> [1] 0.2
#> 
#> $boundary_scale
#> [1] 1.5
#> 
#> $ls
#> - lognormal distribution (max: 60):
#>   meanlog:
#>     3
#>   sdlog:
#>     0.32
#> 
#> $alpha
#> - normal distribution:
#>   mean:
#>     0
#>   sd:
#>     0.05
#> 
#> $kernel
#> [1] "matern"
#> 
#> $matern_order
#> [1] 1.5
#> 
#> $w0
#> [1] 1
#> 
#> attr(,"class")
#> [1] "gp_opts" "list"

# For very stable transmission
gp_opts(alpha = Normal(mean = 0, sd = 0.005))
#> $basis_prop
#> [1] 0.2
#> 
#> $boundary_scale
#> [1] 1.5
#> 
#> $ls
#> - lognormal distribution (max: 60):
#>   meanlog:
#>     3
#>   sdlog:
#>     0.32
#> 
#> $alpha
#> - normal distribution:
#>   mean:
#>     0
#>   sd:
#>     0.005
#> 
#> $kernel
#> [1] "matern"
#> 
#> $matern_order
#> [1] 1.5
#> 
#> $w0
#> [1] 1
#> 
#> attr(,"class")
#> [1] "gp_opts" "list"

4.4 Random walk for \(R_t\) (alternative to GP)

What it controls: Step-wise changes in \(R_t\) at fixed intervals instead of smooth GP changes.

Default: Not used (GP is default). When enabled via rt_opts(rw = 7), the prior SD is HalfNormal(0, 0.1).

Expected impact of changes:

When to modify:

Example:

Code
# Weekly step changes
fit_rw <- estimate_infections(
  reported_cases,
  generation_time = gt_opts(example_generation_time),
  delays = delay_opts(delay),
  rt = rt_opts(prior = LogNormal(mean = 2, sd = 0.5), rw = 7),
  gp = NULL,  # Disable GP when using random walk
  stan = stan
)
#> Warning: The largest R-hat is NA, indicating chains have not mixed.
#> Running the chains for more iterations may help. See
#> https://mc-stan.org/misc/warnings.html#r-hat
#> Warning: Bulk Effective Samples Size (ESS) is too low, indicating posterior means and medians may be unreliable.
#> Running the chains for more iterations may help. See
#> https://mc-stan.org/misc/warnings.html#bulk-ess
#> Warning: Tail Effective Samples Size (ESS) is too low, indicating posterior variances and tail quantiles may be unreliable.
#> Running the chains for more iterations may help. See
#> https://mc-stan.org/misc/warnings.html#tail-ess

4.5 Observation model: overdispersion

What it controls: How much extra variability exists in reported cases beyond Poisson variation. The parameter is 1/√φ where φ is the negative binomial overdispersion.

Default: Normal(mean = 0, sd = 0.25)

With truncation at 0, this half-normal has 95% prior mass below 0.5, corresponding to moderate overdispersion.

Expected impact of changes:

When to modify:

Example:

Code
# For highly variable reporting
obs_opts(dispersion = Normal(mean = 0, sd = 0.5))
#> $family
#> [1] "negbin"
#> 
#> $dispersion
#> - normal distribution:
#>   mean:
#>     0
#>   sd:
#>     0.5
#> 
#> $weight
#> [1] 1
#> 
#> $week_effect
#> [1] TRUE
#> 
#> $week_length
#> [1] 7
#> 
#> $scale
#> - fixed value:
#>   1
#> 
#> $accumulate
#> [1] 0
#> 
#> $likelihood
#> [1] TRUE
#> 
#> $return_likelihood
#> [1] FALSE
#> 
#> $na_as_missing_default_used
#> [1] TRUE
#> 
#> attr(,"class")
#> [1] "obs_opts" "list"

# For stable, high-quality surveillance
obs_opts(family = "poisson")  # No dispersion parameter needed
#> $family
#> [1] "poisson"
#> 
#> $dispersion
#> - normal distribution:
#>   mean:
#>     0
#>   sd:
#>     0.25
#> 
#> $weight
#> [1] 1
#> 
#> $week_effect
#> [1] TRUE
#> 
#> $week_length
#> [1] 7
#> 
#> $scale
#> - fixed value:
#>   1
#> 
#> $accumulate
#> [1] 0
#> 
#> $likelihood
#> [1] TRUE
#> 
#> $return_likelihood
#> [1] FALSE
#> 
#> $na_as_missing_default_used
#> [1] TRUE
#> 
#> attr(,"class")
#> [1] "obs_opts" "list"

4.6 Observation model: scaling

What it controls: The scale parameter in obs_opts() is shared by both estimate_infections() and estimate_secondary(), but represents different quantities in each context:

Default: Fixed(1) for both functions

For estimate_infections(), this assumes all infections are reported. For estimate_secondary(), this assumes a 1:1 relationship between primary and secondary events (which is rarely realistic).

Expected impact of changes:

4.6.1 For estimate_infections():

  • Estimate scaling (e.g., Normal(mean=0.3, sd=0.1)): Allows model to estimate underreporting. Reported cases = scaling × latent infections × delay convolution. Useful when you know reporting is incomplete but don’t know the rate.
  • Fixed lower value (e.g., Fixed(0.5)): Assumes you know 50% are reported. Changes interpretation of infections but not \(R_t\).

4.6.2 For estimate_secondary():

  • Tight prior (e.g., Normal(mean=0.01, sd=0.005)): Constrains estimates near prior (e.g., 1% case fatality rate), faster convergence
  • Wide prior (e.g., Normal(mean=0.05, sd=0.05)): Flexible but may be poorly identified without long time series

When to modify:

4.6.3 For estimate_infections():

  • Nearly always keep at Fixed(1) and interpret “infections” as “reported infections”
  • Estimate only if: You have external data to anchor scale (seroprevalence, death rates) AND can set an informative prior on the ascertainment rate
  • Identifiability warning: Scaling and initial infections are weakly identified; estimating scale can cause convergence issues without strong external information

4.6.4 For estimate_secondary():

  • Always specify based on domain knowledge (unlike estimate_infections(), the default of 1 is rarely appropriate)
  • Use tighter priors with external estimates (e.g., infection fatality rate from serology studies)

Example:

Code
# estimate_infections(): Estimating with informative prior from serology
obs_opts(scale = Normal(mean = 0.2, sd = 0.05, max = 1))
#> $family
#> [1] "negbin"
#> 
#> $dispersion
#> - normal distribution:
#>   mean:
#>     0
#>   sd:
#>     0.25
#> 
#> $weight
#> [1] 1
#> 
#> $week_effect
#> [1] TRUE
#> 
#> $week_length
#> [1] 7
#> 
#> $scale
#> - normal distribution (max: 1):
#>   mean:
#>     0.2
#>   sd:
#>     0.05
#> 
#> $accumulate
#> [1] 0
#> 
#> $likelihood
#> [1] TRUE
#> 
#> $return_likelihood
#> [1] FALSE
#> 
#> $na_as_missing_default_used
#> [1] TRUE
#> 
#> attr(,"class")
#> [1] "obs_opts" "list"

# estimate_infections(): Fixed known ascertainment (rarely used)
obs_opts(scale = Fixed(0.3))
#> $family
#> [1] "negbin"
#> 
#> $dispersion
#> - normal distribution:
#>   mean:
#>     0
#>   sd:
#>     0.25
#> 
#> $weight
#> [1] 1
#> 
#> $week_effect
#> [1] TRUE
#> 
#> $week_length
#> [1] 7
#> 
#> $scale
#> - fixed value:
#>   0.3
#> 
#> $accumulate
#> [1] 0
#> 
#> $likelihood
#> [1] TRUE
#> 
#> $return_likelihood
#> [1] FALSE
#> 
#> $na_as_missing_default_used
#> [1] TRUE
#> 
#> attr(,"class")
#> [1] "obs_opts" "list"

# estimate_secondary(): Case fatality rate ~1% with uncertainty
obs_opts(scale = Normal(mean = 0.01, sd = 0.005, max = 1))
#> $family
#> [1] "negbin"
#> 
#> $dispersion
#> - normal distribution:
#>   mean:
#>     0
#>   sd:
#>     0.25
#> 
#> $weight
#> [1] 1
#> 
#> $week_effect
#> [1] TRUE
#> 
#> $week_length
#> [1] 7
#> 
#> $scale
#> - normal distribution (max: 1):
#>   mean:
#>     0.01
#>   sd:
#>     0.005
#> 
#> $accumulate
#> [1] 0
#> 
#> $likelihood
#> [1] TRUE
#> 
#> $return_likelihood
#> [1] FALSE
#> 
#> $na_as_missing_default_used
#> [1] TRUE
#> 
#> attr(,"class")
#> [1] "obs_opts" "list"

4.7 Generation time distribution

What it controls: The distribution of times from infection of a primary case to infection of secondary cases. Fundamental to the renewal equation.

Default: Fixed(1) (all transmission after 1 day - leads to Rt = exponential growth rate)

Expected impact of changes:

When to modify:

Example:

Code
# Well-established generation time (COVID-19 example)
gt_opts(example_generation_time)  # Uses package data
#> - gamma distribution (max: 14):
#>   shape:
#>     - normal distribution:
#>       mean:
#>         1.4
#>       sd:
#>         0.48
#>   rate:
#>     - normal distribution:
#>       mean:
#>         0.38
#>       sd:
#>         0.25

# Custom uncertain generation time
gt_opts(
  Gamma(
    shape = Normal(mean = 3, sd = 0.5),
    rate = Normal(mean = 2, sd = 0.3),
    max = 14
  )
)
#> - gamma distribution (max: 14):
#>   shape:
#>     - normal distribution:
#>       mean:
#>         3
#>       sd:
#>         0.5
#>   rate:
#>     - normal distribution:
#>       mean:
#>         2
#>       sd:
#>         0.3

# Custom fixed generation time
gt_opts(Gamma(shape = 2.5, rate = 1.5, max = 10))
#> - gamma distribution (max: 10):
#>   shape:
#>     2.5
#>   rate:
#>     1.5

4.8 Delays (incubation and reporting)

What it controls: The time from infection to observation (e.g., symptom onset + reporting delay).

Default: Fixed(0) (no delay, observe infections immediately)

Expected impact of changes:

When to modify:

Example:

Code
# Simple fixed delay
delay_opts(LogNormal(meanlog = 1.6, sdlog = 0.5, max = 10))
#> - lognormal distribution (max: 10):
#>   meanlog:
#>     1.6
#>   sdlog:
#>     0.5

# Combined incubation + reporting delay (additive)
incubation <- LogNormal(meanlog = 1.6, sdlog = 0.5, max = 10)
reporting <- LogNormal(meanlog = 0.5, sdlog = 0.5, max = 5)
delay_opts(incubation + reporting)
#> Composite distribution:
#> - lognormal distribution (max: 10):
#>   meanlog:
#>     1.6
#>   sdlog:
#>     0.5
#> - lognormal distribution (max: 5):
#>   meanlog:
#>     0.5
#>   sdlog:
#>     0.5

# Uncertain delay (advanced)
delay_opts(
  LogNormal(
    meanlog = Normal(1.6, 0.1),
    sdlog = Normal(0.5, 0.1),
    max = 10
  )
)
#> - lognormal distribution (max: 10):
#>   meanlog:
#>     - normal distribution:
#>       mean:
#>         1.6
#>       sd:
#>         0.1
#>   sdlog:
#>     - normal distribution:
#>       mean:
#>         0.5
#>       sd:
#>         0.1

4.9 Truncation

What it controls: Right-truncation of recent data due to reporting delays (recent counts will be revised upward).

Default: No truncation (Fixed(0))

Expected impact of changes:

When to modify:

Example:

Code
# Known truncation from external analysis
trunc_opts(LogNormal(mean = 0.5, sd = 0.5, max = 3))
#> - lognormal distribution (max: 3):
#>   meanlog:
#>     -1
#>   sdlog:
#>     0.83

# Estimated truncation (run separately first)
truncation_estimate <- estimate_truncation(
  truncation_data,
  truncation = trunc_opts(
    LogNormal(
      meanlog = Normal(0, 1),
      sdlog = Normal(1, 1),
      max = 10
    )
  )
)

# Then use in estimate_infections
trunc_opts(get_parameters(truncation_estimate)[["truncation"]])
#> - lognormal distribution (max: 10):
#>   meanlog:
#>     - normal distribution:
#>       mean:
#>         0.9
#>       sd:
#>         0.005
#>   sdlog:
#>     - normal distribution:
#>       mean:
#>         0.6
#>       sd:
#>         0.007

4.10 Model choice in estimate_infections()

estimate_infections() supports two models, each with different prior requirements:

Renewal equation model (default, rt != NULL):

Non-mechanistic model (rt = NULL):

Example:

Code
# Renewal equation model (default)
fit_renewal <- estimate_infections(
  reported_cases,
  generation_time = gt_opts(example_generation_time),
  delays = delay_opts(delay),
  rt = rt_opts(prior = LogNormal(mean = 2, sd = 0.5)),
  stan = stan
)
#> Warning: There were 1 chains where the estimated Bayesian Fraction of Missing Information was low. See
#> https://mc-stan.org/misc/warnings.html#bfmi-low
#> Warning: Examine the pairs() plot to diagnose sampling problems
#> Warning: The largest R-hat is NA, indicating chains have not mixed.
#> Running the chains for more iterations may help. See
#> https://mc-stan.org/misc/warnings.html#r-hat
#> Warning: Bulk Effective Samples Size (ESS) is too low, indicating posterior means and medians may be unreliable.
#> Running the chains for more iterations may help. See
#> https://mc-stan.org/misc/warnings.html#bulk-ess
#> Warning: Tail Effective Samples Size (ESS) is too low, indicating posterior variances and tail quantiles may be unreliable.
#> Running the chains for more iterations may help. See
#> https://mc-stan.org/misc/warnings.html#tail-ess

# Non-mechanistic model
fit_nonmech <- estimate_infections(
  reported_cases,
  delays = delay_opts(delay),
  rt = NULL,  # No Rt estimation
  backcalc = backcalc_opts(prior = "reports"),
  stan = stan
)
#> Warning: ! No generation time distribution given. Using a fixed generation time of 1
#>   day, i.e. the reproduction number is the same as the daily growth rate.
#> ℹ If this was intended then this warning can be silenced by setting `dist =
#>   Fixed(1)`'.
#> Warning: The largest R-hat is 1.15, indicating chains have not mixed.
#> Running the chains for more iterations may help. See
#> https://mc-stan.org/misc/warnings.html#r-hat
#> Warning: Bulk Effective Samples Size (ESS) is too low, indicating posterior means and medians may be unreliable.
#> Running the chains for more iterations may help. See
#> https://mc-stan.org/misc/warnings.html#bulk-ess
#> Warning: Tail Effective Samples Size (ESS) is too low, indicating posterior variances and tail quantiles may be unreliable.
#> Running the chains for more iterations may help. See
#> https://mc-stan.org/misc/warnings.html#tail-ess

4.11 Priors for estimate_secondary()

estimate_secondary() models secondary observations (deaths, hospitalizations) from primary observations (cases, admissions) with a delay and scaling.

Key priors:

4.11.1 Delay between primary and secondary

What it controls: Time from primary event to secondary event (e.g., case to death).

Default: LogNormal(meanlog = Normal(2.5, 0.5), sdlog = Normal(0.47, 0.25), max = 30)

Expected impact:

  • Uncertain parameters: Allow model to learn delay from data, but require longer time series (>30 days)
  • Fixed parameters: Faster, more stable with short time series

When to modify:

  • Use literature estimates when available (e.g., case-to-death ~14-21 days for COVID-19)
  • Make uncertain only with long time series and uncertain external estimates

Example:

Code
# Fixed delay from literature
fit_secondary_fixed <- estimate_secondary(
  secondary_data,
  delays = delay_opts(LogNormal(mean = 14, sd = 5, max = 30)),
  stan = stan
)
#> WARN [2026-02-04 18:29:48] estimate_secondary (chain: 1): The largest R-hat is 1.07, indicating chains have not mixed.
#> Running the chains for more iterations may help. See
#> https://mc-stan.org/misc/warnings.html#r-hat - 
#> WARN [2026-02-04 18:29:48] estimate_secondary (chain: 2): The largest R-hat is 1.07, indicating chains have not mixed.
#> Running the chains for more iterations may help. See
#> https://mc-stan.org/misc/warnings.html#r-hat -
#> WARN [2026-02-04 18:29:48] estimate_secondary (chain: 1): Bulk Effective Samples Size (ESS) is too low, indicating posterior means and medians may be unreliable.
#> Running the chains for more iterations may help. See
#> https://mc-stan.org/misc/warnings.html#bulk-ess - 
#> WARN [2026-02-04 18:29:48] estimate_secondary (chain: 2): Bulk Effective Samples Size (ESS) is too low, indicating posterior means and medians may be unreliable.
#> Running the chains for more iterations may help. See
#> https://mc-stan.org/misc/warnings.html#bulk-ess -
#> WARN [2026-02-04 18:29:48] estimate_secondary (chain: 1): Tail Effective Samples Size (ESS) is too low, indicating posterior variances and tail quantiles may be unreliable.
#> Running the chains for more iterations may help. See
#> https://mc-stan.org/misc/warnings.html#tail-ess - 
#> WARN [2026-02-04 18:29:48] estimate_secondary (chain: 2): Tail Effective Samples Size (ESS) is too low, indicating posterior variances and tail quantiles may be unreliable.
#> Running the chains for more iterations may help. See
#> https://mc-stan.org/misc/warnings.html#tail-ess -

# Uncertain delay (long time series)
fit_secondary_uncertain <- estimate_secondary(
  secondary_data,
  delays = delay_opts(
    LogNormal(
      meanlog = Normal(2, 0.3),
      sdlog = Normal(0.5, 0.2),
      max = 30
    )
  ),
  stan = stan
)
#> WARN [2026-02-04 18:29:48] estimate_secondary (chain: 1): The largest R-hat is 1.52, indicating chains have not mixed.
#> Running the chains for more iterations may help. See
#> https://mc-stan.org/misc/warnings.html#r-hat - 
#> WARN [2026-02-04 18:29:48] estimate_secondary (chain: 2): The largest R-hat is 1.52, indicating chains have not mixed.
#> Running the chains for more iterations may help. See
#> https://mc-stan.org/misc/warnings.html#r-hat -
#> WARN [2026-02-04 18:29:48] estimate_secondary (chain: 1): Bulk Effective Samples Size (ESS) is too low, indicating posterior means and medians may be unreliable.
#> Running the chains for more iterations may help. See
#> https://mc-stan.org/misc/warnings.html#bulk-ess - 
#> WARN [2026-02-04 18:29:48] estimate_secondary (chain: 2): Bulk Effective Samples Size (ESS) is too low, indicating posterior means and medians may be unreliable.
#> Running the chains for more iterations may help. See
#> https://mc-stan.org/misc/warnings.html#bulk-ess -
#> WARN [2026-02-04 18:29:48] estimate_secondary (chain: 1): Tail Effective Samples Size (ESS) is too low, indicating posterior variances and tail quantiles may be unreliable.
#> Running the chains for more iterations may help. See
#> https://mc-stan.org/misc/warnings.html#tail-ess - 
#> WARN [2026-02-04 18:29:48] estimate_secondary (chain: 2): Tail Effective Samples Size (ESS) is too low, indicating posterior variances and tail quantiles may be unreliable.
#> Running the chains for more iterations may help. See
#> https://mc-stan.org/misc/warnings.html#tail-ess -

4.12 Priors for estimate_truncation()

estimate_truncation() estimates the distribution of reporting delays that cause recent data to be revised upward over time.

Key priors:

4.12.1 Truncation distribution parameters

What it controls: The lognormal distribution describing reporting delays.

Default: LogNormal(meanlog = Normal(0, 1), sdlog = Normal(1, 1), max = 10)

Expected impact:

  • Tighter priors: Faster convergence if you have prior knowledge of typical delays
  • Wider priors: More flexible but may be poorly identified

When to modify:

  • Use informative priors if you have data on typical reporting delays
  • Common reporting delays: 1-3 days (test results), 7-14 days (death reporting)

Example:

Code
# Quick turnaround testing (1-3 days typical delay)
fit_trunc_fast <- estimate_truncation(
  truncation_data,
  truncation = trunc_opts(
    LogNormal(
      meanlog = Normal(0.5, 0.3),  # ~1.6 day median delay
      sdlog = Normal(0.5, 0.2),
      max = 7
    )
  ),
  stan = stan
)
#> WARN [2026-02-04 18:29:49] estimate_truncation (chain: 1): The largest R-hat is NA, indicating chains have not mixed.
#> Running the chains for more iterations may help. See
#> https://mc-stan.org/misc/warnings.html#r-hat - 
#> WARN [2026-02-04 18:29:49] estimate_truncation (chain: 2): The largest R-hat is NA, indicating chains have not mixed.
#> Running the chains for more iterations may help. See
#> https://mc-stan.org/misc/warnings.html#r-hat -
#> WARN [2026-02-04 18:29:49] estimate_truncation (chain: 1): Bulk Effective Samples Size (ESS) is too low, indicating posterior means and medians may be unreliable.
#> Running the chains for more iterations may help. See
#> https://mc-stan.org/misc/warnings.html#bulk-ess - 
#> WARN [2026-02-04 18:29:49] estimate_truncation (chain: 2): Bulk Effective Samples Size (ESS) is too low, indicating posterior means and medians may be unreliable.
#> Running the chains for more iterations may help. See
#> https://mc-stan.org/misc/warnings.html#bulk-ess -
#> WARN [2026-02-04 18:29:49] estimate_truncation (chain: 1): Tail Effective Samples Size (ESS) is too low, indicating posterior variances and tail quantiles may be unreliable.
#> Running the chains for more iterations may help. See
#> https://mc-stan.org/misc/warnings.html#tail-ess - 
#> WARN [2026-02-04 18:29:49] estimate_truncation (chain: 2): Tail Effective Samples Size (ESS) is too low, indicating posterior variances and tail quantiles may be unreliable.
#> Running the chains for more iterations may help. See
#> https://mc-stan.org/misc/warnings.html#tail-ess -

# Slower reporting (e.g., deaths, 7-14 days)
fit_trunc_slow <- estimate_truncation(
  truncation_data,
  truncation = trunc_opts(
    LogNormal(
      meanlog = Normal(2, 0.5),  # ~7 day median delay
      sdlog = Normal(0.5, 0.2),
      max = 21
    )
  ),
  stan = stan
)
#> WARN [2026-02-04 18:29:50] estimate_truncation (chain: 1): The largest R-hat is NA, indicating chains have not mixed.
#> Running the chains for more iterations may help. See
#> https://mc-stan.org/misc/warnings.html#r-hat - 
#> WARN [2026-02-04 18:29:50] estimate_truncation (chain: 2): The largest R-hat is NA, indicating chains have not mixed.
#> Running the chains for more iterations may help. See
#> https://mc-stan.org/misc/warnings.html#r-hat -
#> WARN [2026-02-04 18:29:50] estimate_truncation (chain: 1): Bulk Effective Samples Size (ESS) is too low, indicating posterior means and medians may be unreliable.
#> Running the chains for more iterations may help. See
#> https://mc-stan.org/misc/warnings.html#bulk-ess - 
#> WARN [2026-02-04 18:29:50] estimate_truncation (chain: 2): Bulk Effective Samples Size (ESS) is too low, indicating posterior means and medians may be unreliable.
#> Running the chains for more iterations may help. See
#> https://mc-stan.org/misc/warnings.html#bulk-ess -
#> WARN [2026-02-04 18:29:50] estimate_truncation (chain: 1): Tail Effective Samples Size (ESS) is too low, indicating posterior variances and tail quantiles may be unreliable.
#> Running the chains for more iterations may help. See
#> https://mc-stan.org/misc/warnings.html#tail-ess - 
#> WARN [2026-02-04 18:29:50] estimate_truncation (chain: 2): Tail Effective Samples Size (ESS) is too low, indicating posterior variances and tail quantiles may be unreliable.
#> Running the chains for more iterations may help. See
#> https://mc-stan.org/misc/warnings.html#tail-ess -

5 Practical workflow for prior specification

5.1 Step 1: Start with defaults

Always begin with default priors to check the model runs and produces reasonable results.

Code
# Minimal specification
estimates <- estimate_infections(
  reported_cases,
  generation_time = gt_opts(example_generation_time),
  delays = delay_opts(example_incubation_period + reporting_delay),
  stan = stan
)
#> Warning: The largest R-hat is NA, indicating chains have not mixed.
#> Running the chains for more iterations may help. See
#> https://mc-stan.org/misc/warnings.html#r-hat
#> Warning: Bulk Effective Samples Size (ESS) is too low, indicating posterior means and medians may be unreliable.
#> Running the chains for more iterations may help. See
#> https://mc-stan.org/misc/warnings.html#bulk-ess
#> Warning: Tail Effective Samples Size (ESS) is too low, indicating posterior variances and tail quantiles may be unreliable.
#> Running the chains for more iterations may help. See
#> https://mc-stan.org/misc/warnings.html#tail-ess

5.2 Step 2: Identify candidates for modification

Ask these questions:

  1. Do I have strong domain knowledge about \(R_0\)? → Consider modifying rt_opts(prior = ...)
  2. Does \(R_t\) change rapidly or gradually in my setting? → Consider modifying gp_opts(ls = ...)
  3. Is reporting highly variable or stable? → Consider modifying obs_opts(dispersion = ...) or obs_opts(family = ...)
  4. Are there sudden policy changes? → Consider using rt_opts(rw = ...) instead of GP
  5. Is recent data truncated? → Consider adding trunc_opts(...)

5.3 Step 3: Modify one prior at a time

Make incremental changes and check sensitivity:

Code
# Modify R0 prior based on literature
estimates_r0 <- estimate_infections(
  reported_cases,
  generation_time = gt_opts(example_generation_time),
  delays = delay_opts(example_incubation_period + reporting_delay),
  rt = rt_opts(prior = LogNormal(mean = 2.5, sd = 0.5)),
  stan = stan
)
#> Warning: The largest R-hat is NA, indicating chains have not mixed.
#> Running the chains for more iterations may help. See
#> https://mc-stan.org/misc/warnings.html#r-hat
#> Warning: Bulk Effective Samples Size (ESS) is too low, indicating posterior means and medians may be unreliable.
#> Running the chains for more iterations may help. See
#> https://mc-stan.org/misc/warnings.html#bulk-ess
#> Warning: Tail Effective Samples Size (ESS) is too low, indicating posterior variances and tail quantiles may be unreliable.
#> Running the chains for more iterations may help. See
#> https://mc-stan.org/misc/warnings.html#tail-ess

# Compare results
plot(estimates)
plot of chunk workflow-step3
plot of chunk workflow-step3
Code
plot(estimates_r0)
plot of chunk workflow-step3
plot of chunk workflow-step3

5.4 Step 4: Check prior predictive distributions

It is good practice to check if your priors generate reasonable trajectories before seeing data:

Code
# You can visualise prior choices using EpiNow2's distribution objects
prior_r0 <- LogNormal(mean = 2, sd = 0.5, max = 10)
plot(prior_r0)
plot of chunk workflow-step4
plot of chunk workflow-step4

This shows the probability density of your prior. Check if this range is sensible for your application. For example, does it put reasonable mass on plausible \(R_0\) values for your pathogen?

Note: Full prior predictive checks (simulating complete epidemic trajectories) require additional Stan code not currently exposed in {EpiNow2}, but visualising individual prior distributions as shown above is a useful first step.

5.5 Step 5: Check model convergence

After modifying priors, always check that the model has converged properly. Poor prior choices can lead to convergence problems.

Code
# Check convergence diagnostics
# Extract the Stan fit object
fit <- estimates_r0$fit

# Check Rhat (should be < 1.01)
summarise_draws(fit, "rhat")
#> # A tibble: 511 × 2
#>    variable   rhat
#>    <chr>     <dbl>
#>  1 params[1] 1.05 
#>  2 params[2] 1.01 
#>  3 params[3] 1.05 
#>  4 params[4] 1.01 
#>  5 eta[1]    1.06 
#>  6 eta[2]    1.03 
#>  7 eta[3]    0.999
#>  8 eta[4]    0.982
#>  9 eta[5]    1.02 
#> 10 eta[6]    1.03 
#> # ℹ 501 more rows

# Check effective sample size (should be > 400 for reliable inference)
summarise_draws(fit, "ess_bulk", "ess_tail")
#> Warning: The ESS has been capped to avoid unstable estimates.
#> Warning: The ESS has been capped to avoid unstable estimates.
#> # A tibble: 511 × 3
#>    variable  ess_bulk ess_tail
#>    <chr>        <dbl>    <dbl>
#>  1 params[1]    102.      85.9
#>  2 params[2]    112.      75.9
#>  3 params[3]     35.4     27.1
#>  4 params[4]     60.6    113. 
#>  5 eta[1]       170.      54.6
#>  6 eta[2]       115.      77.8
#>  7 eta[3]       105.      71.9
#>  8 eta[4]       117.      81.2
#>  9 eta[5]        78.4     55.3
#> 10 eta[6]       130.     119. 
#> # ℹ 501 more rows

# You can also use the summary method which includes these diagnostics
summary(estimates_r0)
#>                         measure                estimate
#>                          <char>                  <char>
#> 1:       New infections per day     2125 (1306 -- 3337)
#> 2:   Expected change in reports       Likely decreasing
#> 3:   Effective reproduction no.      0.87 (0.67 -- 1.1)
#> 4:               Rate of growth -0.035 (-0.11 -- 0.024)
#> 5: Doubling/halving time (days)        -20 (29 -- -6.4)

Key diagnostics to check:

If you encounter convergence problems after changing priors, consider:

6 Common pitfalls and recommendations

6.1 Pitfall 1: Over-informative priors without justification

Problem: Setting very tight priors (small SD) without strong external evidence.

Solution: Use weakly informative priors unless you have literature or prior data supporting tight priors. When in doubt, make priors wider rather than tighter.

6.2 Pitfall 2: Ignoring generation time and delays

Problem: Using default gt_opts() and delay_opts(), which assume 1-day generation time and no delays.

Solution: Always specify generation time and delays from literature for your pathogen and data type.

6.3 Pitfall 3: Estimating too many uncertain parameters

Problem: Making generation time, delays, and scaling all uncertain in a short time series.

Solution: Prioritize fixed parameters from literature. Only estimate parameters that are truly unknown and have sufficient data (usually >60 days) to identify them.

6.4 Pitfall 4: Wrong time scale for length scale

Problem: Setting GP length scale to 1-2 days, expecting very flexible \(R_t\).

Solution: If you want daily flexibility, use a random walk (rt_opts(rw = 1)) rather than GP with very short length scale. GPs with length scale <7 days often struggle.

6.5 Pitfall 5: Forgetting the max parameter

Problem: Not setting max on distributions, leading to very long tails and slow computation.

Solution: Always set sensible max values for generation time and delays (e.g., max = 14 or max = 20 for most respiratory diseases).

7 References and further reading

7.1 Key papers

For background on renewal equation models: