tbl.now 1.0.0

example_engine() no longer emits duplicate prediction rows

get_latest_reported_cases() answers by the censoring flag as well as by the event date and the strata, so a cell holding both an exactly-dated and a censored arrival comes back as two rows – and answers by a declared covariate the same way. example_engine() passed those rows straight through, so its predictions repeated the (event date, stratum, quantile level) key that a tbl_nowcast is built on.

Nothing complained at the time. It surfaced much later and much further away: tidy() died with an unreadable recycling error (Size 116: Existing data. Size 118: Column estimate.) and autoplot() with is.finite(x): default method not implemented for type 'list', because pivot_wider() had folded the repeated key into a list-column.

The engine now pools the counts down to the event date and the stratum, which is the only key its result has. This is the same summarise(.by = key) step the Adding your own nowcasting model article already shows; the counts themselves are unchanged, and so is every other engine.

A trailing comma no longer aborts nowcast_ensemble() or nowcast_backtest()

Both take their members through ..., collected with list() – and list() turns a trailing comma into argument is missing, with no default, an error naming neither the argument nor the call. Every verb accepts one, so there was no reason for a reader to expect otherwise. They now collect with rlang::list2(), which tolerates the trailing comma (and accepts !!! splicing). An empty argument in the middle of a call is still refused, and now says which one it was.

engine_surveillance() works on Sunday-start weeks

surveillance::nowcast() refuses a now, a when or a control$dRange that is not the first day of one of its epochs – a Monday for weekly data. An epidemiological week starts on a Sunday, so weekly tbl_now objects built the ordinary way died with The variables 'now' and 'when' needs to be at the first of each epoch.

get_surveillance_when() and get_surveillance_range() now snap their grids to the epoch start, which is what surveillance::linelist2sts() already does to the data, and run_nowcast() shifts the estimates back onto the object’s own weekday on the way out – so the predictions are still indexed by the event dates you supplied. Event dates that sit on no common weekday are refused up front, naming align_weeks(), rather than after a fit.

Breaking: case_autocorrelation() and reporting_completeness() are gone

Both were written by an AI and never reviewed by a human. They were taken out of summary() earlier for that reason and kept exported behind a warning on every call; shipping an unverified statistic in a CRAN release is a different proposition, so they are now removed from the package entirely and parked, tests and all, in devel/unreviewed_summaries.R. There is no deprecation shim.

The autocorrelation and completeness components no longer exist anywhere, and nowcast_summary_components documents the ten remaining blocks. Nothing else in the family changes.

The Get Started vignette is now a five-minute tour

vignette("tbl.now") used to be a 1,500-line reference that opened with the tidyverse and ended with format converters. It is now a five-minute walk of the whole workflow – clean, tbl_now(), diagnose(), summary(), autoplot(), and then the fork between tbl_now_to_*() and run_nowcast()/nowcast_backtest() – drawn as a diagram and run on the same dengue data as the README, with every stop linking to the article that covers it properly.

Everything that was cut moved, unchanged, to a new article: More on the tbl_now object (https://rodrigozepeda.github.io/tbl.now/articles/more-on-tbl-now.html). It keeps the attribute reference (now complete: revision_date, revision_type and revision_units were missing), the three data types, censoring, temporal effects, the revision process, the dplyr methods and the utilities. The plotting and diagnostics sections were dropped in favour of the articles that already cover them.

The website’s articles are now grouped into Tutorials, Diagnosing and visualizing and Miscellaneous rather than listed flat.

?tbl_now_workflows is gone; the vignette says the same thing with an example you can run.

Smaller installation

LazyDataCompression: xz plus the shorter vignette take the installed package from 5.1 Mb to 3.4 Mb, comfortably back under CRAN’s threshold. The datasets themselves are unchanged.

list_nowcast_methods() no longer loads the modelling packages

installed_only = TRUE asked requireNamespace() whether each backend was available, which loads the namespace to find out. Listing the methods therefore pulled , and the rest into the session – several seconds of Stan machinery, and six namespaces attached as a side effect of a question about the library path. It now asks system.file(package = ), which is the cheap form of the same question. The only behavioural difference is that a package which is installed but cannot be loaded is now listed; run_nowcast() still says so, clearly, when you try to use it.

Upstream examples now guard converter and fit equivalence

The shipped examples from epinowcast, NobBS, surveillance, EpiNow2 and epidist now exercise both halves of each integration: the package-native input is compared with the matching tbl_now_to_*() result, and a seeded native fit is compared with the native fit retained by run_nowcast() (or, for epidist, with the same delay-model pipeline, since epidist is not a nowcasting engine).

autoplot() is now one column per process

The gallery used to flow left to right in two columns, so which process a panel belonged to was something you read off its colour. It is now a matrix: the epidemic process is the first column, the reporting process the second, and the revision process – on an object that declares one – the third, with each row asking the same question of every process. A two-date object is therefore two columns wide and a three-date object three, and a row a process cannot answer (weekly data has no day-of-week panel) leaves that cell empty rather than sliding the next panel into the wrong column.

The number of columns follows the selection, because the columns are the processes: panels = "calendar" or panels = "delay_calendar" now comes back as a single column rather than a two-wide flow.

(breaking)

plot_revision_delay() drew the report-to-resolution delay as boxplots split by outcome, and was the one delay picture that was not a panel of autoplot().

Delays and arrivals can be split by revision outcome

plot_delay_distribution() and plot_reporting_process() gain by_revision_type (default TRUE). It splits the panel by how each case eventually resolved – confirmed, pending, retracted and unknown – so “do negative results come back faster than positive ones?” and “was that spike a day of retractions?” are visible rather than inferred.

as_forecast_point() is no longer exported (breaking)

as_forecast_point() is a generic, and this package supplied both a method for it and an exported function of the same name. The exported copy masked the generic: after library(scoringutils); library(tbl.now) a bare as_forecast_point() reached tbl.now’s version, which is not a generic and aborts on anything that is not a tbl_nowcast or a nowcast_backtest(). as_forecast_quantile() and as_forecast_sample() were never exported this way, so the three were also inconsistent with each other.

diagnose() stops reporting the revision axis as a defect

Declaring a revision axis made diagnose() complain about the two things that axis exists to record. A pending case has no revision date yet, and that NA was read as a value.

The delay-distribution converters return one row per observed delay

tbl_now_to_epidist() and tbl_now_to_EpiNow2(target = "estimate_dist") are the two converters that keep the is_censored_report flag, and they were also the two that never pooled. Neither carries an undeclared column or the revision axis onto its result, so rows separated only by those arrived as duplicates that nothing downstream could tell apart: covid_colombia (which carries an undeclared sex) came back from tbl_now_to_epidist() as 3456 rows covering 1898 distinct delays.

plot_scalogram() is removed

The wavelet scalogram was shipped as very experimental and never earned its keep: it was never confirmed to identify batches reliably, and the two packages it needed to do so – and – were carried in Suggests for that one plot alone.

Most of the package is now stable

The lifecycle badges have been promoted: every exported topic now reads stable, except the surfaces still being worked on — update(), summary(<tbl_now>), diagnose(), diagnose_batches(), diagnose_batches2(), simulate_batch(), transport_discriminant(), autoplot() (both methods), tbl_now_to_EpiNow2() and tbl_now_to_epidist(), which keep experimental.

No behaviour changed; this is a statement about which signatures are now expected to hold still.

autoplot(<tbl_nowcast>) gains date_lim and ylim

A nowcast covers the whole series but only corrects its final periods, so almost every call was followed by a coord_cartesian() to zoom on to them.

Two fixes the revisions article turned up

A revisions walk-through

Site

complete_zeroes() keeps temporal effects computed

complete_zeroes() adds rows, and materialised [temporal_effects()] columns are a per-row property, so the new cells joined in as NA while computed_temporal_effect_cols went on claiming the columns were computed. The function announced this with a cli_alert_warning() and left the caller to repair it.

Reported downstream in diseasenowcasting#126, where every count-cumulative fit calls complete_zeroes() and so emitted the alert.

rowwise() demotes cleanly

rowwise() stays deliberately unimplemented for tbl_now – but it now says so properly and gets out of the way properly.

Implementing rowwise() properly was considered and rejected: it would need a second parallel rowwise_tbl_now class carrying the same dozen dplyr methods grouped_tbl_now already needs, and dplyr::group_vars() reports the rowwise variables while dplyr::is_grouped_df() is FALSE – so every function here that reads group_vars() to pick an aggregation key (to_count(), complete_zeroes(), censor_delays(), align_weeks(), …) would silently read a rowwise variable as a stratifying one.

Sharper epidist integration

Findings from the epidist audit against epidist 0.4.1:

Sharper epinowcast integration

Findings from the epinowcast audit against epinowcast 0.7.0:

Native strata dispatch on baselinenowcast

baselinenowcast >= 0.2.1 accepts a long tidy data.frame with a strata_cols argument and returns a single baselinenowcast_df with the strata columns still attached. engine_baselinenowcast() uses that path for stratified objects instead of looping one triangle at a time, which enables sharing estimates across strata and drops an internal wrapper class.

tbl.now 0.35.2

Fixed a bug in the is_weekend() function that made it work solely on an English locale.

tbl.now 0.35.1

One help page for the two delay-distribution tidy() methods

tidy.epidist_fit() and tidy.estimate_dist() now share a single help page, ?tidy.delay_distribution. Both methods return the same delay-shaped table, and documenting them apart duplicated the Value, How mean and sd are obtained and name-collision sections. Nothing about either method’s behaviour changed, and both names still work as topic aliases.

Smaller installed package

The installed size drops from 5.4 Mb to 4.5 Mb, below the 5 Mb R CMD check threshold:

Dataset documentation lives in R/data.R

The roxygen blocks for covid_colombia and hai_bucaramanga moved out of R/data-covid_colombia.R and R/data-hai_bucaramanga.R into R/data.R, which is now the single file documenting every shipped dataset. The rendered help pages are unchanged.

tbl.now 0.35.0

diseasenowcasting engine follows the revision/cumulative API

The diseasenowcasting engine documentation and tests now expect automatic model selection to live in diseasenowcasting::nowcast(). tbl.now passes the tbl_now and engine arguments through without injecting model components.

For count-cumulative data, diseasenowcasting consumes signed changes in the cumulative trajectory and selects its cumulative model unless the caller supplies an explicit model(cumulative = cumulative_process(...)).

tbl.now 0.34.0

Documentation clarifies scoring, covariates and article endings

The documentation now states that scoring and backtests use the resolved truth defined by truth_axis and truth_type, rather than an ambiguous “eventual” quantity. It also makes the covariate as-of rule explicit: covariates used in a fit or backtest snapshot should be values available at that snapshot’s now.

The introductory and custom-model vignettes now end with the shared learning-more fragment, and the ensemble article places its References header before that final fragment.

as_forecast_point() exposes scoringutils point forecasts

as_forecast_point() now converts a tbl_nowcast, ensemble or nowcast_backtest() to a scoringutils point forecast using the median prediction. The old as_scoringutils() quantile-frame helper is now internal; use scoringutils::as_forecast_quantile() directly for quantile scoring.

Reporting hexamap labels are more robust

plot_reporting_hexamap() now keeps its custom event, report and delay labels outside the plotted lattice with range-aware spacing, so axis titles are less likely to overlap tick labels or data marks when article or user figure sizes change.

Revision-axis hexamaps now use revision_units for their arrival grid instead of always using report_units.

Ensemble validation is stricter

nowcast_ensemble() now rejects non-finite fixed weights, duplicated or unknown weight names, duplicate prediction/draw keys inside a member, and invalid n_draws values before doing ensemble arithmetic. These inputs previously led to low-level errors, silently ignored weights, or ensemble predictions whose values were NA.

The ensemble documentation now states that members are assumed to target the same reporting or revision quantity, and that the result should be scored with the matching truth_axis and truth_type.

Autoplot can show revision-date calendar effects

The calendar-effect helpers now accept type = "revision", and autoplot() includes revision calendar and cycle panels whenever a tbl_now has a revision process. Revision panels use the new revision palette role, #C79800 by default.

Temporal effects can target revision dates

add_temporal_effects() now accepts date_type = "revision_date" for revision-aware tbl_now objects. compute_temporal_effects() materialises those specs as .revision_* columns using the revision date and .revision_num. Revision-date changers now preserve lazy temporal-effect specs while invalidating computed columns, and remove_revision_date() drops revision-date specs while keeping event/report specs.

Temporal-effect columns are inrevised when date columns change

change_event_date() and change_report_date() now drop already materialised temporal-effect columns and clear their tracking attribute. The lazy temporal_effects() specification is preserved, so later calls to compute_temporal_effects() or converters that carry temporal effects rebuild fresh columns from the current event/report date declarations.

complete_zeroes() preserves missing counts and completes coarser grids

complete_zeroes() now preserves explicit NA counts in the input instead of rewriting them as observed zeroes. Generated incidence cells are still filled with zero, and generated cumulative cells carry forward the previous known cumulative total.

complete_zeroes() now reconstructs generated report dates through the shared delay helper, so monthly and yearly count grids are completed through now instead of silently returning only the originally observed periods.

Backtesting and scoring are stricter about fairness

score_nowcast() and as_scoringutils() now warn when predictions contain targets outside the supplied truth grid. Missing observed rows inside the tbl_now truth grid are scored as zero, because tbl_now objects need not store zero-count cells; targets outside that grid are still omitted because there is no observed value to score.

score_nowcast(), as_scoringutils() and nowcast_backtest() gained explicit truth_axis and truth_type arguments. The default remains reported totals (truth_axis = "report", truth_type = "total"), and revision-aware truth can now be scored with truth_axis = "revision".

nowcast_backtest() now validates explicit now_dates: they must be non-missing Dates within the observed surveillance window and cannot be after the object’s now. A now_date equal to the object’s now is allowed with a warning, because today’s data may still be incomplete.

nowcast_weights() gained now and include_now arguments. When nowcast_ensemble() derives performance weights from a backtest, rows at the members’ own now dates are excluded by default so weights are trained on a holdout window unless include_now = TRUE is set explicitly.

Constructors and dplyr rebuilds are stricter

mutate() and $<- now rebuild generated numeric date columns when event, report, or revision dates are edited, including .revision_num and .revision_delay for revision-aware objects.

Demotion after renaming protected generated columns now drops all tbl_now attributes, and count-data revision now errors when the declared count column is not numeric. Empty tbl_now() inputs remain unsupported, and one-row inputs must declare their units.

EpiNow2 converter and engine checks are stricter

tbl_now_to_EpiNow2() now validates accumulate, complete, and snapshots before building the target object. accumulate and complete must be TRUE, FALSE, or "auto"; snapshots must be NULL or a single positive whole number.

The EpiNow2 converter now warns when lazy temporal-effect specs are dropped by the date/confirm series targets. engine_epinow2() maps the supported report-date day_of_week temporal effect to EpiNow2’s native observation model with EpiNow2::obs_opts(week_effect = TRUE) when the caller has not supplied obs; temporal effects with no EpiNow2 equivalent warn before being dropped.

The ensemble-nowcasting article now tolerates older cached display data that lacks member_predictions, falling back to the cached baseline member predictions so the comparison plot still renders.

Nowcasts and backtests convert directly to scoringutils (#20, #69)

as_scoringutils() now accepts a nowcast_backtest and reuses the truth already stored in it. The result keeps now as a forecast unit, so the same target predicted at different retrospective dates remains distinct.

scoringutils::as_forecast_quantile() now accepts a tbl_nowcast directly, including the result of run_nowcast() and nowcast_ensemble(), as well as a nowcast_backtest. A full tbl_now can be supplied as truth; a backtest uses its stored truth by default.

scoringutils::as_forecast_sample() now does the same for a tbl_nowcast that retains posterior draws and for a draw-based ensemble. Quantile ensembles are refused because samples cannot be recovered from reported quantiles. A nowcast_backtest() can opt into retaining draws with keep_draws = TRUE; its sample coercion checks that every successful engine/date fit supplied them, rather than silently dropping quantile-only members.

Nested ensembles are now covered explicitly: a quantile ensemble can be a member of another quantile ensemble, and a linear-pool ensemble can be a member of another linear pool when it carries draws.

tidy() on a backtest now returns the predictions, not only the truth (#70)

tidy() on a nowcast_backtest reported what was observed and how each method scored against it, but not what the method had actually said – so the one table you would plot a backtest from was missing half the comparison, and recovering it meant reshaping the object’s long $predictions by hand.

It now carries estimate, conf.low, conf.high and level, named as [tidy()] names them everywhere else: estimate is the 0.5 quantile and level the width of the widest symmetric pair of quantile levels actually present. nowcast_backtest() refuses engines whose quantile_levels disagree, so level is one number for the whole table. Where the levels cannot support a bound the column is NA rather than a guess, exactly as on a tbl_nowcast.

The full column order is now method, now, event_date, stratum, observed, estimate, conf.low, conf.high, level, wis, ae_median, coverage_50, coverage_90. Nothing was removed or renamed and no rows were added – (method, now, stratum, event_date) is still the unique key – so code that selects columns by name is unaffected; code that assumed the frame had exactly nine columns is not.

diseasenowcasting is installed from GitHub, and the error now says so (#72)

Asking for engine_diseasenowcasting() without the package installed suggested install.packages("diseasenowcasting", repos = c(options('repos'), epinowcast = ...)), which cannot work: the package is on GitHub and in no CRAN-style repository. The error now gives the instruction that does work:

install.packages("pak")
pak::pkg_install("RodrigoZepeda/diseasenowcasting")

The internal helper behind these messages takes a character vector now, one bullet per line, and interpolates the instruction rather than pasting it into the cli template – an install call carrying a brace (a git ref, say) used to be read as a glue expression and abort while formatting the abort.

tbl.now 0.33.0

aggregate_time_units() now coarsens the temporal-effect specification (#65)

Aggregating dropped the materialised temporal-effect columns but kept the lazy temporal_effects() spec untouched, so the next compute_temporal_effects() rebuilt a day-of-week column on dates that are all the same weekday. The spec now moves onto the new grid with the dates:

A specification left with nothing in it is removed. verbose = TRUE reports what was dropped and what was rescaled.

add_temporal_effects() (the data.frame method) gained a units argument for this; compute_temporal_effects() reads it off the object.

Breaking: the palette is named by ROLE, not by hue

Every colour in the package now comes from the exported tbl_now_palette(), and every element of it is named for the role it plays rather than for the colour it happens to be. The old names encoded the hue (primary_green, accent_red, near_black), which made the palette impossible to re-theme: somebody handing in a blue-and-orange palette had to call their orange primary_green for the plots to find it.

was is
accent_red reporting
light_red reporting_light
primary_green epidemic
light_green epidemic_light
medium_green epidemic_mid
dark_green epidemic_dark
near_black ink
muted_green ink_muted

The red/green grammar is unchanged – red is still the reporting process and green still the epidemic one. What changed is that the names now say that instead of naming the pigment.

There is no deprecation shim. A palette argument carrying the old names is rejected with an error listing the roles it is missing.

The palette also grew the colours that used to be written into the plotting code as literals ("grey60", "white", "#C4D5DE"): ink_inverse, surface, surface_muted, surface_dark, grid_major, grid_minor, guide, guide_strong, annotation, neutral, zero, pending and observed. A re-theme now reaches every mark on the page, and no plotting function contains a hex code.

tbl_now_palette() fills every role from its own defaults, so a partial override is still a complete palette:

plot_reporting_triangle(x, palette = tbl_now_palette(reporting = "#5B4B8A"))

New: plot_weekend_effects(), and the day-type panels are normalized only

plot_weekend_effects() is plot_holiday_effects() for the common case of wanting to see the weekend without first writing the specification down. It attaches temporal_effects(weekend = TRUE) when the object does not already ask for a weekend effect – on a copy, so your object is unchanged – and then draws the same day-type panel. A holiday calendar that is already attached still contributes its Holiday box, and a weekend_days argument sets the weekend definition when this is the call that has to attach it. It needs daily data, and says so rather than drawing a single box:

plot_weekend_effects(daily_now)                  # the weekend, in one call
plot_weekend_effects(daily_now, type = "report") # ... and in the delay
plot_weekend_effects(weekly_now)
#> Error: A weekend effect needs daily data.
#> x `event_units` is "weeks".

Breaking: the four holiday panels – "calendar_holiday", "calendar_holiday_lag" and their delay_* twins – are now always drawn measure = "normalized", and measure is gone from the signatures of plot_holiday_effects() and plot_holiday_lag_effects() (it stays on plot_day_of_week_effects(), plot_week_of_year_effects() and plot_month_of_year_effects()). autoplot() keeps the argument and simply ignores it for those four panels.

A percentage share only means something when the groups are comparable slices of the block it is taken over. A weekend is two days in seven, so “29% of the cases at the weekend” is exactly average and reads as low; the same goes for “1 working day after a holiday”, which is however many days the calendar happens to put there. Normalizing against the mean asks the question the panel is for – is this day type unusual? – and 1 is the answer for “no”.

The day-type panels are also retitled “Weekend and/or holiday effects” (and “Weekend and/or holiday delay effects” for the reporting twin), because that is what their categories are: the panel has never been only about holidays. The holiday-lag panels keep their own titles.

Every plot takes size and linewidth

Point, label and line sizes were hard-coded, so a figure drawn large had marks too small to see. Outside plot_reporting_hexamap() they are multipliers defaulting to 1, which leaves every existing figure unchanged and, unlike an absolute size, preserves a panel’s own hierarchy: plot_transport_discriminant(x, size = 2) doubles both the unflagged points (1.1) and the confirmed batches (2.6) rather than flattening them to one value.

Where a function draws a reference grid of its own – not ’s panel grid, which these plots switch off – that grid has its own absolute argument:

plot_reporting_process(), plot_epidemic_process(), plot_scalogram() and plot_revision_status() draw only bars, tiles or areas, so they take neither, and say so in their documentation rather than offering an argument that would do nothing.

plot_revision_delay() and plot_revision_status() also gained the palette argument they had been missing – they used to call the default palette internally – and now use the shared package theme instead of a bare ggplot2::theme_minimal().

Breaking: the hexamap draws points, not hexagons

plot_reporting_hexamap() marks each (event, delay) cell with a point at the centre of the hexagon it used to fill. A hexagon is drawn in data units: it tiles at any zoom, and that is exactly why it cannot be made bigger – there is no room. A point is drawn in millimetres, so size is a free knob, which is what the plot needed. The projection, the triangular grid and the axes are unchanged; shape = 15 gives squares, which tile the lattice closely.

Because a point is sized in millimetres and the lattice in data units, no default size can suit every combination of cell count and figure size. That is the argument’s reason for existing: raise it until the marks nearly touch at the size you are actually drawing.

Code reading the built plot changes with it – the marks are one row per cell in a geom_point layer rather than six polygon vertices per hexagon.

A batch screen you have subset stops claiming to be one

diagnose_batches() and transport_discriminant() return tibbles with a print() method that summarises the screen. Taking a few columns out of one – the obvious way to look at a result –

flagged <- screened[screened$batch, ]
flagged[order(flagged$p_transport_bh), c("report_date", "reported", "baseline")]

kept the class, so auto-print went looking for the batch column it needed and aborted inside the print method, which is the hardest place to read an error:

Error: Can't subset rows with `!is.na(x$batch) & x$batch`.
x Logical subscript must be size 1 or 4, not 0.

Both classes now demote to a plain tibble when a subset drops a column their summary reads, taking the screen’s own attributes (lookback, alpha, …) with them – the same rule as a tbl_now losing a protected column, silently rather than with a warning, since what is lost here is a print format and not the ability to nowcast. [, dplyr::select() and dplyr::mutate() all follow it; row subsetting, head() and dplyr::filter() keep every column and so keep the screen. As a backstop, both print() methods now fall back to printing the table when a column has gone missing some other way (x$batch <- NULL), rather than erroring.

diagnose_batches() and transport_discriminant() print properly again

Both classes have a print() method, and neither was reached. They were registered with a plain @export, which puts the method in the package’s own methods table; the namespace defines an S7 print generic that shadows base::print once tbl.now is attached, so auto-printing either object fell through to the default and showed a bare tibble instead of the batch screen or the discriminant summary. Both are now registered with @exportS3Method base::print, as every other print() method in the package already was, and there is a regression test asserting auto-print dispatch for each.

Plot backgrounds match the site in dark mode

On the pkgdown site every figure was a black rectangle on a charcoal page. The cause was not in the R code: pkgdown ships one PNG for both themes and relies on a bslib CSS filter, invert(100%) hue-rotate(180deg), which takes a ggplot2 background from white to #000000 while the body is #212529.

pkgdown/extra.css now overrides that filter with brightness(0.871) invert(100%) hue-rotate(180deg) plus mix-blend-mode: lighten. The brightness() before the invert maps white to #212121 while still taking black to white, so no contrast is lost; the blend closes the remaining few units to the page’s slightly blue #212529 and, since nothing in the filtered image is darker than #212121, touches nothing else. The page background is unchanged, and light mode is untouched.

Breaking: diagnose_batch_shape() is now diagnose_batches2()

The shape test is the second half of one question – diagnose_batches() asks how many records arrived on a date, diagnose_batches2() asks which event dates they came from – and the old name read as an unrelated function. It is a straight rename with no deprecation shim: the function is experimental and warns on every call.

Breaking: summary() no longer reports autocorrelation or completeness

case_autocorrelation() and reporting_completeness() were written by an AI and have not been reviewed by a human. They were part of summary(), so every reader of a summary got two numbers nobody had checked, with nothing in the output saying so.

Both are still exported, and both now warn on every call – deliberately not throttled, unlike the experimental diagnostics, because the caveat belongs to the number rather than to the session that produced it. summary() loses its lags, completeness_delays and mature_only arguments, which only ever fed those two blocks; the autocorrelation and completeness components are gone from its output.

summary(x)                        # no longer contains those two components
case_autocorrelation(x, lags = 1) # still there, and says what it is
reporting_completeness(x)

The batch family ignores censored arrival dates

The batch testsdiagnose_batches(), diagnose_batches2() and transport_discriminant() – now drop the rows flagged censored on the axis they are scanning: is_censored_report for axis = "report", is_censored_revision for axis = "revision".

Only those three. The flag is a statement about the arrival axis and about nothing else, so a row censored on the report axis is still a case that happened on its event date: every plot, including plot_epidemic_process() and the diagnostic_plot() panels other than transport, keeps every row.

A censored date is a bound, not the date the record arrived. Censoring is usually applied because something was already known about those dates, and the censored rows all carry the same bound, so leaving them in piles them onto one date and the detector rediscovers, as a finding, the artefact it was told about. Pass drop_censored = FALSE to scan them anyway.

diagnose_batches2() no longer errors on a date with no arrivals

A line list cannot represent a zero, so a report date on which nothing arrived has no rows at all. That is the observation “no arrivals”, not a missing date, and the test now reports n_at = 0 for it instead of aborting. A date off the object’s report grid – where there is nothing to compare against – is still an error, and the message now says the grid’s step.

plot_transport_discriminant() is usable as a plotly widget

summary() says what n and total count

One shared schema means the two count columns mean different things in different blocks, and printing them side by side left the reader guessing. Each block now prints a one-line gloss: total is always cases, and n is the block’s own unit – dates on the grid for cases, runs for zero_run, (event, report) cells for delay and composition.

complete_zeroes() works when a date is missing (#66)

A single NA report date made every bound of the grid NA: max_delay came out NA and seq(0, NA) aborted with 'to' must be a finite number, which says nothing about the missing date that caused it. The only workaround was to censor_reports() first, which is a real answer but not the only one a user might want.

Every bound is now computed ignoring the missing dates. A row whose event or report date is NA has no cell on the rectangle, so it takes no part in the grid – but it is still a case, and it is carried through unchanged rather than deleted by the closing report_date <= bound filter (NA <= bound is NA, which dplyr::filter() drops). Only an object in which every row is missing one of the two dates is refused, with a message saying so.

Two things fixed alongside it:

Breaking (behaviour): a stratified baselinenowcast fit completes its grid (#67)

run_nowcast(x, engine_baselinenowcast()) returned a different nowcast for a line list than for the same object passed through to_count() |> complete_zeroes() – with the same seed. The stratified path built its triangles from tbl_now_to_baselinenowcast(format = "long"), which is a tidy data frame with no grid and so is deliberately never completed. A line list has no row at all for an event period in which nothing was reported, so the reference axis silently stopped short: 54 reference times where the completed counts gave 81.

nowcast_fit.baselinenowcast() now asks for format = "triangle_list", which is the format that exists for exactly this – one triangle per stratum. It completes the grid, restores the not-yet-observed cells to NA, and absorbs negative increments, none of which the hand-rolled split did. Stratified fits on line-list input will change, and they now agree with the count path.

Because the triangle really does drop declared covariates, a stratified fit on an object carrying them now warns that it did; the long format used to carry them into a frame the fit then ignored.

tbl_now_to_EpiNow2() completes a line list’s grid (#67, audit)

Found by auditing every converter for the defect behind #67. A line list has no row for an event period in which nothing was reported, and .epinow2_series_data() built its date/confirm series from the rows it was handed: on daily data the series stopped at the last period carrying a report rather than at the object’s [get_now()], which is the period the nowcast is about. estimate_truncation() was worse – .epinow2_snapshots() completes each snapshot with complete_zeroes(), which refuses a line list, and the surrounding tryCatch() swallowed the refusal and kept the short snapshot, though ?estimate_truncation asks for “a complete vector of dates”.

tbl_now_to_EpiNow2() gains a complete argument with the same contract as tbl_now_to_baselinenowcast()’s: "auto" (the default) completes line-list input only, because count data can say “observed zero” itself and filling those cells would claim reporting was complete when it was not. TRUE / FALSE force either behaviour. All three series targets (estimate_infections, regional_epinow, estimate_truncation) now reach the now.

The rest of the audit came back clean, and is now pinned by tests: epinowcast completes through enw_complete_dates(), NobBS and surveillance are handed the now by their fit methods (NobBS(now =), get_surveillance_range()), and epidist fits a delay distribution with no event grid at all. tests/testthat/test-engines-linelist-equivalence.R asserts that every engine returns the same nowcast, under the same seed, from a line list and from the equivalent to_count() |> complete_zeroes() object.

tbl.now 0.32.0

Breaking: diagnose() no longer signposts the statistical tests

diagnose_signposts() is removed, along with the "signposts" check, and the not_run status is gone from the findings schema – status is now error > warning > note > ok > skipped.

diagnose() still runs no statistical test, and for the same reason: drift and batching are statements about a distribution, and answering them means choosing a method, a window and a multiplicity correction. What changed is that it no longer spends four rows of every report saying so. Call the tests yourself when you want them:

diagnose_drift(x, axis = "report")
diagnose_batches(x, axis = "report")

They are listed under @seealso on diagnose(), which is where a pointer belongs.

diagnose() findings

Documentation

tbl.now 0.31.0

Breaking: the *_confirmed() counters are gone, replaced by a revised-cases family (#64)

get_latest_confirmed(), get_net_confirmed(), get_initial_confirmed() and get_nth_confirmed() are removed. They answered a version of the question get_latest_reported_cases() already answered, in a different return shape (a plain tibble), with a delay measured from a different anchor – so the two families could not be read against each other.

In their place, the reporting getters have an exact twin on the revision axis:

get_initial_revised_cases(x)                     # as of the FIRST result back
get_latest_revised_cases(x)                      # everything settled so far
get_nth_revised_cases(x, delay = 7)              # settled within 7 periods
get_latest_revised_cases(x, type = "confirmed")  # was get_latest_confirmed()
get_latest_revised_cases(x, type = "net")        # was get_net_confirmed()
get_latest_revised_cases(x, type = "by_type")    # every outcome, side by side

The reported-cases getters respect a grouping; to_count() says it does not (#61)

get_latest_reported_cases(), get_initial_reported_cases() and get_nth_reported_cases() (and the three new revised ones) now keep the caller’s grouping and answer by it: the grouping columns join the event date and the strata as keys, and come back on the result.

tn |> dplyr::group_by(hospital) |> get_latest_reported_cases()

This is the only way to ask for a count by a covariate – a column that matters without being something you nowcast by. These verbs can do it because they select a point in the process rather than reshaping the object.

to_count() cannot, and now warns rather than dropping the grouping in silence: after aggregating, one row is an (event, report) cell rather than one of the rows that were grouped, so the grouping describes nothing that is left. Declare the column with add_strata() or add_covariates() to keep it out of the sum.

is_tbl_now() is a class check again, not a revision run (#62)

is_tbl_now() used to call validate_tbl_now() inside a tryCatch() that caught errors but not warnings, so the object’s findings escaped from wherever the predicate was called – which is every .assert_tbl_now() in the package. A verb that fixed a problem warned about it twice, after the fix.

It is now a structural check: the class, the attributes a tbl_now cannot do without, and the columns those attributes name. Cheap, and silent.

Fractional delays are refused where they are created, and reported where they are found (#63)

A calendar has no half-days, so a fractional delay had to become something. It became round() – round-half-to-even, so 2.5 went down and 3.5 went up, silently – while the numeric axis refused the same value outright.

tbl.now 0.30.0

New: coarsen the time grid in one call (#56)

aggregate_time_units() moves a tbl_now onto a bigger time unit – daily to weekly, weekly to monthly, monthly to yearly – and updates the object so that .delay, the converters and the models all count in the new unit:

hai <- hai_bucaramanga |>
  tbl_now(event_date = specimen_date, report_date = report_date,
          strata = sex, data_type = "linelist", units = "days")

hai |> aggregate_time_units(to = "weeks")

New: censor by condition, and replace the date (#57)

censor_reports() and censor_reporting_delays() take a filter()-style condition and record the matching rows as bounds rather than measurements – optionally replacing the date at the same time. This is the fix for the two dates that are not really dates: the missing one, and the sentinel far in the future.

hai |> censor_reports(is.na(report_date), to_report = Sys.Date())
hai |> censor_reports(report_date == as.Date("2222-02-22"), to_report = Sys.Date())
tn  |> censor_reporting_delays(.delay > 60, to_delay = 60)

The censoring family is now six verbs, two axes by three ways to select

by date by delay threshold
reporting (is_censored_report) censor_reports() censor_reporting_delays() censor_reporting_delays_above()
revision (is_censored_revision) censor_revisions() censor_revision_delays() censor_revision_delays_above()

"pending" cases are skipped when a revision date would be written, with a warning saying how many. A pending case is reported and still waiting, so it has no resolution date; writing one would assert a resolution that never happened and make the case look resolved to everything counting arrivals on that axis. Set revision_type to "confirmed" or "retracted" first if the case really was resolved. Flagging without a replacement is unaffected – no date is written, so nothing is contradicted.

New: one units argument instead of three (#58)

tbl_now() gains units, the shared default for event_units, report_units and revision_units:

tbl_now(hai_bucaramanga, event_date = specimen_date, report_date = report_date,
        strata = sex, data_type = "linelist", units = "days")

Anything given explicitly still wins, so units = "days", report_units = "weeks" reads a daily event date against a weekly report date, and an explicit event_units = "auto" still means infer.

Fixes

tbl.now 0.29.0

Breaking: is_censored is now is_censored_report (#54)

There are two censoring axes now, so the unqualified name had to go. The old spelling is removed outright, not deprecated:

was is
tbl_now(is_censored = ) tbl_now(is_censored_report = )
get_is_censored() get_is_censored_report()
add_is_censored(), change_is_censored(), remove_is_censored() add_is_censored_report(), change_is_censored_report(), remove_is_censored_report()
is_censored attribute is_censored_report attribute
.is_censored (the column censor_reporting_delays_above() creates) .is_censored_report

New: is_censored_revision, the revision-axis censoring flag (#53)

The twin of is_censored_report, for models that use censored revision delays. It marks rows whose time from report to resolution is a bound rather than a measurement.

Breaking: censor_revision_delays_above() flags instead of erasing

It used to set the offending rows’ revision_type to "pending" and delete their revision date. That was wrong: a case confirmed after 200 days is still a confirmed case, and the object should say so. It now sets is_censored_revision and leaves the date and the outcome alone, exactly as censor_reporting_delays_above() does on the report axis. get_latest_confirmed() therefore still counts those cases.

New: revision_levels, for data not recorded in English (#54)

revision_type may hold only "confirmed", "retracted", "pending" or NA – that was already enforced, and the error now names the way out. tbl_now(revision_levels = ) is that way out: a named dictionary whose names are the labels in your data and whose values are the canonical four.

tbl_now(casos,
  revision_type   = desenlace,
  revision_levels = c(
    confirmado = "confirmed", retractado = "retracted", pendiente = "pending"
  ),
  ...
)

The column is rewritten to the canonical values; the dictionary is kept on the object and read back with get_revision_levels(). A dictionary that would recode a canonical value into a different one is refused, because it would flip the column on every rebuild.

Fixed: change_now() re-censors instead of erroring (#51)

Moving now backwards is what change_now() is for – it is how a backtest walks through time. On an object carrying a revision process it aborted for every now earlier than the last revision, which is nearly every historical as-of date.

It now masks revisions dated after the new now: the revision date becomes NA and the outcome returns to "pending", because a resolution that has not happened yet is not a resolution. change_now() and update_now() gain verbose to silence the report of how many rows were masked.

covid_us carries a revision process (#52)

No shipped dataset had one, so every example fabricated an outcome by row position. covid_us is rebuilt from the same CDC source with the two date columns that were being left on the floor, and it now runs onset -> positive specimen -> registration at CDC:

was is
cdc_case_earliest_dt, cdc_report_dt, n (2020-2021) onset_dt, pos_spec_dt, cdc_report_dt, current_status, sex, n (2020)

cdc_case_earliest_dt is CDC-derived and equals onset_dt for 99.997% of the rows kept, so it is gone as redundant; sex is a stratum, and current_status is the revision outcome – in CDC’s own words, so that mapping it is a worked example of revision_levels. The relationship between outcome and revision delay is real rather than fabricated: probable cases are registered a median of 2 days after the specimen, laboratory-confirmed ones 4 days. CDC does not withdraw cases, so "retracted" does not occur.

tbl.now 0.28.0

Breaking: the confirmation process is now the revision process

The optional third date a tbl_now can carry is called a revision rather than a confirmation, throughout. The old spelling is gone, not deprecated – it had not shipped.

was is
add_confirmation(), change_confirmation(), remove_confirmation() add_revision_date(), change_revision_date(), remove_revision_date()
get_confirmation_date(), get_confirmation_type(), get_confirmation_units(), has_confirmation() get_revision_date(), get_revision_type(), get_revision_units(), has_revision()
confirmation_counts, confirmation_delay revision_counts, revision_delay
censor_confirmation_delays_above(), diagnose_confirmation_delay() censor_revision_delays_above(), diagnose_revision_delay()
plot_confirmation_delay(), plot_confirmation_status(), prop_confirmation_type() plot_revision_delay(), plot_revision_status(), prop_revision_type()
confirmation_date, confirmation_type, confirmation_units arguments revision_date, revision_type, revision_units
.confirmation_num, .confirmation_delay columns .revision_num, .revision_delay
axis = "confirmation" axis = "revision"
"event_to_confirmation", "report_to_confirmation" "event_to_revision", "report_to_revision"

The outcome values are unchanged: a case is still "confirmed", "retracted" or "pending". Revision is what the process does; confirmed is one of the things it can conclude.

diseasenowcasting now uses revision_process() for row-level report resolution and cumulative_process() for count-cumulative signed changes.

Documentation: fewer, fuller reference pages

Fixed: baselinenowcast on a snapshot (“as of”) series

A snapshot stream restates the whole history in every snapshot, so its delay axis is as long as the series itself and the reporting triangle comes out square. baselinenowcast needs more reference dates than delay columns – it spends max_delay of them estimating the delay distribution and keeps two back for the uncertainty model – so it refused, with a message about reference-time arithmetic that mentioned neither the delay axis nor anything to do about it. Three of the six shipped datasets are that shape.

Note that a snapshot series must be declared data_type = "count-cumulative". infer_data_type() reads a single downward revision as incidence, by design, and a revised running total has them; left to the inference, every delay carries a whole period’s count instead of an increment and nothing downstream can tell.

New: diagnose() and summary() print as reports

Both still return the tibbles they always returned, and every dplyr verb still works on them. What changed is what you see when you print one.

New: a nowcast prints its value at the now edge

print() on a tbl_nowcast now leads with the number it was fitted to produce – the estimate and interval at the last event date it covers, one line per stratum – before the quantile table, which starts at the oldest event date.

tbl.now 0.27.0

Breaking: a nowcast is specified with an engine()

run_nowcast() and nowcast_backtest() used to take a method name plus a ... (and, for the backtest, a method_args list of lists keyed by label). Both failed the same silent way: an argument that missed its backend simply vanished, and you got a fitted model at its default with nothing on the object to say so.

An engine is one modelling package plus every argument it needs:

run_nowcast(x, engine_nobbs(max_D = 10, moving_window = 64))

nowcast_backtest(x,
  engine_baselinenowcast(draws = 1000),
  engine_nobbs(max_D = 10),
  now_dates = dates, seed = 20260824
)

New: min_date, per engine

Every engine takes min_date, saying how much history to fit on:

min_date means
NULL (default) the whole series
a Date keep event dates on or after it
a number keep the last n periods before now, in the object’s own units

It is per engine on purpose. baselinenowcast and diseasenowcasting take a long series in their stride, while epinowcast and EpiNow2 scale with the number of reference dates and are best given a window – so one global filter() over all of them was the wrong tool.

Prefer the number inside a nowcast_backtest(): now moves between fits, so a fixed calendar cut makes the fitted window grow as the backtest walks forward and the last fit is trained on more data than the first.

min_date trims the event axis, not now, and the trimmed object is what the result carries – so score_nowcast() and autoplot()’s reported counts describe the series the model was actually shown.

Breaking: score_nowcast() / as_scoringutils() take a tbl_now as truth

observed_col is removed, and a plain data frame is no longer accepted. The tbl_now already knows which column holds the observed counts – it is get_case_count(), or the count to_count() produces from a line list – so naming it was a burden on the caller and the old default (“the last column that is neither the event date nor a stratum”) was a guess that could mis-score silently.

score_nowcast(nc, truth = dengue)     # the FULL tbl_now, line list or counts
as_scoringutils(nc, truth = dengue)

Breaking: argument names made consistent

A documentation audit read every exported function and found the same argument wearing different names in different places. 116 of the 148 exports already took x first; these were the exceptions.

Breaking: align_weeks() numbers weekdays the ISO way

align_weeks(align_on_day = ) counted weekdays from Sunday while is_weekday(weekend_days = ) counted them from Monday. align_weeks() now uses ISO numbering too – 1 = Monday … 7 = Sunday – so the two agree. is_weekday() is unchanged.

The default is unchanged. It becomes 7, which is still Sunday, so align_weeks(x) – and tbl_now(..., align_weeks = TRUE), which is where nearly everyone meets it – behaves exactly as before. Only an explicit align_on_day changes meaning, and the migration is to subtract one, wrapping 1 to 7:

you wrote you meant now write
1 Sunday 7
2 Monday 1
3 Tuesday 2
7 Saturday 6

New: example_engine(), a toy engine for examples

Every real engine needs its modelling package, so every example that fitted a nowcast sat inside \donttest{} behind a requireNamespace() guard – and none of them ran on a default check. example_engine() needs nothing, is deterministic, and returns in milliseconds, so the examples for run_nowcast(), nowcast_backtest(), nowcast_weights(), score_nowcast() and tidy() on a backtest now show real output.

It is not a nowcasting method. It ignores the reporting delay entirely – reporting the counts that have arrived and putting a spread-wide band around them – so it under-predicts recent dates by construction. That is useful to see and useless to rely on; the examples say so. Its source is also the shortest complete nowcast_fit() / nowcast_tidy() pair in the package, if you are writing a backend.

New: tbl_now() warns on misspelled argument names

tbl_now() keeps unmatched ... names as user metadata, which meant a typo in a real argument name was accepted in silence. case_col = "n" set a useless attribute and left count data typed as a line list – as it had been doing in one of this package’s own examples.

Names close enough to a real argument to be a typo now warn and name the intended one. Deliberate metadata (data_source, citation, population) stays silent: a match needs a shared first letter and an edit distance under a third of the longer name, which is what keeps source from being read as a misspelling of force.

autoplot() on a nowcast draws the reported counts as columns

The cases reported so far were points floating in the middle of the fan, which reads as a second estimate. They are now grey columns under it, so they read as a count measured from zero and the correction the nowcast applies is the visible gap between the top of a bar and the band. The bars are one period wide, taken from get_event_units().

EpiNow2 keeps its draws

nowcast_tidy.EpiNow2() now reads the posterior samples with EpiNow2::get_predictions(format = "sample") instead of the fit’s lower_<pct>/upper_<pct> summary. Before, EpiNow2 could report only a median and the two tails of whatever CrIs it happened to be fitted with – three levels – so quantile_levels could not be honoured, tidy(probs =) was an error, and it could not join a type = "linear_pool" ensemble. It now does all three. The summary path remains as a fallback for a fit get_predictions() cannot read.

This has a visible knock-on: an ensemble containing EpiNow2 now shares all nine of nowcast_quantile_levels() rather than collapsing to three.

Performance: tbl_now() and every dplyr verb on one

No behaviour changed, but the class got substantially cheaper. tbl_now() is about 3x faster and validate_tbl_now() – which runs on every dplyr verb via tbl_now_reconstruct() – about 4x.

Almost all of the cost was building findings that were then discarded. validate_tbl_now() reports at floor = "note", so on a clean object it formatted eleven cli messages and showed one; formatting is the expensive part (a hint interpolating a vector of row numbers costs ~15 ms), and each finding also built its own one-row tibble (~2 ms).

diagnose() returns exactly the same tibble, and validate_tbl_now() the same conditions.

Documentation

Every reference page was read once, function by function, for an audience of public-health practitioners first and statisticians second.

tbl.now 0.26.0

One surveillance line list per stratum

tbl_now_to_surveillance() gains format = "linelist_list", which returns one line list per stratum as a tbl_now_surveillance_list instead of one frame with a pasted strata column. surveillance::nowcast() has no strata argument, so a stratified analysis is one fit per stratum, and the split no longer has to be done by hand:

pieces <- tbl_now_to_surveillance(x, format = "linelist_list")
lapply(pieces, function(piece) surveillance::nowcast(data = piece, ...))

It mirrors tbl_now_to_baselinenowcast(format = "triangle_list") throughout: the result is a plain list, so lapply(), [[ and friends work unchanged; it is length one and named "all" when the object declares no strata, so the return type never depends on whether strata happen to be attached; it prints what it is; and as_tbl_now() binds it back into a tbl_now, restoring the original date-column names, the strata and the covariates. Count input comes back as a "linelist" – one row per case, totals unchanged – because that is what a surveillance line list holds.

format = "linelist" remains the default and is unchanged.

Documentation

tbl.now 0.25.0

A vignette on writing your own back-end

vignette("custom-nowcast-models") is the full account of the nowcast_fit() / nowcast_tidy() contract: what a method may assume about the tbl_now it is handed (get the column names from the getters, work on .event_num/.delay rather than the calendar, run the grid to get_now(), remember that a line list cannot hold a zero), how to reuse the tbl_now_to_*() converters and as_tbl_now() instead of reshaping by hand, and what shipping a back-end in a package involves.

The worked example is a delay-ratio nowcast: for each delay it takes the median of the factor by which past mature event dates grew from that delay to their eventual total, and applies the empirical quantiles of that factor to the counts reported so far. It needs no modelling package, so the article runs every line of its own code – including the scoring, the backtest and the ensemble – and it is written twice, once returning predictions and once returning draws, to show both branches of the contract.

vignette("ensemble-nowcasting")’s section 4 now points here instead of carrying its own smaller version of the same material.

Bug fixes

Documentation

tbl.now 0.24.0

diagnose(): a structural health check

summary() describes a tbl_now; diagnose() looks for what is wrong with it. One row is one finding, sorted worst first, and the offending row indices come with it:

findings <- diagnose(dengue_now)
findings |> dplyr::filter(status <= "note")

bad <- findings |> dplyr::filter(check == "ordering")
dengue_now[bad$rows[[1]], ]

Ten checks: declarations (attribute types, the columns they name, role collisions, columns the object was never told about, temporal effects added but never materialised), ordering (event <= report <= confirmation, including the transitive leg that a missing report_date would otherwise hide), missing, duplicates, units, negatives, now, truncation, strata and signposts. Each is also an exported function of its own – see ?nowcast_diagnose_components – and diagnose(x) is exactly the dplyr::bind_rows() of them.

status is an ordered factor, worst first, which is why the tibble sorts itself and why status <= "note" reads as “anything worth acting on”: error > warning > note > ok > not_run > skipped.

Four decisions worth knowing about:

validate_tbl_now() is the same engine, presented as conditions

validate_tbl_now() no longer has a check list of its own. It calls the findings engine and re-emits the result as the cli conditions it has always emitted: it aborts on the errors and warns about the warnings. One implementation, two presentations.

What that changes for you:

Breaking: the statistical tests take the diagnose_ prefix

The five tests are named for what they are for rather than for the fact that they are tests. The old names are gone, not deprecated:

was is now
test_delay_drift() diagnose_drift()
test_delay_changepoint() diagnose_changepoint()
test_confirmation_delay() diagnose_confirmation_delay()
batch_test() diagnose_batches()
batch_shape_test() diagnose_batch_shape()

The S3 class batch_test, and with it print.batch_test(), is renamed to diagnose_batches to match.

Documentation and website

summary() and diagnose() are now documented where people actually meet the package:

Fixed: the light/dark switch never rendered

template: light-switch: true was set and lightswitch.js was being loaded, but the site had no toggle. The control is a navbar component, and _pkgdown.yml named an explicit navbar: structure: right: that replaced pkgdown’s default [search, github, lightswitch] without listing it. The script loaded, the button did not exist, and nothing errored. lightswitch is now listed explicitly.

tbl.now 0.23.0

summary() describes the object the way a nowcaster reads it

summary() on a tbl_now now returns a tibble rather than the column-by-column listing summary.data.frame() produces, which said nothing about the structure the class exists to carry. One row is one statistic of one quantity of one stratum:

summary(dengue_now) |> dplyr::filter(component == "delay")

It covers the case counts on each of the object’s time axes (event, report and, where there is one, confirmation), the delay distributions between them, the lengths of the runs of zero dates, the compositional shares (censored, per confirmation outcome, per stratum, per categorical covariate level), the lag-1 autocorrelation of each series, the reporting-completeness curve, the totals, the date ranges and now, and how full the reporting triangle is.

Three decisions worth knowing about:

Every block of the summary is its own function

summary() is exactly the bind_rows() of these, and each returns the same schema, so they stack:

cases_per_date(), delay_summary(), zero_run_summary(), prop_censored(), prop_confirmation_type(), prop_strata(), prop_covariate_levels(), case_autocorrelation(), date_ranges(), triangle_occupancy(), reporting_completeness() and cumulative_growth().

delay_summary() names the three delays explicitly – "event_to_report", "event_to_confirmation" and "report_to_confirmation" – because the first two are measured from the event and the last is the laboratory’s own turnaround, measured from the report, and confusing them is a documented hazard.

Internal

One date-grid helper replaces three inlined copies of the same seq(from, to, by = <units>) logic, including the one in complete_zeroes() that only knew about days and weeks.

tbl.now 0.22.0

The back-ends that stratify by ONE column

NobBS::NobBS.strat() takes a single strata column name, EpiNow2::regional_epinow() a single region, and surveillance::nowcast() takes no strata argument at all. A tbl_now may declare several stratifying columns, and their interaction – “nowcast each observed combination separately” – is exactly one stratum to those back-ends. The converters now build that column, so there is an argument to write:

Previously tbl_now_to_nobbs() handed back the strata as ordinary columns and nothing else, so there was no way to call NobBS.strat() on a multiply stratified object at all. run_nowcast(x, "NobBS") had its own copy of the pasting logic; it now uses the converter’s column, so the two cannot disagree.

tidy() also learned the last per-stratum shape it did not know: a list of stsNC fits, which is what split()-ing a surveillance line list and looping produces.

tidy() returns the quantiles a NobBS fit was asked for

NobBS keeps no draws, so tidy(fit, probs = ...) refused every probs outright. But NobBS(specs = list(quantiles = c(0.1, 0.5, 0.9))) computes those levels at fit time and puts them in estimates – reading them back is a lookup, not an approximation, and refusing it made the documented workflow (“ask at fit time, then request them with probs”) impossible to complete.

tidy() now returns them. A level the fit was not asked for still aborts, because that one really is unrecoverable, and the message now names the missing levels and the specs = list(quantiles = ...) call that would have produced them.

The two date grids surveillance::nowcast() needs

Both read the step off the object’s own event units and abort on a "numeric" grid rather than anchoring integer indices at the 1970 epoch. dRange matters more than it looks: left to itself nowcast() infers the axis from the line list it was handed, and a line list cannot express a zero – the quiet days at the now edge have no rows, so the inferred axis stops short of exactly the days being nowcast.

The article now runs the code it shows

vignettes/articles/nowcasting-models.Rmd displayed cached results next to code that a separate script, data-raw/nowcast_comparison.R, kept its own copy of. The two drifted, invisibly, because the article never ran what it printed.

data-raw/nowcast_models_precompute.R replaces it: it knitr::purl()s the article, runs the article’s own chunks with the fits live, and reads the displayed objects back out by name. The code that produced every number is now literally the code printed above it. Renaming an object in the article stops the script with a list of what is missing instead of quietly saving a shorter file.

Fixed along the way, all of it drift the old arrangement hid:

tbl.now 0.21.0

The confirmation process

A tbl_now can now carry a third date. Influenza is the picture to keep in mind: symptoms begin (the event), the patient visits a doctor (the report), and days later a swab comes back positive (the confirmation) or negative (a retraction – reported, but not a case after all). The assumed timeline is event <= report <= confirmation <= now.

Counting when cases can be undone

get_latest_confirmed(), get_net_confirmed() (confirmed minus retracted), get_nth_confirmed(x, delay) and get_initial_confirmed() – the confirmation mirrors of the report-axis getters. censor_confirmation_delays_above() returns implausibly long confirmations to "pending", which is what they really were.

Diagnostics on the confirmation axis

A laboratory clearing a backlog looks exactly like a surveillance system clearing its inbox, so rather than duplicate every diagnostic, they take an axis = c("report", "confirmation") argument: batch_test(), batch_screen(), batch_shape_test(), transport_discriminant(), plot_reporting_process(), plot_epidemic_process(), plot_reporting_triangle(), plot_delay_profiles(), plot_reporting_hexamap(), plot_scalogram(), plot_delay_drift(), test_delay_drift(), test_delay_changepoint() and diagnostic_plot().

On the confirmation axis, delays are still measured from the event, so the two axes are directly comparable and the gap between them is the time the laboratory adds. Cases still "pending" are excluded – counting them would invent an arrival on a date they do not have.

New in their own right: plot_confirmation_status() (the confirmed / retracted / pending shares over time), and test_confirmation_delay() / plot_confirmation_delay(), which ask whether retractions come back faster than confirmations – a laboratory that rules cases out sooner than it confirms them biases any nowcast that treats the two alike.

Other changes

tbl.now 0.20.0

Bugs found by the new engine test suite

Every one of these was found by writing the tests, not before:

Covariates and censoring are no longer dropped in silence

nowcast_truth() removed

Dropped entirely rather than kept internal. It was get_latest_reported_cases() reshaped. score_nowcast() and as_scoringutils() take the tbl_now itself as truth.

covidat removed

covid_us is kept: it is the only shipped dataset that actually exhibits backlog dumps, which vignette("batch-reporting") is about. Measured against a 15-day rolling baseline, covid_us has 21 report days above 2x and 5 above 3x; covid_colombia has one above 2x and none above 3x.

New tests

All skip_on_cran(), all on synthetic fixtures built by tests/testthat/helper-engines.R rather than on shipped data, so one axis can be varied at a time:

Articles

DEVELOPMENT_SKILL

tbl.now 0.19.0

Converters no longer make you aggregate first

covid_colombia carries sex. An object built without strata = sex therefore has two rows per (notification_date, diagnosis_date) cell, and a reporting triangle, a tsibble key and an epinowcast observation table each have exactly one slot per cell. Until now that meant tbl_now_to_baselinenowcast() aborted (“duplicate reference_date and report_date combinations”) and tbl_now_to_tsibble() aborted (“a valid tsibble must have distinct rows”), and you had to group_by() |> summarise() before converting.

Both now pool undeclared columns for you, as tbl_now_to_nobbs(), tbl_now_to_surveillance(), tbl_now_to_EpiNow2(), tbl_now_to_epinowcast() and tbl_now_to_data_table() already did. The pooling is to_count(), so case totals are preserved exactly, and it is reported under verbose = TRUE:

i `tbl_now_to_baselinenowcast()`: pooled over 1 undeclared column ("sex");
  18195 rows -> 10129.
i Declare it with `add_strata()` to nowcast it separately.

Line lists are left alone: one row is already one case there, and collapsing would destroy the individual records the target package is being handed.

The non-uniqueness warning now names the culprit

It used to say “Consider using to_count() to aggregate the data or distinct() to remove repeated observations”. The distinct() half is wrong whenever the cause is an undeclared column – those rows are distinct, they differ in sex – so it sends you in a circle, and on data with genuine repeats it silently deletes cases. The warning now inspects the object and says which:

tbl_now_to_baselinenowcast(max_delay = )

A cap on the delay axis, counted exactly as tbl_now_to_epinowcast() counts it – max_delay = 30 keeps delays 0 to 29, giving a 30-column triangle – so the same number means the same triangle in both. NULL (default) keeps every delay, which is the previous behaviour. This replaces the filter(.delay <= 30) |> idiom the docs used to recommend.

nowcast_truth() is now internal

It was get_latest_reported_cases() with the class stripped, undeclared columns summed away and the count renamed .observed – the values were identical. A second public name for that is a second thing to learn for no gain.

score_nowcast() and as_scoringutils() now accept the tbl_now itself as truth and do the reshaping internally, which is shorter than what it replaces:

score_nowcast(nowcast, truth = dengue)          # was: truth = nowcast_truth(dengue)

A data frame of observed counts still works, as does NULL.

?run_nowcast says what the models actually are

Three new sections, because “it calls the package with its defaults” is not enough to read the output:

And a section on how each engine’s default model is specified, with the two that most need saying out loud:

Article fixes

tbl.now 0.18.0

New: one call per model, and ensembles

Until now tbl.now prepared data for six nowcasting packages and normalised what they returned, but running several of them still meant six different calls and six different result shapes to reconcile by hand. This release adds the layer that removes that bookkeeping.

New: tidy() for a nowcast and for a backtest

tidy() already worked on every raw engine fit. It now also works on what run_nowcast() and nowcast_ensemble() return, which is the way round it should always have been.

New: reproducible backtests

nowcast_backtest() gains a seed argument. When given, the RNG is seeded immediately before each fit, from the seed and the method and date that fit is for. One set.seed() before the whole backtest only pins anything if every method draws the same random numbers in the same order – which stops being true the moment a method is dropped or one date is refitted. This is the same lesson data-raw/nowcast_comparison.R already records.

nowcast_weights(type = "optim") now falls back to equal weights, with a warning, when the optimiser does not converge on a usable point. It used to return NA weights, which do not fail until much later inside nowcast_ensemble(), as an all-NA nowcast that reads like a modelling problem rather than an optimisation one.

Removed: the nowcaster backend

nowcaster was dropped in 0.16.0 along with its converters, for the reasons recorded there. The run_nowcast() backend for it is not shipped: it called tbl_now_to_nowcaster() and get_nowcaster_strata(), which no longer exist. Neither nowcaster nor INLA is reintroduced to DESCRIPTION.

Other

tbl.now 0.17.0

New: support

tbl_now_to_EpiNow2() and tbl_now_from_EpiNow2(), against EpiNow2 1.9.0 (now the minimum in Suggests). EpiNow2 takes four different input shapes, one per entry point, so target names the function the result is passed to and it can be handed over unchanged:

Three things worth knowing:

obs_date and the censoring windows are different quantities, and the converter now treats them as such. [sdate_lwr, sdate_upr) brackets when the report happened – at weekly resolution [W, W + 7), a half-open interval whose upper bound is the end of that week, not a claim that anything happened on day W + 7. obs_date is when observation stopped, which estimate_dist() asserts is >= sdate_upr on every row. A tbl_now’s now labels a period, so the instant observation stopped is the end of it: obs_date = now + w. That makes the assertion hold by construction, and nothing is observed after it. Clamping the windows at now instead was tried and rejected – it moves reports in the final period into an earlier one, which the epidist round-trip test caught.

The nowcasting-models article now covers across all three strata, with its results precomputed into nowcast-comparison.rds like every other engine. Two caveats are stated in the article itself: the delay distributions are ’s shipped examples rather than distributions fitted to the Colombian data, and sampling is lighter than the default (500 draws, 250 warmup, 2 chains) because it is much the slowest engine in the comparison.

data-raw/nowcast_comparison.R now takes engine names (Rscript data-raw/nowcast_comparison.R EpiNow2) and merges them into the existing file, leaving every other engine’s rows and recorded timings alone; with no arguments it rebuilds everything as before. This replaces a second script that re-created the setup by parsing the first one.

Two correctness fixes came out of that. Every engine is now seeded per (engine, stratum) immediately before its fit, rather than relying on a single set.seed() at the top of the script – which only pins results if every engine consumes the same random numbers in the same order, and so does not survive refitting a subset. Refitting baselinenowcast alone had been silently changing its estimates, and one EpiNow2 fit produced a stratum whose upper credible bound sat at 1e8 for all 181 days and would not reproduce. Both now refit to max abs diff == 0. The script also refuses to cache any fit whose scale exceeds 100x the observed maximum for its stratum, since an unconverged Stan or INLA fit returns numbers rather than an error.

tidy() gained methods for estimate_infections, epinow, estimate_truncation and estimate_dist, plus a regional_epinow branch in tidy.list() giving one block per region.

tidy.estimate_dist() reports the fitted distribution’s mean and sd alongside its parameters, so its output is directly comparable with tidy.epidist_fit(). They are derived from the distribution, not from the family’s algebra: each draw’s parameters go back into the fit’s own dist_spec and through [EpiNow2::discretise()], which knows the families, and the moments follow by summation over the PMF. Nothing in this package names a distribution, so a family adds later works as soon as discretise() supports it. Against the closed forms the mean is exact and the sd runs about 1% high – the variance a discrete grid adds – so expect a difference of that order against , which reports continuous-distribution moments.

It also honours probs and takes a level argument, matching tidy.epidist_fit(). (An earlier draft rejected probs with a message claiming the engine keeps no draws. It does: summary.estimate_dist() reads them.)

tbl_now_to_EpiNow2(target = "estimate_dist") warns when it pools strata – estimate_dist() has no grouping argument, so it fits one distribution to everything – and warns when a large share of delays are exactly zero, since a lognormal has zero density there and will inflate its variance rather than fail. The message points at the families that do have positive density at zero ("exp", or "gamma"/"weibull" with shape below 1) rather than at a constant shift, which would silently bias every parameter.

Two more points of care:

.epidist_drop_unusable_counts() is now .drop_unusable_counts() and shared: EpiNow2::estimate_dist() asserts n >= 1 with the identical message epidist uses, so the same filter applies to both.

Audit of the converters and tidy() against the target packages’ own docs

Every claim the converters and tidy() methods make about diseasenowcasting, baselinenowcast, epinowcast, epidist, NobBS, surveillance, tsibble and data.table was re-checked against those packages’ installed help pages and source. Five defects came out of it, all of them cases where the code was silently plausible rather than wrong-looking.

The remaining findings were addressed too:

Behaviour changes

Tests

tbl.now 0.15.0

tbl.now 0.14.1

tbl.now 0.14.0

tbl.now 0.13.1

tbl.now 0.13.0

tbl.now 0.12.0

Batch detection, rebuilt around a conservation law

The report-batch detectors were rebuilt on a single, exact principle: a batch moves reports along the report axis without creating them, so a window of report dates spanning both the lull and the release has an unchanged total, whereas a genuine epidemic surge inflates it. The previous heuristic detect_report_batches() / plot_report_batches() (multi-signal robust-z, and the model-based conditional scan) are removed and replaced by three model-free, r lifecycle::badge("experimental") functions. Each derives its mathematics in a “The mathematics” section of its help page.

tbl.now 0.10.1

tbl.now 0.10.0

tbl.now 0.9.0

tbl.now 0.8.0

tbl.now 0.7.5

tbl.now 0.7.3

tbl.now 0.7.0

tbl.now 0.6.4

tbl.now 0.6.3

tbl.now 0.6.2

tbl.now 0.6.1

tbl.now 0.6.0