epidist models the delay between a primary event (e.g. symptom
onset) and a secondary event (e.g. report), storing each as an
interval-censored pair of date columns: pdate_lwr/pdate_upr for the
primary event and sdate_lwr/sdate_upr for the secondary event. It comes
in two shapes: a one-row-per-case epidist_linelist_data
(epidist::as_epidist_linelist_data()) and an epidist_aggregate_data that
adds an n count column (epidist::as_epidist_aggregate_data()). epidist
stores everything in days and requires every censoring window to have a
strictly positive width.
tbl_now_from_epidist() converts either shape into a tbl_now:
"auto"(default): use the lower bounds —primary(pdate_lwr) becomesevent_date,secondary(sdate_lwr) becomesreport_date. Anepidist_aggregate_data(or any input with anncolumn) becomesdata_type = "count-incidence"withcase_count = "n"; otherwisedata_type = "linelist". Theevent_units/report_unitsare inferred from the primary censoring-window width (a 7-day window ->"weeks"), and a left-censored secondary window[origin, report]is decoded back tois_censored_report = TRUEwith the report taken fromsecondary_upper."interval": instead attach the upper boundsprimary_upper(pdate_upr) andsecondary_upper(sdate_upr) ascovariates(a warning is emitted).
tbl_now_to_epidist() performs the inverse. By default (format = "auto")
it builds an epidist_aggregate_data when x holds counts and an
epidist_linelist_data otherwise, filling all four interval columns:
the primary event spans
[event_date, event_date + w], where the windowwmatches thetbl_nowunit ("days"-> 1 day,"weeks"-> 7 days, ..., orcensoring_windowif supplied);the secondary event spans
[report_date, report_date + w]normally, but for rows flagged byis_censored_reportit is left-censored to[event_date, report_date](the report is known only to have happened at or before its report date, and cannot precede the event, i.e. epidist time 0) — encoding thetbl_nowconvention that a censored report is known only to have happened at or before its report date, so the window is[event_date, report_date].
The strata, the covariate columns and any materialised temporal-effect columns
(holidays, Fourier terms, calendar effects; see compute_temporal_effects())
are carried onto the epidist data unchanged, so the strata are available as
covariates in an epidist model formula (epidist has no separate grouping
argument).
Usage
tbl_now_from_epidist(
data,
...,
format = c("auto", "interval"),
primary = "pdate_lwr",
secondary = "sdate_lwr",
primary_upper = "pdate_upr",
secondary_upper = "sdate_upr",
verbose = TRUE
)
tbl_now_to_epidist(
x,
...,
format = c("auto", "linelist", "aggregate", "interval"),
primary_upper = NULL,
secondary_upper = NULL,
censoring_window = NULL,
obs_date = NULL,
verbose = TRUE,
quiet = FALSE
)Arguments
- data
A
data.frame,epidist_linelist_dataorepidist_aggregate_dataof epidist delay data.- ...
Forwarded to
as_tbl_now()(from) or to the relevant epidist constructor (to).- format
For
from:"auto"(default) or"interval". Forto:"auto"(default),"linelist","aggregate"or"interval".- primary, secondary
Column names of the primary / secondary event lower-bound dates. Default to epidist's
"pdate_lwr"/"sdate_lwr".- primary_upper, secondary_upper
Column names of the upper-bound dates. Default to epidist's
"pdate_upr"/"sdate_upr". Used to infer units and decode censoring (from) or, withformat = "interval", taken from covariate columns (to).- verbose
Logical. Print the choices that were made.
- x
A
tbl_nowobject.- censoring_window
(
toonly) Optional positive integer width, in days, of the censoring windows. IfNULL(default) it is derived from thetbl_nowevent_units.- obs_date
(
toonly) OptionalDateof length one (or a vector of lengthnrow(x)) to use as epidist'sobs_datecolumn. IfNULL(default) it is set toget_now(x) + censoring_windowso the right-truncation clock ends at the object's ownnow.- quiet
Logical. A different channel from
verbose:verbosecontrols the informational summary of what the conversion did, whilequietsuppresses the lossy-conversion warning emitted bytbl_now_to_epidist().
One row, one observed delay
Every row of the result is a distinct delay observation, and for the
aggregate shape n is its weight. Two things would otherwise break that, and
tbl_now_to_epidist() resolves both before building the object:
Columns the object was never told about. covid_colombia carries
sex, so an object built withoutstrata = sexhas two rows per(notification_date, diagnosis_date)cell.sexis not carried onto the epidist data, so those rows would arrive as indistinguishable duplicates; they are pooled instead, exactly astbl_now_to_baselinenowcast()andtbl_now_to_tsibble()do. Declare the column withadd_strata()to keep it as a model covariate rather than pool it away.The revision axis, which is dropped (see below) and therefore cannot keep two rows apart either. Pooling over it gives the
"total"case count: every case has exactly one outcome, so no case is counted twice.
Line lists are left alone – one row is already one case – and declared
strata, covariates, materialised temporal-effect columns and the
is_censored_report flag all keep rows apart, because all of them reach the
epidist object (the flag through the censoring windows).
Delays of zero, and the lognormal
A delay distribution with a point mass at zero cannot be fitted with a
lognormal (or a gamma, or a Weibull): all have zero density at zero. If a
large share of your cases are reported the same period they occur, the fit
does not fail loudly – it inflates the variance until the density piles up
near zero. On a daily COVID series where 57% of cases carried a delay of
exactly 0, epidist returned sigma = 17.9 and an implied mean delay of
1.5e73 days.
Check before fitting:
mean(as.numeric(x[[get_report_date(x)]] - x[[get_event_date(x)]]) == 0)If that share is large, model the delay as discrete, use a zero-inflated/hurdle form, or fit the continuous distribution to the non-zero delays and report the zero share separately.
Counts epidist cannot use
epidist_aggregate_data requires n >= 1, and so does
EpiNow2::estimate_dist() – with the identical assertion message. Count data
routinely holds rows that violate it:
zeros – an
(event, report)cell where the report added nothing, which is most cells oncecomplete_zeroes()has run, and which de-accumulating acount-cumulativeseries produces wherever a cumulative total was unchanged;negatives – a cumulative total revised downward, which de-accumulates to a negative increment.
Both are dropped before the epidist object is built. A zero contributes no
case to a delay distribution, so dropping it is lossless and is only reported
when verbose = TRUE. A negative is not a number of cases at all, so dropping
it discards the revision and warns. If nothing usable is left the
conversion aborts saying so, rather than letting epidist's own
Assertion on 'data$n' failed through.
Model choice for count data
epidist::as_epidist_marginal_model() is the model built for aggregated
counts: it works from the (delay, observation time) cells the converter
produces, so a month of cases costs a few hundred weights rather than a
few thousand rows. The latent and naive models are alternatives that expand
the counts back to one row per case.
The now and the observation window
epidist uses obs_date (an "observation stopped at" instant) to
correct for right truncation: any case with an event date near the end of
the series is under-observed, because there was less time for its report to
arrive. tbl_now_to_epidist() sets obs_date <- get_now(x) + w (the end
of the now period, widened by the censoring window w) so the truncation
clock ends at the object's own now rather than at the last reported case.
The two are usually the same on a fully-observed series and can differ when
the tail is silent or when change_now() moves now forward for a
backtest. Pass obs_date explicitly to override.
tbl_now_from_epidist() reads the same column back on the "auto" path:
now on the returned tbl_now is max(obs_date) - w, so a round trip
preserves it (up to the censoring-window widening).
Revision axis (not modelled)
epidist estimates one delay distribution – the primary-to-secondary
delay, which the converter maps to event_date -> report_date. It has no
way to represent the revision axis, so has_revision(x),
revision_type, is_censored_revision and the revision dates are
dropped from the epidist object. tbl_now_to_epidist() warns once when
it drops them, so a user who declared a revision process is told the
converter is not surfacing it. Count rows that differed only in their
revision date or outcome are then pooled, so the drop does not leave
duplicate rows behind.
See also
add and revision_delay, since epidist is about
delay distributions and a tbl_now may carry two of them;
censor_reporting_delays_above() for the long delays that would otherwise dominate a
fitted distribution;
tidy() for the fitted result.
as_tbl_now() for the generic that dispatches to the *_from_*() side;
run_nowcast(), which does the conversion for you when you fit through an
engine(). The
One dataset, many nowcasts article
fits the same data with every supported package.
Examples
## --- Linelist epidist data (one row per case) ---
ll <- suppressMessages(epidist::as_epidist_linelist_data(
data.frame(
pdate_lwr = as.Date(c("2020-03-01", "2020-03-02", "2020-03-02")),
sdate_lwr = as.Date(c("2020-03-05", "2020-03-04", "2020-03-06"))
),
pdate_lwr = "pdate_lwr", sdate_lwr = "sdate_lwr"
))
# -> a linelist tbl_now ...
nowll <- tbl_now_from_epidist(ll, verbose = FALSE)
get_data_type(nowll)
#> [1] "linelist"
# ... and back to an epidist_linelist_data
tbl_now_to_epidist(nowll, verbose = FALSE, quiet = TRUE)
#> # A tibble: 3 × 10
#> ptime_lwr ptime_upr stime_lwr stime_upr obs_time pdate_lwr pdate_upr
#> <dbl> <dbl> <dbl> <dbl> <dbl> <date> <date>
#> 1 0 1 4 5 6 2020-03-01 2020-03-02
#> 2 1 2 3 4 6 2020-03-02 2020-03-03
#> 3 1 2 5 6 6 2020-03-02 2020-03-03
#> # ℹ 3 more variables: sdate_lwr <date>, sdate_upr <date>, obs_date <date>
## --- Aggregate epidist data (counts in an `n` column) ---
agg <- suppressMessages(epidist::as_epidist_aggregate_data(
data.frame(
pdate_lwr = as.Date(c("2020-03-01", "2020-03-02")),
sdate_lwr = as.Date(c("2020-03-05", "2020-03-04")),
n = c(7, 3)
),
n = "n", pdate_lwr = "pdate_lwr", sdate_lwr = "sdate_lwr"
))
## -> a count-incidence tbl_now (case_count = "n") ...
nowagg <- tbl_now_from_epidist(agg, verbose = FALSE)
get_data_type(nowagg)
#> [1] "count-incidence"
## ... and back to an epidist_aggregate_data (auto-detected from the counts)
tbl_now_to_epidist(nowagg, verbose = FALSE, quiet = TRUE)
#> # A tibble: 2 × 11
#> ptime_lwr ptime_upr stime_lwr stime_upr obs_time pdate_lwr pdate_upr
#> <dbl> <dbl> <dbl> <dbl> <dbl> <date> <date>
#> 1 0 1 4 5 5 2020-03-01 2020-03-02
#> 2 1 2 3 4 5 2020-03-02 2020-03-03
#> # ℹ 4 more variables: sdate_lwr <date>, sdate_upr <date>, obs_date <date>,
#> # n <dbl>
