Intro to rollcast

Author

Giancarlo Vercellino

Published

September 2, 2026

rollcast rollercoaster hexagon logo

“A map is not the territory it represents.” – Alfred Korzybski, engineer and philosopher

“The most important questions of life are, for the most part, really only problems of probability.” – Pierre-Simon Laplace, mathematician and astronomer

“We can’t control systems or figure them out. But we can dance with them!” – Donella H. Meadows, systems thinker

What rollcast is trying to do

rollcast is a small R package for probabilistic forecasting of one numeric time series. It does not start with a heroic global model. It starts with rolling evidence: what the recent mean says, what the median says, what the local extremes say, what a short regression endpoint says, and what selected rolling quantiles say.

Those rolling summaries become forecast anchors. A proper-score gate then learns which anchors deserve more probability in the current state. Optional state-conditional residuals add local dispersion, and recursive simulation turns the one-step mixture into predictive paths.

The short version: rollcast is a careful committee with a calibration habit.

rollcast workflow diagram

The package works this way:

  • Anchor dictionary: rolling summaries define plausible next locations.
  • State features: each anchor is compared with the current level under a robust scale.
  • Soft responsibilities: historical outcomes tell the model which anchors were close without forcing a hard label.
  • Proper-score gate: a softmax model is trained by predictive log score, so the target is probabilistic calibration rather than classification theater.
  • Residual texture: nearby historical states can donate residual shapes when error_scale > 0.
  • Adaptive persistence: anchor weights can move slowly in stable states and faster when the state changes.
  • Recursive simulation: forecast paths are simulated forward, so uncertainty can widen with horizon.

Five anchors walk into a forecast. The gate checks the room, turns up the useful ones, and lets the simulation do the rest.

1. Install and load

For a local source build, install the package tarball from the package directory:

install.packages("rollcast_0.1.0.tar.gz", repos = NULL, type = "source")

Then load it in the usual way. In this rendered article, rollcast is loaded from a local render library so the examples below show their actual results.

library(rollcast)

2. Fit rollcast, aka anchor first, argue later

We will use a simple synthetic series: enough drift to make direction matter, enough noise to keep the forecast honest.

set.seed(1)
y <- 100 + cumsum(rnorm(180, mean = 0.03, sd = 0.8))

fit <- rollcast(
  y,
  window = 45,
  tau = 0.25,
  lambda = 0.01,
  conditional_k = 20,
  state_bw = 1,
  residual_bw = 0.35,
  error_scale = 0.25,
  residual_smoothing = 0.03,
  rho_min = 0.05,
  rho_max = 0.90,
  rho_decay = 1,
  min_history = 15,
  maxit = 120
)

fit
#> 
#> Rollcast probabilistic forecasting model
#> ------------------------------------------
#> Observations          : 180 
#> Rolling window        : 45  
#> Anchors               : mean, median, min, max, regression_forecast, q0_05, q0_10, q0_25, q0_75, q0_90, q0_95 
#> Conditional k         : 20 
#> State bandwidth       : 1 
#> Selected lambda       : 0.01 
#> Selected tau          : 0.25 
#> Selected residual bw  : 0.35 
#> Selected error scale  : 0.25 
#> Adaptive rho range    : [0.05, 0.9] 
#> Selected rho decay    : 1 
#> Raw log score         : 2.76401 
#> Stabilized log score  : 2.90505 
#> 
#> Current anchor probabilities:
#>                 max               q0_95               q0_90 regression_forecast 
#>              0.7564              0.1012              0.0840              0.0299 
#>               q0_75                mean              median               q0_25 
#>              0.0175              0.0087              0.0016              0.0007 
#>               q0_10               q0_05                 min 
#>              0.0000              0.0000              0.0000

The printed object gives the selected anchors, gate settings, adaptive persistence range, diagnostic score, and current anchor probabilities. That last part is the quick read: which rolling summaries are carrying probability right now.

3. Read the anchor mix

The current probabilities live on the fitted object:

round(sort(fit$current$probabilities, decreasing = TRUE), 4)
#>                 max               q0_95               q0_90 regression_forecast 
#>              0.7564              0.1012              0.0840              0.0299 
#>               q0_75                mean              median               q0_25 
#>              0.0175              0.0087              0.0016              0.0007 
#>               q0_10               q0_05                 min 
#>              0.0000              0.0000              0.0000

If regression_forecast is high, recent direction is doing useful work. If median or inner quantiles dominate, the series is behaving more like a stable level. If min, max, or tail quantiles get weight, the model has found a state where boundaries helped historically.

This is one of the main reasons to like anchor models: the forecast is not only a number. It has a small audit trail.

4. Forecast paths, not just a line

predict() recursively simulates the all-anchor mixture. The summary gives means, medians, standard deviations, and requested quantiles by horizon.

pred <- predict(
  fit,
  horizon = 12,
  nsim = 800,
  seed = 123
)

head(pred$summary)
#>   horizon     mean   median       sd   q0_010   q0_025   q0_050   q0_100
#> 1       1 113.2537 113.6613 1.184922 108.3126 108.7232 110.4791 112.0872
#> 2       2 113.1456 113.7000 1.503952 108.4076 108.7997 109.2807 110.7217
#> 3       3 113.0651 113.7592 1.746056 107.7525 108.5165 109.3431 109.8990
#> 4       4 112.9863 113.7786 1.935527 106.9662 108.4269 108.9380 109.8479
#> 5       5 112.8958 113.8033 2.106903 106.0501 108.2365 108.6462 109.6781
#> 6       6 112.7724 113.7912 2.294568 105.4170 107.3765 108.5208 109.4555
#>     q0_250   q0_500   q0_750   q0_900   q0_950   q0_975   q0_990
#> 1 113.2056 113.6613 113.8948 114.0800 114.2151 114.3147 114.4008
#> 2 112.9683 113.7000 114.0073 114.2529 114.4071 114.5281 114.6657
#> 3 112.4328 113.7592 114.1403 114.4413 114.5960 114.7691 114.8798
#> 4 112.4930 113.7786 114.2434 114.5684 114.7382 114.9174 115.0900
#> 5 111.7207 113.8033 114.2899 114.6712 114.8870 115.0485 115.3593
#> 6 111.1796 113.7912 114.3503 114.7743 115.0366 115.2415 115.5216

The prediction object also returns small distribution functions:

c(
  density_at_113 = pred$dfun(113, h = 1),
  probability_le_113 = pred$pfun(113, h = 1)
)
#>     density_at_113 probability_le_113 
#>          0.2042424          0.2371789

pred$qfun(c(0.05, 0.50, 0.95), h = 1)
#> [1] 110.4791 113.6613 114.2151
pred$rfun(5, h = 1)
#> [1] 109.5176 112.7127 113.8637 114.2933 112.7622

Calling rfun() without h returns coherent recursive paths rather than independent marginal draws:

paths <- pred$rfun(20)
dim(paths)
#> [1] 20 12

That is the difference between twelve separate one-step stories and twenty possible futures that remember where they came from.

5. Fixed mode versus search mode

The interface has one main rule: scalar value = fixed input; vector of values = causal validation search.

For a fast fixed run, pass scalar hyperparameters:

fixed <- rollcast(
  y,
  window = 45,
  tau = 0.25,
  lambda = 0.01,
  conditional_k = 20,
  state_bw = 1,
  residual_bw = 0.35,
  error_scale = 0.25,
  residual_smoothing = 0.03,
  rho_min = 0.05,
  rho_max = 0.90,
  rho_decay = 1,
  min_history = 15,
  maxit = 120
)

fixed$diagnostics$stabilized_log_score
#> [1] 2.905047

To let the model choose among a few sensible options, pass candidate vectors:

tuned <- rollcast(
  y,
  window = c(35, 45),
  tau = c(0.20, 0.35),
  lambda = 0.01,
  conditional_k = c(10, 20),
  state_bw = 1,
  residual_bw = c(0.25, 0.45),
  error_scale = c(0, 0.25),
  residual_smoothing = 0.03,
  rho_min = 0.05,
  rho_max = 0.90,
  rho_decay = c(0.75, 1.50),
  min_history = 15,
  search_maxit = 80,
  maxit = 120,
  verbose = FALSE
)

tuned$hyperparameter_search$selected
#> $window
#> [1] 45
#> 
#> $error_scale
#> [1] 0.25
#> 
#> $tau
#> [1] 0.35
#> 
#> $residual_bw
#> [1] 0.45
#> 
#> $conditional_k
#> [1] 20
#> 
#> $state_bw
#> [1] 1
#> 
#> $lambda
#> [1] 0.01
#> 
#> $rho_decay
#> [1] 1.5
#> 
#> $residual_smoothing
#> [1] 0.03
#> 
#> $rho_min
#> [1] 0.05
#> 
#> $rho_max
#> [1] 0.9
tuned$hyperparameter_search$validation_log_score
#> [1] 3.708636

The search is a cached coordinate search on common causal validation origins, not a full Cartesian grid. Translation: it tries to be useful without turning your quick experiment into a calendar event.

6. Residual texture: pure anchors or local shape

Set error_scale = 0 for a pure weighted-anchor forecast:

anchor_only <- rollcast(
  y,
  window = 45,
  tau = 0.25,
  lambda = 0.01,
  conditional_k = 20,
  state_bw = 1,
  residual_bw = 0.35,
  error_scale = 0,
  residual_smoothing = 0.03,
  rho_min = 0.05,
  rho_max = 0.90,
  rho_decay = 1,
  min_history = 15,
  maxit = 120
)

anchor_only$error_scale
#> [1] 0
round(sort(anchor_only$current$probabilities, decreasing = TRUE), 4)
#>                 max               q0_95               q0_90 regression_forecast 
#>              0.7228              0.1856              0.0406              0.0246 
#>               q0_75                mean              median               q0_25 
#>              0.0135              0.0099              0.0026              0.0005 
#>               q0_10               q0_05                 min 
#>              0.0000              0.0000              0.0000

Use error_scale between zero and one when anchors should keep their location role but nearby historical residuals should add shape around them. Zero is crisp. One is full residual correction. The middle is often where the forecast behaves like it has both a steering wheel and suspension.

7. Plot the forecast

The default plot is a compact diagnostic dashboard:

plot(pred)

The diagnostic view combines four checks:

  1. predictive fan;
  2. absolute uncertainty width and transition horizon;
  3. adaptive gating persistence;
  4. median forecast drift from the last observation.

When rollcast is a good fit

rollcast is most natural when you have a univariate series, want probabilistic output, and do not want a single rigid structural assumption to own the whole forecast. It is especially useful when rolling summaries are meaningful and the recent state should influence which summary gets trusted.

It is not magic. It is a compact probabilistic control room: anchors suggest locations, the gate allocates attention, residuals add local texture, and recursive simulation turns the result into paths.

Fit, inspect, simulate, plot. Then let the distribution do the talking.

Enzoi!