epidist and EpiNow2::estimate_dist() (new in EpiNow2 1.9.0) do
not produce a nowcast: they estimate a reporting-delay distribution. There
are therefore no per-event-date case estimates to tidy, and the columns
tidy.nowcast() promises (event_date, stratum, ...) would all be
meaningless. Both methods instead return the same delay-shaped table: one
row per distribution parameter, with term rather than event_date, so the
two packages' fits can be compared side by side.
Arguments
- x
A fit from
epidist::epidist()orEpiNow2::estimate_dist().- probs
Optional numeric vector of probabilities in
[0, 1], adding oneq*column each.- level
Width of the reported interval. Defaults to
0.95.- ...
Unused, for generic consistency.
- newdata
tidy.epidist_fit()only. Optional data frame passed toepidist::predict_delay_parameters(), for a fit with covariates in the delay model (formula = mu ~ 1 + gender, say).NULLuses the fit's own data.
Value
A tibble with one row per parameter of the fitted delay
distribution – whichever family was fitted, named as the fitting package
names it – plus the derived mean and sd of the delay, which are
the numbers most people actually want. The columns are:
termcharacter. The parameter: the distribution's own parameters (mu,sigma, ...) plusmeanandsd.estimatenumeric. Posterior median.conf.low,conf.highnumeric. Interval bounds, following broom's naming.levelnumeric. The width of that interval.enginecharacter."epidist"or"EpiNow2".
Everything is summarised from the posterior draws, so level is the interval
you asked for rather than whichever CrIs the fit happened to use, and
probs appends one q* column per requested probability – a real quantile
rather than an approximation.
How mean and sd are obtained
epidist reports continuous-distribution moments via
epidist::add_mean_sd().
EpiNow2 gets them without naming a distribution. It can fit five
families today and may add more, and a switch() in this package would
quietly stop reporting anything the day it does. Instead each draw's
parameters are put back into the fit's own dist_spec and discretised with
EpiNow2::discretise(), which knows the families; the moments are then a
summation over the PMF. A new family works as soon as discretise() supports
it.
The trade-off is that the EpiNow2 numbers are the moments of the discretised delay – the distribution EpiNow2 convolves with downstream. Against the closed forms the mean is exact and the sd runs about 1% high, that being the variance a discrete grid adds. Expect a difference of that order when comparing the two packages' fits.
A name collision worth knowing about
summary() on an estimate_dist fit has mean and sd columns, and
those are the posterior mean and sd of the parameter on that row – not of
the delay. The mean and sd these methods report are rows, and are the
delay distribution's own moments. Same words, different quantities.
Dispatch
epidist() returns an object of class c("brmsfit", "epidist_fit"), in that
order, so if broom.mixed is loaded its tidy.brmsfit() method matches
first and you get raw brms parameters instead of this table. Call
tidy.epidist_fit(fit) explicitly when you want the delay distribution and
cannot be sure which method will win.
See also
tbl_now_to_epidist() and tbl_now_to_EpiNow2() for the conversions;
tidy() for tidying a case-count nowcast rather than a delay
distribution; revision_delay for the delay these are estimating.
Examples
data(denguedat)
# A short window: fitting a delay distribution does not need twenty years of
# data, and Stan is slow.
recent <- subset(denguedat, onset_week >= as.Date("2010-06-01"))
nowobj <- tbl_now(recent,
event_date = "onset_week", report_date = "report_week", verbose = FALSE
)
# `target = "estimate_dist"` gives the censored linelist EpiNow2 wants: one
# row per case, each date as the interval it is known to fall in.
delays <- tbl_now_to_EpiNow2(nowobj,
target = "estimate_dist", verbose = FALSE, quiet = TRUE
)
head(delays)
#> pdate_lwr pdate_upr sdate_lwr sdate_upr obs_date
#> 1 2010-06-07 2010-06-14 2010-06-14 2010-06-21 2010-12-27
#> 2 2010-06-07 2010-06-14 2010-06-14 2010-06-21 2010-12-27
#> 3 2010-06-07 2010-06-14 2010-06-14 2010-06-21 2010-12-27
#> 4 2010-06-07 2010-06-14 2010-06-14 2010-06-21 2010-12-27
#> 5 2010-06-07 2010-06-14 2010-06-14 2010-06-21 2010-12-27
#> 6 2010-06-07 2010-06-14 2010-06-14 2010-06-21 2010-12-27
# A short chain keeps the example quick -- use EpiNow2's defaults for real
## work. `try()` guards the case where EpiNow2 is installed but its Stan
# toolchain is not.
fit <- try(
EpiNow2::estimate_dist(
delays,
stan = EpiNow2::stan_opts(samples = 100, chains = 1)
),
silent = TRUE
)
#> WARN [2026-09-11 10:38:54] estimate_dist (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-09-11 10:38:54] estimate_dist (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 -
# One row per fitted parameter, plus the delay's own mean and sd.
if (!inherits(fit, "try-error")) {
print(tidy(fit))
print(tidy(fit, probs = c(0.05, 0.95)))
}
#> # A tibble: 4 × 6
#> term estimate conf.low conf.high level engine
#> <chr> <dbl> <dbl> <dbl> <dbl> <chr>
#> 1 meanlog 2.33 2.31 2.34 0.95 EpiNow2
#> 2 sdlog 0.367 0.357 0.377 0.95 EpiNow2
#> 3 mean 11.0 10.8 11.1 0.95 EpiNow2
#> 4 sd 4.20 4.06 4.33 0.95 EpiNow2
#> # A tibble: 4 × 8
#> term estimate conf.low conf.high level engine q5 q95
#> <chr> <dbl> <dbl> <dbl> <dbl> <chr> <dbl> <dbl>
#> 1 meanlog 2.33 2.31 2.34 0.95 EpiNow2 2.32 2.34
#> 2 sdlog 0.367 0.357 0.377 0.95 EpiNow2 0.358 0.376
#> 3 mean 11.0 10.8 11.1 0.95 EpiNow2 10.9 11.1
#> 4 sd 4.20 4.06 4.33 0.95 EpiNow2 4.08 4.31
