Version 2026.10.23
- An NA count in the identity family of
rolling_slope_matrix(), and so of the ensembleshort_term_trend(), gives NA only in the windows that hold its week. Before, the cumulative sums carried it into every later window, also those of each later series in the same ensemble. The other two families were already local. -
short_term_trend_v4()with one draw callsstats::qt()once per distinct degrees of freedom, not once per row. The growth-rate percentiles are identical. - In the identity family, an
Infor-Infcount counts as NA, local to its windows. Before, it made every later window NaN. - The rebuild of a compressed v4 ensemble gives each base measure its own u, from a seed from its name and seed 4. Before, every measure of a row shared one u, so a numerator and its denominator were perfectly rank-correlated, while
nowcast_ecdf_v4()draws them independently. The test triangle of test-v4_nowcast_ecdf.R has 9 nowcast weeks. There, the interquartile range of the rebuilt rate was 0.017 to 0.13 times that of the full rate. It is now 0.91 to 1.13 times. - The rebuild of a compressed v4 ensemble interpolates between the stored percentiles on the
log1p()scale. It does so in each row whose 9 percentiles are all 0 or more. Before, a straight line on the original scale put too many draws in the upper tail of a right-skewed row. - On a luftvei replay of 1,272 newest weeks, HLM P(high) from rebuilt draws was within 0.05 of the full draws in 95.0%, against 92.3% before. The mean signed error fell from 0.0074 to 0.0054. At older horizons the agreement stayed 100%.
- The rebuilt draws of two measures are now independent, also where the full draws were dependent.
vignette("v4")says when to read a stored rate and when to add it again from its parts.
Version 2026.10.22
- The identity family of the ensemble
short_term_trend()takesdenominatorandper, asens_add_rate()does. With adenominator, the residual variance has a floor at the Poisson variance of the rate,mean(rate) * per / mean(denominator). Without one, there is no floor, as before. For a count, pass a denominator of 1 andper = 1. -
rolling_slope_matrix()takes the same floor asrate_denominatorandper. A denominator MUST be 0 or more and finite, or NA. A 0 or NA week affects only the windows that hold it. Before, values on an exact line always got a standard error of 0, and P(increasing) of exactly 0 or 1. - On the percent-positive rate of
vignette("pipeline"), which now passes its denominator, the floor set the standard error in 62% of the window-draws. It raised the median standard error 1.18 times. -
signal_detection_hlm()on acsfmt_ensemble_v3, andsignal_detection_hlm_v4(), flag a draw above the limit as high, as the v1 method does. Before, a draw at the limit was high. In 2026.10.18, this rule alone separated v4 from v1 in 965 NorSySS weeks with a rate and a limit of 0. -
signal_detection_hlm_v4()stops when a series is not one row per ISO week, asshort_term_trend_v4()does. Its baseline counts back 52 rows a year, so a missing week shifted the baseline without a warning. -
vignette("csalert"), the README and the home page mention the v4 functions. The vignette heading “Use the ensemble methods” is now “Which methods to use”, because it covers the table methods too. -
Config/roxygen2/versionis 8.1.1, the roxygen2 that writes the help pages.roxygen2::roxygenise()moved it from 8.0.0 in 2026.10.16, and the package keeps it.
Version 2026.10.19
-
mem_thresholds_v4()writes the post-epidemic threshold, the season start and the season length, as<measure>_mem_thr_postepidemic,<measure>_mem_season_startand<measure>_mem_season_weeks. These are the fields that norsyss.cs9 uses from its v1 MEM fit. On the 17 national r80 seasons that both versions fit, the start and the length equal v1. The post-epidemic threshold is within 0.017% of v1. - The probability columns of
signal_detection_hlm_v4()andmem_thresholds_v4()are NA, not NaN, in a row where every draw is NA.
Version 2026.10.18
- New: a v4 layer that carries one ensemble,
csfmt_ensemble_v4(), through every step. A full ensemble holds the draws in memory, and a compressed ensemble holds a point value and 9 percentiles for each base measure.print()shows the state. -
vignette("v4", package = "csalert")runs the v4 layer from weekly counts to a stored table and back. -
ens_compress_v4()returns the compressed ensemble as a plain table to store, such as in PostgreSQL. It stops with an error when a name is longer than 63 bytes, the limit of a PostgreSQL name. -
as_ensemble_v4()reads a stored table back as a compressed ensemble. A step that needs draws rebuilds them from the percentiles, with seed 4 and an independent draw for each week. - The rebuild draws each week independently, because that rule gave the higher arrow agreement on a replay of luftvei data. On that replay, the trend arrow from rebuilt draws agreed with the arrow from full draws in 99.98% of 6,360 indicator-weeks. The replay covered 24 indicators, 53 as-of dates and 5 horizons.
-
csfmt_reporting_triangle_v4(),nowcast_ecdf_v4()andnowcast_passthrough_v4()build the triangle and nowcast it. With the same seed, their draws are identical to those ofnowcast_delay_ecdf_v1()andnowcast_passthrough_to_ensemble_v1(). -
ens_add_rate_v4()adds a rate draw by draw. On a compressed ensemble, it rebuilds the numerator and the denominator with paired draws. -
short_term_trend_v4()fits the trend on the quasi-Poisson kernel of 2026.10.17, with the denominator as an offset for a rate. It computes P(increasing) exactly from the fits, with no simulation. -
signal_detection_hlm_v4()andmem_thresholds_v4()write the share of the draws at each level, as<measure>_hlm_prob_<level>and<measure>_mem_prob_<level>. - On 5 national NorSySS groups, a
short_term_trend_v4()P(increasing) of at least 0.95 agreed with the v1 status “increasing” in 100% of 5,398 weeks. - On the same 5 groups, HLM “high” agreed with v1 in 99.90% of 3,150 weeks. In 965 other weeks the rate and the limit were both 0. v4 flags these weeks “high”, as v3 does, and v1 does not.
- On the r80 national NorSySS series, the 68 MEM thresholds of the 17 seasons that both versions fit were within 0.019% of the v1 thresholds.
- On synthetic data in the production shape of NorSySS, the v4 trend and HLM together took 0.53 times the v1 run time. The measurement used 3 groups of 614,628 rows.
- The v1 and v3 functions do not change.
Version 2026.10.17
- The
csfmt_rts_data_v1method ofshort_term_trend()fits all rolling windows of a series in one vectorised step, and it no longer callsglm2. On a synthetic group of 252 series of 1,000 weeks, it took 34 s, against 1,180 s before. - In that synthetic group, the statuses and forecasts did not change. 9 of 251,496 doubling days changed, and each was above 8 million days in absolute value.
- A window still leaves out its weeks with an NA count or denominator. With fewer than 3 weeks left, it now gets NA status, NA doubling days and, as the last window, NA forecasts.
- A window whose counts are all 0 keeps its status, but its doubling days are now NA. Before, they came from a fit that has no estimate.
- A window gets NA status and NA doubling days when its first week, or its last week, holds every count above 0. Before, it got the status of a
glm2fit that has no estimate, and doubling days near 0. A window whose fit does not converge still gets NA. - A window of 2 weeks with a count above 0 now gets NA status and NA doubling days. Its fit has no degree of freedom left for the dispersion. Before, some such windows got
notincreasing. A last window of 2 weeks now gives NA forecasts. - A window where
glm2halved the step got NA before. It now gets the estimate of the fit, when the fit converges. - Without a
denominator, thecsfmt_rts_data_v1method no longer stops with “object ‘varname_forecast_denominator’ not found” when the last window has no fit. It returns NA forecasts, as it does with a denominator. - csalert no longer imports
glue, because no code calls it.
Version 2026.10.16
- The
csfmt_rts_data_v1table methods ofshort_term_trend()andsignal_detection_hlm()are supported for counts without delayed reporting. The documentation no longer calls them deprecated, and their code does not change. -
gluemoves from Suggests to Imports, because the table method ofshort_term_trend()calls it. csalert no longer importsggplot2or suggestscsmaps, because no code uses them. - Removed the development files in
dev/andtests/manual/, the CRAN submission files, the R-hub workflow, two unused internal data generators and commented-out code.R/nowcast_evaluate.Ris nowR/nowcast_score.R, named afternowcast_score_v1().
Version 2026.10.15
-
short_term_trend()floors the quasi-Poisson dispersion at 1. A window with less variation than Poisson now gets the Poisson standard error. Before, it could get a near-zero one, and a<measure>_trend_increasing_prof exactly 0 or 1.
Version 2026.10.13
- Removed the
intervalandoutage_gap_daysarguments ofnowcast_delay_ecdf_v1(). On 52 replayed weeks of Norwegian surveillance data, neither option beat the default empirical interval by enough to matter. Output without these arguments does not change. A call that still passes either argument stops with an error, so it cannot silently get the default.
Version 2026.10.12
- Removed
nowcast_estimate_calibration_v1(),nowcast_apply_calibration_v1()andnowcast_evaluate_v1(). The two calibration functions rescaled the intervals after the fit, so calibrate the intervals in the nowcast model instead. Replacenowcast_evaluate_v1()withnowcast_backtest(),nowcast_truth()andnowcast_score_v1(), as stage 2 ofvignette("pipeline", package = "csalert")shows. - Inside
nowcast_delay_ecdf_v1(), the default path and the option path share one selection of the weeks to complete. The output does not change.
Version 2026.10.11
-
reporting_triangle_matrix()returns a new element,late: the count of each reference week reported at delaymax_delay_daysor later. These reports no longer count in the last delay column ofmat. -
nowcast_delay_ecdf_v1()estimates the count reported withinmax_delay_daysdays, andnowcast_truth()returns that count. Before, the last delay column also held every later report, so a backtest scored the nowcast against everything ever reported. - The published observed count keeps the late reports.
original,<denominator_col>_observedand the draws of a week that is not nowcast arerowSums(mat) + late. Sonowcast_passthrough_to_ensemble_v1()andreporting_completion_v1()give the same output as in 2026.10.10. -
nowcast_delay_ecdf_v1()leaves a week out of the pool when all its reports are past the horizon, as in a bulk load of old weeks. The week stays in the output with its observed count. - On a triangle with no report at delay
max_delay_daysor later, the nowcast engines,nowcast_truth(),nowcast_backtest()andreporting_completion_v1()give output identical to 2026.10.10.
Version 2026.10.10
-
R CMD checkno longer gives a NOTE forqc_week_over_week_v1(): thecolumnit filters on in 2026.10.8 is declared as a data.table column.
Version 2026.10.9
- Tests only: the
family = "identity"kernel test ofshort_term_trend()compares the draws withexpect_equal(tolerance = 1e-12)instead ofexpect_identical(). On macOS arm64 the draws differed from x86_64 in the last bits, so the r-universe macOS checks failed since at least 2026.10.7. A change of 1e-8 still fails the test.
Version 2026.10.8
-
qc_week_over_week_v1()has a new argument,model_pattern, default"_trend_(gr|beta1)_".$integrityleaves out the columns whose names match it. Before, the Monte-Carlo noise of trend estimates in settled weeks filled$integrity: 2314 growth-rate and 217 slope rows of 2599 in one weekly run, which hid the real data revisions. Roleless published medians, such ascases_q50x0, are still compared.
Version 2026.10.7
-
nowcast_delay_ecdf_v1()has two new options,outage_gap_daysandinterval = "log_robust". A few pool weeks with a delivery outage have extreme pool ratiosT_s / O_s, and these set the upper limit of the interval. The defaults are unchanged and give output identical to 2026.9.24 for the same seed. -
outage_gap_daysremoves a pool week from the pool when its delay days 0 todhold that many or more consecutive dates with no delivery. Heredis the age in days of the reference week to nowcast. The full pool stays when that week has its own outage, or when fewer than 3 pool weeks would remain. -
interval = "log_robust"draws the log pool ratio from a normal distribution with the median and the MAD of the log pool ratios. The median and the MAD change little when a few pool weeks have extreme ratios. -
nowcast_score_v1()is new and exported. It scores replayed nowcast quantiles against the settled truth. The scores are the weighted interval score (WIS), the WIS afterlog1p(), the 95% coverage and the 95% interval width relative to the truth. Coverage alone rewards an interval that is too wide, and WIS penalises both a miss and the width. -
nowcast_evaluate_v1()callsnowcast_score_v1()and gains the columnswis,wis_log,coverage_95andwidth_95_rel_median. The earlier columns keep their values.
Version 2026.9.24
-
reporting_triangle_matrix()sums the counts in a cell withna.rm = TRUE. Before, oneNAcount set its whole cell to 0, so the real counts in that cell were lost with no warning. -
nowcast_backtest()returns an empty data.table when no report is inside the horizon andas_of_weeksisNULL. Before, it errored with “NA/NaN argument”. -
isoyearweek_week_start()is exported. It returns the Monday that starts an ISO week, the day every delay counts from. - Documentation only: each
nowcast_delay_ecdf_v1()draw is the observed count times a quantile of the pool ratioT_s / O_s. The help page and the pipeline vignette said observed /p(d), butp(d)cancels out. - Documentation only: the pipeline vignette no longer says every older settled week finished reporting before the extract day. That holds only because
sim_reports()emits no delay beyond day 34. - Documentation only: the
nowcast_delay_ecdf_v1()help page says that a settled pool week can still receive late reports, so a long backlog makes the nowcast run low, and that a pool week with no early reports cannot enter the pool. - Documentation only: the help pages render as markdown, so their links, code and lists no longer show as raw text.
- Documentation only: every public help page has a description, a family and a vignette link.
- Documentation only: the
mem_thresholds_v1()help page says it adds the five threshold columns to$data, by reference, and not to$draws. - Documentation only: the
signal_detection_hlm()help page says the ensemble method codes a draw 1 fornulland 2 forhigh, not 0 and 1. - Documentation only: the
simulate_baseline_data()help page saysphi = 1gives Poisson counts. It saidphi = 0, which givesNA. - Documentation only: the help pages state that
remove_last_isoyearweeksof the deprecatedsignal_detection_hlm()method has no effect. - Documentation only:
prediction_interval()and its glm method are internal, and left the reference index. The generic was never exported. - Documentation only: the pipeline vignette has a quality-control section, and the Get started vignette has a simulation section.
- Documentation only: the vignettes say that csdb cannot validate a v3 table. They said that a v3 table cannot be stored.
- Documentation only: the README no longer says that
denominatorbelongs only to the deprecated trend method.
Version 2026.9.23
A late report is a report at delay max_delay_days or later, counted in days from the reference week’s Monday.
Breaking: reporting_triangle_matrix() keeps late reports
-
A late report counts in the last delay column,
max_delay_days - 1. Earlier versions dropped it. The delay columns are unchanged,0tomax_delay_days - 1. -
The drop lowered published counts, and nothing warned. Both engines publish
rowSums()of this matrix asoriginal. Measured on 2026-09-23 in luftveisovervaking_trend with one input file,s19_hosp_sarilost every week from 2020-01 to 2025-26. That is 287 weeks and 346,306 admissions, nearly all bulk-loaded on 2025-07-30. The settled weeks lost a net 2,453 inmsis_covid19, 1,367 inlab_hospitalised_sars_cov_2and 448 indaar_sari_scd2. -
Published numbers move wherever a series has a late report. That covers
original, every nowcast,nowcast_truth(), andmean_delayand thepct_delayDcurve ofreporting_completion_v1(). A week whose every report was late now carries its count, where before it was absent or zero. - A report before the reference Monday has a negative delay, and it is still dropped.
-
A settled week is not final: a report at a delay beyond
max_delay_days - 1can still arrive and raise its count.
Version 2026.9.22
The nowcast stack moves from a weekly delay axis to a daily one, and the default engine changes. Two of the six breaking changes below return wrong numbers rather than an error.
Breaking: the reporting axis is a Date
-
csfmt_reporting_triangle_v3()defaultsreporting_coltoreporting_date, and that column MUST be aDate.isoyearweek_reportingis gone. A character column raises an error naming the class it got. -
The
as_ofattribute is aDate. It held the largest ISO-week string before. Code that comparedas_ofagainst an ISO week now compares two types. -
The constructor errors on a missing reporting date, and the message names the count. One
NAused to setas_oftoNA. No reference week then settled, no delay pool built, and every engine returned the observed totals while labelling them a nowcast. Densification dropped the row as well, so the week’s own count was understated too. Map every missing reporting date to a date, or drop those rows, before you build the triangle.
Breaking: max_delay is now max_delay_days, and it counts DAYS
-
max_delay_days = 35keeps delay days 0 to 34 inclusive, which is 35 columns. That is the span the oldmax_delay = 5weekly columns covered. Multiply an old weekly value by 7. - A caller who renames the argument and keeps the value gets a 5-day horizon where they meant 5 weeks. Nothing warns. The call runs and returns numbers, and those numbers are wrong.
- Eight exported functions take
max_delay_days:reporting_triangle_matrix(),nowcast_passthrough_to_ensemble_v1(),nowcast_truth(),nowcast_backtest(),nowcast_evaluate_v1(),nowcast_delay_ecdf_v1(),reporting_completion_v1()andreporting_completion_trend_v1(). -
reporting_completion_v1()returns onepct_delaycolumn per delay day. A call that returnedpct_delay0topct_delay4now returnspct_delay0topct_delay34, so 5 columns become 35. There are exactlymax_delay_daysof them.
Breaking: qc_week_over_week_v1() takes max_delay_weeks, in WEEKS
- Two parameter names, two units, on purpose. This function windows reference weeks, never delay. Its horizon sets the boundary between the settled weeks and the frontier, and that boundary counts reference weeks.
- The nowcast engines take
max_delay_days, because their axis is the delay in days. One name across both would mean days here and weeks there. - Do not rename
max_delay_weeksto match the engines. The old name wasmax_delay, and each new name states its own unit.
Breaking: nowcast_quasipoisson_v1() is now nowcast_delay_ecdf_v1(), and it is a different estimator
-
Its numbers move. The new engine completes each incomplete reference week from a pooled daily delay ECDF. The old engine regressed the settled total on the observed delay columns. The interval is empirical now, taken from the pool’s own
truth / estimateratios, and no longer parametric. -
nowcast_quasipoisson_v1()no longer exists. A call to it raises “could not find function”. - The model family is unchanged. The ECDF is the closed-form maximum likelihood estimator of
n[ref, d] ~ Poisson(lambda[ref] * p[d]), the same Poisson delay model. It estimates the delay profile saturated, one number per delay day, rather than through a regression. -
The old engine failed silently on complete data. Where every settled week’s delay columns sum exactly to its total, the design matrix is perfectly collinear and
coef()returnsNA. The engine then fell back to the observed count and labelled it a nowcast. Measured onlab_hospitalised_influensavirus: 27 of 27 replays at horizon 2. - The delay ECDF was measured against a quasi-Poisson GAM with a smooth in delay and a weekday term. Under a log link the two agree: every difference sits inside 0.5 percentage points over 30 replays. Under the identity link, which the old engine used, the GAM failed to converge on 30 of 30.
Version 2026.8.28
Installation points at the r-universe
-
README.mdtold users to runinstall.packages("csalert"). With no repos set that reaches CRAN, which carries 2024.6.24 from 2024-06-24. It installs without complaint, so a reader following the README got a release two years behind and nothing said so. - The section now sets
repostoniphr.r-universe.devfirst, which builds every commit onmain, and says to put that in~/.Rprofile. - It also records what
pakcan express, measured against pak 0.11.1: a version range such ascsalert@>=2026.8.27is rejected with “Version ranges are not implemented yet”, an exactcsalert@2026.8.27pin works, and aDESCRIPTIONfloor ofImports: csalert (>= x)resolves throughpak::local_install_deps().
No code changed. This release is documentation only.
Version 2026.8.27
Breaking: propagate_slope_error was removed, and the slope error is always propagated
-
short_term_trend()on acsfmt_ensemble_v3no longer takespropagate_slope_error. The slope’s own sampling error now reaches every draw, always, and there is no way to switch it off. - Two things are uncertain and both belong in the draws: the level, which the incoming ensemble already carries, and the line fitted through those levels.
increasing_pris the share of draws whose slope is positive, so without the second it is a sign test on the point slope wearing the name of a probability. A threshold like> 0.975on it then does nothing at all. That was the default behaviour, and nothing warned. -
Published numbers change. Every
_trend_beta1,_trend_grand_trend_increasing_pra caller was producing under the old default is different now, and the trend columns are no longer reproducible run to run without a seed. Set one if you need that. -
$draws[["<measure>_trend_beta1"]]is no longer the point estimate. It holds the perturbed draws. Callrolling_slope_matrix()for the unperturbed slope, its standard error and its convergence flag. - Passing
propagate_slope_errornow raises an error naming the removal. The method takes..., so without that guard a caller who had turned it off would get propagated numbers and no notice. -
family = "identity"andfamily = "quasipoisson"now refusetrend_isoyearweeks = 2. Both read a dispersion offwidth - 2residual degrees of freedom, sosehas no value at width 2 and noerror_referencerepairs it.family = "binomial"fixes the dispersion at 1 and still accepts width 2. -
n_simmoved from argument 5 to argument 4, which changes the meaning of a positional call. -
error_referenceis unchanged and still selects the reference distribution.
The csfmt_rts_data_v1 method is untouched. It fits its own GLM per window and never had this argument.
Version 2026.8.26
Trend on the link scale
-
short_term_trend()on acsfmt_ensemble_v3takesfamily."quasipoisson"is a log link and"binomial"is a logit link.beta1is then the slope on the link scale. That is thecoef(model)[2]the olderglm()pipelines report, so the two become comparable."identity"is the default and reproduces the previous numbers exactly. - The growth rate on a link scale is
100 * (exp(beta1) - 1), a percent change per week. Under"binomial"that percent change is in the odds. - A zero count needs no offset. The log link models
E[Y] = exp(b0 + b1 t)and never takeslog(Y). -
family = "binomial"needs the newdenominatorargument, which names the$drawsmeasure holding the binomial denominator. It enters the fit as the prior weight, andmeasureMUST then be a proportion between 0 and 1. - The rolling GLM stays a filter. IRLS advances every window of every draw together, and no window reaches
stats::glm().
A window that cannot converge returns NA
-
A separated window has no finite slope, and the fit cannot converge. Complete or quasi-complete separation drives the IRLS slope to infinity. The iterate that
maxitstops on is finite, sois.finite()never removed it. A window ofc(0, 0, 0, 0, 0.2, 0.3, 0.4, 0.5)separates at width 4, with a denominator of 50. Its growth rate reached$drawsas 1.06e13 percent per week.beta1,seand the growth rate are nowNA_real_there. -
rolling_slope_matrix()returns a fourth matrix,converged. It is logical, andNAon the leadingwidth-1rows that hold no window. -
beta0isNA_real_there too. A lone intercept beside a missing slope reads as a fitted level, and it is the same meaningless iterate. -
An all-zero window returns
NA, andstats::glm()returns a slope of 0. That is the one place the two disagree on purpose.stats::glm()stops on the relative change in the deviance, which is 0 for an all-zero window from the first iteration. This kernel stops on the coefficient step, and that step never settles: the intercept marches to-Infas the fitted mean goes to 0. An all-zero window identifies no slope, andNAsays so. - The warning reports the iteration the batch stopped on, and names
maxitseparately. It claimedmaxititerations before. A batch holding a missing-value window can settle earlier, because the stopping rule drops that window withna.rm. - The two GLM families warn once per call, with the count of window fits that did not converge. A run of zero weeks followed by a rise is ordinary early-season surveillance data, so the warning will fire.
The slope-error reference is now an argument
-
short_term_trend()on acsfmt_ensemble_v3takeserror_reference."auto"is the default. It gives"identity"and"quasipoisson"a t ontrend_isoyearweeks - 2degrees of freedom. Both estimate a dispersion from that many residual degrees of freedom. That is the casesummary.glm()refers to a t. -
"binomial"keeps a standard normal under"auto", because that family fixes the dispersion at 1 and leaves nothing to estimate. - Pass
error_reference = "normal"to match a pipeline whose interval comes fromstats::confint(), which profiles the deviance against an asymptotic chi-squared."t"forces the other direction on every family. -
propagate_slope_error = TRUEnow needstrend_isoyearweeks >= 3underfamily = "identity"andfamily = "quasipoisson", whatever theerror_reference. Both read a dispersion offwidth - 2residual degrees of freedom, so at width 2seisNaNunder identity andInfunder quasi-Poisson. The growth rate then reached$drawsmissing, or as a random -100 percent.family = "binomial"fixes the dispersion at 1 and still accepts width 2. -
family = "identity"is unchanged. It drewse * t_(width-2)before and it still does.
Static checks
-
R/passes the sharedstatic-checksgate. The gate arrived with the workflow pin two releases ago and reported 127 findings across 23 of the 24 code files. Every one is now fixed. - Every
stop()andwarning()namescall. = FALSE, because the message goes to a user who cannot act on the call. Every function returns explicitly.seq_len()andseq_along()replace1:n,&&and||replace&and|inside a condition, andTRUEandFALSEreplaceTandF. - Seven functions were over 15 cyclomatic complexity and gave their branches to named helpers. In
short_term_trend_ensemble.R,rolling_irls_slope()gave upirls_check_response(),irls_working()andirls_wald_se(), andshort_term_trend.csfmt_ensemble_v3()gave upstt_prior_weights()andstt_error_reference(). - No behaviour changes. One user-facing message reads “0.05, 0.25, 0.5, 0.75 and 0.95” where it read “0.05/0.25/0.5/0.75/0.95”, because the slashes read as a file path.
Version 2026.8.23
- The 51-name
utils::globalVariables()list is gone. Each NSE column name is now declared asNULLat the top of the one function that uses it: 18 declarations across 12 files. Only.stays package-wide, because it is read as an undefined function and no local binding fixes that. - The old list was both too wide and too narrow.
level,method,period,roleandtruthare also formal arguments of other functions, so declaring them package-wide suppressed the note for a genuine undefined variable anywhere in the package.coverage_rawandhorizonwere declared and used nowhere. -
R/csfmt_ensemble.Ralready used this idiom atset_time_series_id(). The package now uses it everywhere.
Version 2026.8.22
The package drops
magrittr. Every%>%is now the base pipe|>, andmagrittris gone fromDESCRIPTION.The rewrite is a relocation, not an edit. Each
%>%call was transformed the way R’s parser transforms|>, and the resulting tree was required to match the tree parsed from the rewritten file. A file whose trees disagreed was left untouched and converted by hand instead.The version runs one day ahead of the calendar on purpose. r-universe already published today’s number from a different commit, and one version must never name two trees.
Version 2026.8.21
-
R/globals.Rdeclares".".R CMD checkread data.table’s.(), its alias forlist(), as an undefined function rather than an undefined variable, so the file’s existing declarations did not cover it.
Version 2026.8.7
Licensing
- The copyright holder is now Folkehelseinstituttet. It read “Core Surveillance”, which names the package family rather than a legal entity.
-
DESCRIPTIONAuthors@Rnow declares that holder withrole = "cph". It declared no copyright holder at all, and neither did any other package in the fleet. Nothing inR CMD checkreports that. - The copyright year is now 2026. It read 2023.
-
CLAUDE.mdnow carries a Licensing section, so the year gets checked rather than silently ageing.
Corrections
-
?nowcast_evaluate_v1claimed common random numbers, and the code does not establish them. Every method starts from the same RNG state on each as-of week, which pairs the comparison. Common random numbers would also need the methods to consume compatible variates, and nothing enforces that. The documentation now states the mechanism and stops there.vignette("pipeline")already said this. The roxygen contradicted it in three places, including a worked example.
Documentation
Every prose file in the repository is swept to ASD-STE100 (Simplified Technical English). The sweep covers the roxygen blocks in
R/, both vignettes,README.md,index.mdand this file. The sweep changed no claim, number, hedge or scope qualifier, and no executable line. The one claim that did change is under Corrections above.Sentences over 25 words: 34 to 0 in roxygen, 80 to 0 in the vignettes. Counted per authored unit, outside fenced code and outside code-bearing roxygen tags.
README.mdwent 3 to 0,index.mdwent 1 to 0, and this file went 45 to 0. Long sentences were split, not shortened, so every condition that made one true survives in the sentences that replaced it.SHOULDandSHOULD NOTare now capitalised in the five places that state an obligation. Three are inR/and two are invignette("pipeline"). A lowercase “should” reads as advice and gets treated as optional.The deprecation notes are unchanged in substance. They are on
?short_term_trendand?signal_detection_hlm. Both still say the replacement is not a drop-in, that migration is a rewrite of the call site, and that the numbers will not match.One roxygen section gained a bulleted list, where three parallel conditions were buried in a single sentence. It is the “What it does NOT check” section on
?validate_ensemble. The three conditions are unchanged.31
@paramdescriptions gained the terminal full stop they were missing. No wording changed. Without it a sentence counter runs straight through the boundary between one field and the next, and reports one very long sentence.New
vignette("csalert"), the get-started page. It is an orientation and runs no analysis. It covers what the package is for, which of the two generations is current, where thecsfmt_rts_data_*classes live, and what cannot be done yet. pkgdown promotes a vignette named after the package to “Get started” in the navbar, which is where the site’s primary call to action now points.vignette("nowcasting")is nowvignette("pipeline"), retitled to “The pipeline: from incomplete counts to published numbers”. Its content, numbers and statistical claims are unchanged; the opening paragraphs were rewritten in plainer language. The 23@seealsocross-references inR/were updated with it, so no help page points at a vignette that no longer exists.vignette("short_term_trend")is deleted. It taughtshort_term_trend.csfmt_rts_data_v1, which is deprecated, so it was teaching new readers the route they should not take. The only worked example of that method is now the one on?short_term_trend. Its county-map demonstration was not ported.
No behaviour change
- This release changes prose only. No exported function, argument, default or return value moved.
Bug fix
-
qc_week_over_week_v1()silently dropped every HLM alert transition.signal_detection_hlm()writes naming-grammar role"hlmstatus", but the function selected onlyrole == "status"for its$signaltable. An escalation from normal to high therefore never appeared in the week-over-week review. Demonstrated on a 380-week series: a real transition from 1 to 2 at week 2025-14 returned 0 rows before the fix and 1 after. HLM needs roughly five years of history before it produces a status at all, which is why a shorter test series shows nothing. - The same filter had a second, opposite half.
$integrityexcludedrole != "status", so HLM status CODES were diffed as if they were continuous medians. An ordinal 1-to-2 step read as a numeric revision. - Both halves are now driven by a new
status_rolesargument, which defaults toc("status", "hlmstatus"). The roles excluded from$integrityare exactly the roles whose transitions appear in$signal. Pass a different vector to narrow or widen it. -
This changes no legacy path.
qc_week_over_week_v1()operates on collapsed ensemble output. Its only callers are this package’s tests and the ensemble-first pipelines. Nothing on the pre-ensemblecsfmt_rts_data_v1route reaches it.
Version 2026.8.6
Bug fix
-
short_term_trend()on an ensemble broke the data.table’s self-reference, so the NEXT stage warned. It added its P(increasing) column with base[[<-, which copies the table. Any later stage assigning with:=then emitted data.table’s ten-line “shallow copy was taken” advisory. Bisected:rate -> hlmis clean,rate -> trend -> hlmwarns. That is the canonical order, so every production pipeline running a trend before another ensemble stage saw it on every run. Now usesdata.table::set(). Proven causally red: reverting the one line reproduces the warning.
Documentation
-
vignette("nowcasting")now runs the whole canonical chain on one synthetic triangle with a denominator. The stages are nowcast, replay validation, reporting completion, rate, short-term trend, MEM intensity, HLM signal detection, and the terminal collapse. It opens by stating the rule the architecture turns on. Every analytical stage takes acsfmt_ensemble_v3and returns one, andens_collapse()is terminal. A collapsed table is therefore output and never input.ens_add_rate(),mem_thresholds_v1()andsignal_detection_hlm()were previously named in prose but never run; production depends on all three.
Deprecated
-
short_term_trend.csfmt_rts_data_v1()andsignal_detection_hlm.csfmt_rts_data_v1()are deprecated. They are the pre-ensemble architecture. Both still work and NEITHER WARNS AT RUN TIME, so existing pipelines are undisturbed; the mark is a signpost for new work. - The replacement is not a drop-in. The v1 methods take
numerator,denominator,prXand the naming-prefix arguments, and return a status label. The ensemble methods take onemeasureand return a per-draw slope, growth rate and P(increasing), with no classification. Migrating is a rewrite of the call site and the numbers will not match. -
short_term_trend()andsignal_detection_hlm()now havecsfmt_rts_data_v3methods that onlystop(), naming the architecture instead of failing with a bareUseMethoderror. A collapsed table carries quantiles, not draws, so a per-draw trend cannot be recovered from one.
Version 2026.8.5
Bug fix
-
reporting_completion_v1(triangle, max_delay = 1)returned nonsense, silently. With a single delay columnapply(M, 1, cumsum)returns a vector rather than a matrix. The transpose then produced a 1-by-n_settled matrix. The function emitted onepct_delaycolumn PER SETTLED WEEK instead of one in total, withcomplete_by_mdfar below 1. Measured on a 30-week triangle: 30 completion columns andcomplete_by_mdof 0.033, where both should be 1. No error was raised. Fixed, with a regression test that also pins the column count tomax_delayat horizons 1 through 4.
Documentation
-
vignette("nowcasting")is now a four-stage end-to-end pipeline on one seeded synthetic triangle: nowcast, replay validation, reporting completion, short-term trend. Every chunk executes underR CMD check. The reporting-completion section answers the question the old text left open: is the first completion column the reference week or the week after? It walks a named ISO week day by day. It shows that delay is measured in whole ISO weeks, so a Monday run and a Friday run bucket every report identically. -
nowcast_truth()’s description was off by one, in both bounds. It said it summed “all delays up tomax_delay” and kept weeks “at leastmax_delayweeks before” the as-of. The code sums delays0 .. max_delay - 1and keepsage >= max_delay - 1. Measured: atmax_delay = 3the newest settled reference week is 2 weeks before as-of, not 3. Corrected.
Breaking change
-
reporting_completion_v1()renames its completion columns frompct_wNtopct_delayD, and they are now 0-based and indexed by DELAY. Withmax_delay = 3the columns werepct_w1,pct_w2,pct_w3; they are nowpct_delay0,pct_delay1,pct_delay2. - The old names were indexed by weeks-observed, counting the reference week itself as week 1, so
pct_w1held delay 0. The number in the name never equalled the delay it represented. Next to a reporting triangle whose columns are delays 0, 1, 2, it read as “the week after”.pct_delay0says outright that it is the reference week itself. - The new names match the
max_delayargument: the column count equalsmax_delayand the highest index ismax_delay - 1. -
Update any code that reads these columns. The old names are gone rather than redefined, so stale code fails with an unknown-column error instead of silently returning a different week.
reporting_completion_trend_v1()passes the columns through and changes with it.
Version 2026.8.4
Documentation
-
Every exported function now has a runnable example. 21 of the 34 exports had none; they already had a title, a description,
@paramand@returns, but nothing a reader could run. The new examples all execute underR CMD check. None is wrapped in\dontrun{}or\donttest{}, because nothing in the package needs a live database, a credential or a mounted share to demonstrate. The nowcast pipeline examples share one 40-week synthetic reporting triangle, right-truncated so the newest weeks are genuinely incomplete. -
Every exported function now carries an
@seealsothat says which vignette covers it, including when the honest answer is “neither”. Only eight exports are actually run inside a vignette code chunk.ens_add_rate(),mem_thresholds_v1()andsignal_detection_hlm()are named in the closing “Where next” list of the nowcasting vignette, but not demonstrated there. The@seealsonow says so rather than implying coverage. -
Eight
@familygroups added as topical navigation, so the reference pages cross-link. The groups are ensemble format functions, reporting triangle functions, naming grammar functions, nowcast engines, nowcast diagnostics, nowcast calibration functions, ensemble operations, and reporting completion functions. These group functions by the concept they belong to, not by a shared call signature. Onlynowcast enginesis a set of interchangeable implementations. The other seven are pipeline stages or helpers around one concept, and each member’s@seealsostates its own specific role. The twoqc_*functions were deliberately not made a family.qc_surveillance_data_v1()screens one input feed and returns a verdict.qc_week_over_week_v1()diffs two finished runs and returns two tables. They are cross-linked with@seealsoinstead. -
compare_results()gained an “Identity columns” section. It finds its value columns withcsfmt_interpret(). An identity column outside the csfmt structural schema —locationrather thanlocation_code— is therefore read as a value column.cur/prvthen come back as character instead of numeric.qc_week_over_week_v1()then fails outright withError in cur - prv : non-numeric argument to binary operator. The nowcasting vignette teaches exactly those non-schema names, so this is easy to hit. - README grown from 23 words to a landing page. It covers what the package is, the two routes through it, installation, one quick start, a which-function-do-I-want table, and links to the pkgdown site. It copies no passage from either vignette; the longest shared run of consecutive words is three. It does necessarily summarise the same pipeline the nowcasting vignette explains in full.
Corrected statistical documentation
No behaviour changed. Each of these help pages described a model, or a guarantee, that the code does not implement. An adversarial review found them.
-
nowcast_quasipoisson_v1()is not fitted without an intercept. The documentation said “no intercept” in two places. But the formula is built asy ~ d1 + d2 + ..., which carries R’s default intercept. An inline comment in the source already said# + intercept. The documented model now matches the fitted one. -
“Honestly dispersed” removed from the same engine. Simulating from a fitted model and truncating at the observed count does not establish calibrated coverage. The documentation now says calibration is an empirical question about a given series and points at
nowcast_evaluate_v1(), which exists to measure it. -
The calibration functions no longer claim split conformal or nominal coverage. The estimator takes the ordinary type-7 quantile of
|truth - median| / halfwidth, not the conformal order statistic. That symmetric residual is only faithful for intervals roughly symmetric about the median. There is no finite-sample guarantee.coverage_rawis now documented as what the engine did on the replayed weeks, not as a property of the engine. -
complete_by_mdis documented as identically 1. It was described as detecting reporting that continues past the horizon. It cannot:reporting_triangle_matrix()discards delays at or beyondmax_delaybefore the total is formed. The final cumulative fraction of that truncated total is therefore always 1, andpct_delay<max_delay-1>is always 100. To look for a tail, re-run with a largermax_delay. This is a source defect left unfixed and now documented. -
q_value()is no longer described as the inverse ofq_label(). The label format holds two integer-percent digits, soq_label(1)gives"q100x0", whichq_value()returnsNAfor. The round trip holds on[0, 1). -
csfmt_parse()is no longer described as the inverse ofcsfmt_var(). It strips a role from the right against a fixed vocabulary. On the package’s own rate namenumerator_nowcasted_vs_denominator_nowcasted_pr100it eats the denominator’s_nowcastedas the role, and returnsdenom = "denominator". A new section documents the limit with that exact name. -
validate_ensemble()is retitled “Check a csfmt_ensemble_v3’s structural shape”. It checks class, two column names, and that each draw matrix has one row per row of$data— the row COUNT only. Permuting the rows of$dataor of a draw matrix passes, as do a missing key, a brokentime_series_internal_idand a deletedtime_series_label. The page previously implied it was a safety net for hand-edited objects. It now says to rebuild withcsfmt_ensemble_v3()instead, and its example demonstrates a permutation passing. -
qc_week_over_week_v1()cannot see HLM transitions.signal_detection_hlm()writes role"hlmstatus"while the check selects role"status", so onlymem_thresholds_v1()output reachesqc$signal. The example said otherwise and now records the gap.
Packaging
-
^pkgdown$and^Rplots\.pdf$added to.Rbuildignore.pkgdown::build_site()leaves apkgdown/favicon/directory behind, and an example that draws a plot leaves anRplots.pdf. Both otherwise ship in the tarball and raise a non-standard-top-level-file NOTE.
Version 2026.7.1
Trend uncertainty
-
short_term_trend()can now propagate the slope’s own sampling error. The growth rate is100 * beta1 / Y. Until now only the uncertainty of the levelYreached it, and the OLS standard error ofbeta1was computed and discarded. So every trend interval reflected nowcast uncertainty alone. A passthrough (single-draw) series got a zero-width interval and aP(increasing)of exactly 0 or 1. That is a bare sign test on a three-point slope presented as certainty.propagate_slope_error = TRUEaddsse * t_(width-2)per draw, widening the trend’s own draw axis ton_simwhen the incoming ensemble is degenerate. Defaults toFALSE, so published numbers are unchanged until it is switched on deliberately. Note the degrees of freedom aretrend_isoyearweeks - 2: at the default window of 3 that is a Cauchy, so widen the window first.
Nowcast calibration, restored as a diagnostic
-
nowcast_estimate_calibration_v1()/nowcast_apply_calibration_v1()are back, after being removed earlier in this release cycle. They are available to check an engine with, not applied to published numbers. The estimator reports a per-horizonfactor. So “your 90% interval would need to be 1.4x wider to actually cover 90%” is readable straight off the backtest. That is a far more actionable red flag than a bare coverage fraction, and it keeps the published number the model’s own. - Their tests now run against
nowcast_quasipoisson_v1. The old assertions (factor > 1everywhere, raw coverage below 0.82) encoded the removednowcast_survrtrunc_v1’s behaviour. The current engine covers about 0.87 on the same drifting-delay synthetic, close enough to nominal that no widening is warranted.
Bug fixes
-
prediction_interval.glmis now registered withS3method(). It resolved when the package was installed, but not underdevtools::load_all(). The method’s own test therefore failed in a dev tree while passing inR CMD check.
Simplification
-
reporting_completion_trend_v1returns the completion curve by calendar year (all) + by month (last N, per series), with ascopecolumn. That is the year/month trend the luftveis pipeline used to assemble by hand. -
nowcast_evaluate_v1is now the single entry point for scoring nowcasts. Give it a triangle and one method, or a NAMED list. It replays each – paired by a sharedseed– and returns one per-horizon table of interval coverage + point-estimate revision, with amethodcolumn. It subsumes the formernowcast_score(coverage, via scoringutils),nowcast_revision,nowcast_compareandnowcast_validate. Coverage is read straight off the interval quantiles, soscoringutilsis no longer a dependency (WIS was dropped). - Removed the unused
nowcast_survrtrunc_v1engine, and itsflexsurvdependency. Also removed the conformal calibration functions (nowcast_estimate_calibration_v1/_apply_/print.nowcast_calibration). Both were already dropped from the production pipeline. - Renamed
mem_thresholds->mem_thresholds_v1(versioned-engine convention).
Documentation
- New vignette “Nowcasting a reporting triangle with csfmt_ensemble_v3” — a runnable end-to-end walk-through (synthetic triangle -> nowcast -> collapse -> backtest coverage/revision/completion -> naming grammar).
- Documented the previously-bare
print.csfmt_ensemble_v3method; added the missing@returnsto the ensemble/nowcast S3 methods (mem_thresholds_v1,short_term_trend,signal_detection_hlm,nowcast_quasipoisson_v1). - Added runnable
@examplesto the naming-grammar functions (csfmt_var,csfmt_parse,q_label,q_value) and to the nowcast analysis functions (nowcast_evaluate_v1,reporting_completion_trend_v1). - Fixed unescaped
%in the nowcast /reporting_completionroxygen that had been corrupting their generated.Rd, plus copy-paste errors in the simulation-helper docs.
csfmt_ensemble_v3 surveillance engine
A new draw-parallel ensemble format and the full analysis pipeline built on it (reporting triangle -> nowcast -> trend -> MEM thresholds -> quantile collapse).
-
csfmt_ensemble_v3: S3 container ($datadata.table + per-measure$drawsmatrices) with invariants enforced byvalidate_ensemble. - Naming grammar:
csfmt_var/csfmt_parse(order-independent parse of trailing coordinates),q_label/q_value, andcsfmt_interpretfor self-describing datasets. -
csfmt_reporting_triangle_v3: reference-by-delay input format with a reshape that completes the reference axis. -
nowcast(): reporting triangle -> ensemble via flexsurvsurvrtrunc+ negbin (no epinowcast dependency). Can nowcast a denominator alongside the numerator (full % positive -> MEM path) and surface the observed denominator total. The reporting-before-reference check is NA-safe. -
observed_ensemble(): passthrough (degenerate single-draw) ensemble for indicators that should not be nowcasted. -
collapse(): ensemble -> quantile summary;collapse(heal = TRUE)heals the result into cstidycsfmt_rts_data_v3. -
add_rate(): numerator-vs-denominator rate (% positive). -
short_term_trend.csfmt_ensemble: batched, shared-design-matrix kernel that also emits P(increasing). -
mem_thresholds.csfmt_ensemble_v3: MEM intensity thresholds with provisional seasons.exclude_seasonsdrops anomalous seasons from the baseline. Training is capped to the most recenti.seasonsbeforena.omit. A non-zero-season guard plus a quietmemmodelwrapper prevent sparse indicators from erroring. -
signal_detection_hlm.csfmt_ensemble_v3: per-draw exceedance detection. -
add_holiday_effectfor the ensemble format. - Input QC:
qc_surveillance_data(generic input QC, verdict only) andqc_week_over_week(A/B revision comparison across runs). -
add_rateandcollapseare renamed toens_add_rate/ens_collapse. They are now S3 generics dispatching oncsfmt_ensemble_v3(theens_family), so the ensemble class carries the “operates on an ensemble” meaning. Behaviour is unchanged. (add_holiday_effectis a simulation-data helper on a plain data.table and keeps its name). -
nowcastandobserved_ensembleare renamed tonowcast_survrtrunc_v1andnowcast_passthrough_to_ensemble_v1– concrete, VERSIONED nowcast engines that share the contractf(reporting_triangle, ...) -> csfmt_ensemble_v3. Behaviour is unchanged; the_vNsuffix versions the algorithm (a futurenowcast_stan_v1ornowcast_simple_v2slots in beside them), selected by a caller-side registry. The validation harness (nowcast_backtest/score/compare/validate/censor/truth) is generic tooling and is NOT versioned. -
nowcast_survrtrunc_v1gainsdelay_window(default 26 weeks): the reporting-delay distribution is estimated from only the most recent weeks, so a non-stationary / drifting delay (e.g. a backfilled history then live prospective reporting) is tracked instead of averaged into a stale pooled curve. Fixes the median bias that caused sub-nominal interval coverage. Residual under-coverage (plug-in delay) is documented by the calibration test, and awaits per-draw delay uncertainty or backtest-driven recalibration.NULLrestores the old pool-all-history behaviour. - Calibration test (
test-nowcast-calibration.R): empirical interval coverage vs nominal on synthetic data, with a stationary case (calibrated) and a drifting-delay case (reproduces the real-data under-coverage synthetically). -
nowcast_simple_v1renamed tonowcast_survrtrunc_v1(the name now states the method: right-truncated survival delay + negbin completion). - New engine
nowcast_quasipoisson_v1: a discriminative (regression) nowcast. For each horizon, regress the settled TOTAL on the counts reported so far, on the recent settled weeks. The regression istotal ~ n[delay 0] + n[delay 1] + ..., quasipoisson/identity, no intercept. Then simulate the incomplete weeks (parameter uncertainty from the fit + a dispersion-matched negbin). No per-week magnitude parameter, so it is robust for the recent weeks and honestly dispersed. Drifting-delay synthetic coverage is ~0.79, vs the plug-in survrtrunc’s ~0.72 (nominal 0.90). Base stats only; samef(triangle) -> ensemblecontract -> drops into the registry as a candidate key. - Backtest-driven recalibration.
nowcast_estimate_calibration_v1learns a per-group (default horizon) conformal interval-scaling correction from past nowcasts vs settled truth.nowcast_apply_calibration_v1applies it, so a method’s intervals hit nominal coverage regardless of internal misspecification. Anowcast_calibrationS3 object with a print method sits between. Turns the backtest into calibration data: engine -> backtest -> estimate -> apply -> honest intervals. Distribution-free (split conformal); estimate on past backtests, apply to the current nowcast. - Nowcast validation harness (method-agnostic, replay-based).
nowcast_censorreconstructs what was known as-of a past week from the reporting triangle.nowcast_truthgives settled totals.nowcast_backtestreplays anyf(triangle) -> ensembleacross as-of weeks into tidy quantile nowcasts.nowcast_evaluate_v1scores one or several methods on interval coverage + point-estimate revision by horizon – see the Simplification section above. -
reporting_completion: the empirical reporting-delay summary of a triangle. From the settled weeks it gives the mean delay, and the weeks-observed to reach 25/50/75/90/95% of a reference week’s cases. It also gives the fraction actually in bymax_delay.period = "year"/"month"stratifies the settled weeks in time, by the ISO year / midweek-day month. A drift in reporting speed then shows up as a trend in mean delay, instead of being averaged away.
Version 2022.4.21
- short_term_trend created to allow for easy estimation of short-term trends (increasing/decreasing/null), doubling time in days, and short-term forecasting with prediction intervals.
- prediction_interval created to allow for easy estimation of prediction intervals after fitting glms (family = poisson and quasipoisson) based on Farrington 1996.
