Daily surveillance data is often too sparse to nowcast: most (event date,
report date) cells hold a zero or a one, and the delay distribution is mostly
noise. The usual fix is to work in weeks instead. aggregate_time_units()
does that in one call – it moves every date onto the coarser grid, adds the
counts up, and returns a tbl_now that knows it is now weekly, so
.delay, the converters and the models all count in weeks from then on.
Usage
aggregate_time_units(
x,
to = "weeks",
axes = "all",
label = c("start", "end"),
align_on_day = 7,
type = "epi",
verbose = TRUE
)Arguments
- x
A
tbl_now()object.- to
Character. The unit to aggregate to:
"weeks"(the default),"months","years"or"days". It must be at least as coarse as the units of every axis being aggregated – there is no un-aggregating, so asking a weekly object for"days"is an error rather than a guess.- axes
Character. Which time axes to aggregate:
"all"(the default, meaning the event and report axes plus the revision axis when there is one), or any of"event","report"and"revision". The unit they are aggregated to isto.- label
Character. Which end of the period names it:
"start"(the default – the Sunday of the epi week, the first of the month) or"end"(its last day). It only changes the labels, never which rows are pooled together, but see Aggregating one axis only for when it matters.- align_on_day, type
Passed to
align_weeks()whento = "weeks": the weekday to snap to (ISO numbering, 1 = Monday, default7= Sunday) and the week convention,"epi"(default) or"iso".- verbose
Logical. Whether to report what was aggregated. Default
TRUE.
Value
A tbl_now on the coarser grid, with event_units, report_units
and revision_units updated for the axes that were aggregated, and now
moved onto the new grid.
Details
Each date is replaced by the start of the period it falls in: the Sunday
that begins its epidemiological week, the first of its month, the first of
January of its year (label = "end" names the period by its last day
instead). Weeks go through the same epi/ISO machinery align_weeks() uses,
so type and align_on_day mean exactly what they mean there.
What happens to the rows depends on the data type:
linelist– one row is still one case; only the dates move.count-incidence– rows that land in the same (event, report) cell are summed.count-cumulative– cumulative totals are not additive, so the series is de-accumulated to increments first, aggregated, and accumulated again on the new grid.
Two consequences worth knowing:
Weeks do not nest inside months. Aggregating daily data to weeks and then to months is not the same as going straight to months: the second pass sees only the week's label, so a week beginning 31 December lands in December even though most of its cases happened in January. Aggregate once, to the unit you actually want.
An
NAcount means not yet observed, so a period containing one has an unknown total, not a total that quietly leaves it out. Usecomplete_zeroes()first if theNAs are really zeroes.
What happens to the temporal effects
Any temporal-effect columns that were materialised by
compute_temporal_effects() are dropped, because a day-of-week term computed
on daily dates is meaningless once those dates are weeks.
The lazy temporal_effects() specification moves onto the new grid with
everything else, and the effects the new grid cannot express are dropped
from it rather than silently rebuilt on dates that cannot carry them:
day_of_week,weekend,day_of_month,holiday_lagsandweekend_lagsare properties of a day, so they survive onlyto = "days".week_of_yearsurvives"weeks";month_of_yearsurvives"months".seasonsare rescaled. A Fourier period is a length, not a position in the calendar, so it converts: a 365-day season becomes a 52.14-week one, andseasons = 52, season_length = 7becomesseasons = 52in weeks. A period that ends up two units or shorter is dropped – it is at or below the new grid's Nyquist limit, so it can no longer be told from a constant or from a longer wave.holidaysare kept. On a coarser grid the holiday column stops being a 0/1 indicator and becomes the share of the period's days that the calendar marks – 1/7 for a week containing Christmas Day – which is the same number as the indicator when the period is one day long.
A specification with nothing left is removed. verbose = TRUE says what was
dropped and what was rescaled.
Aggregating one axis only
axes exists because the two axes do not always move together: a system may
record the day a specimen was taken but only publish weekly report batches.
Two things follow:
tbl_now()requires the report axis to be at least as coarse as the event axis, so aggregating only the event axis of a daily object is refused. Aggregate the report axis too, or useaxes = "all".A week named by the day it starts sits before every date inside it, so coarsening a later axis alone – the report against a daily event, or the revision against a daily report – with
label = "start"produces negative delays, andvalidate_tbl_now()warns. Uselabel = "end": a report that arrived somewhere in week W is known by the end of W, which is the honest bound. When the axes move together the labelling cancels out and either choice gives the same delays.
See also
align_weeks(), which snaps weekly dates to a common weekday without
changing the units or the counts; to_count() to change data type without
touching the dates; complete_zeroes() for the cells the coarser grid still
leaves empty; tbl_now()'s units argument to declare the units up front;
temporal_effects() and compute_temporal_effects() for the specification
this coarsens.
Examples
# A sparse daily line list: one or two cases a day.
df <- data.frame(
onset = as.Date("2024-01-01") + c(0, 1, 3, 8, 9, 15),
reported = as.Date("2024-01-01") + c(2, 2, 5, 9, 12, 16),
sex = c("F", "M", "F", "M", "F", "M")
)
daily <- tbl_now(df,
event_date = onset, report_date = reported, strata = sex,
data_type = "linelist", units = "days", verbose = FALSE
)
get_event_units(daily)
#> [1] "days"
# The same cases, on a weekly grid: every date moves to the Sunday that
# starts its epidemiological week, and the delays are whole weeks.
weekly <- aggregate_time_units(daily, to = "weeks", verbose = FALSE)
get_event_units(weekly)
#> [1] "weeks"
weekly[[get_event_date(weekly)]]
#> [1] "2023-12-31" "2023-12-31" "2023-12-31" "2024-01-07" "2024-01-07"
#> [6] "2024-01-14"
weekly$.delay
#> [1] 0 0 0 0 0 0
# Count data is added up rather than merely relabelled.
counts <- to_count(daily, to = "count-incidence")
sum(counts$n)
#> [1] 6
sum(aggregate_time_units(counts, to = "months", verbose = FALSE)$n)
#> [1] 6
# A weekly grid cannot carry a day-of-week effect, so it is dropped from the
# specification; the 365-day season is rescaled to 52.14 weeks instead.
spec <- daily |>
add_temporal_effects(
temporal_effects(day_of_week = TRUE, seasons = c(7, 365))
)
get_temporal_effects(aggregate_time_units(spec, to = "weeks", verbose = FALSE))
#> [[1]]
#> [[1]]$t_effects
#> ── Temporal Effects ────────────────────────────────────────────────────────────
#> The following effects are in place:
#> • "season" periods: 52.142857
#>
#> [[1]]$date_type
#> [1] "event_date"
#>
#> [[1]]$weekend_days
#> [1] "Sat" "Sun"
#>
#>
