--- title: "Uncertainty and Scenario Forecasting" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Uncertainty and Scenario Forecasting} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r, include = FALSE} knitr::opts_chunk$set( collapse = TRUE, comment = "#>", fig.width = 6, fig.height = 3.5 ) set.seed(2024) library(bridgr) ``` # Overview `bridgr` can return point forecasts only, or it can also compute coefficient uncertainty for the fitted target equation and prediction intervals for forecasts. The relevant estimation arguments are: - `se = FALSE`: point forecasts only. - `se = TRUE`: compute HAC or Delta-HAC coefficient uncertainty and prediction intervals. - `bootstrap = list(N = 100, block_length = NULL)`: control the number of predictive simulation paths. `block_length` is only used when `full_system_bootstrap = TRUE`, where it sets the size of the contiguous target-period blocks resampled in each bootstrap draw — larger blocks preserve more temporal dependence. - `full_system_bootstrap = TRUE`: replace the default residual-resampling prediction intervals and HAC / Delta-HAC coefficient standard errors with a full-system target-period block bootstrap. Because this refits the full mixed-frequency workflow on every draw, it can be substantially slower. # Fitting a Model with Default Uncertainty ```{r bootstrap-model} gdp_growth <- suppressMessages(tsbox::ts_na_omit(tsbox::ts_pc(gdp))) boot_model <- mf_model( target = gdp_growth, indic = baro, indic_predict = "auto.arima", indic_aggregators = "mean", indic_lags = 1, target_lags = 1, h = 2, se = TRUE, bootstrap = list(N = 20, block_length = NULL) ) ``` `bridgr` computes HAC standard errors for the linear target equation, or Delta-HAC standard errors when parametric aggregation weights are estimated jointly. Forecast uncertainty is obtained by simulating from resampled centered residuals of the fitted target equation. # Forecast Output Once the model has been estimated with `se = TRUE`, `forecast()` returns a standardized forecast object with: - `mean` - `se` - `lower` - `upper` - `forecast_set` - uncertainty metadata ```{r forecast-output} fc <- forecast(boot_model) fc fc$bootstrap ``` The intervals are empirical prediction intervals based on the stored residual-resampling forecast draws. # Reporting Only Forecasts Below a Width Tolerance In practice prediction intervals widen with horizon. A common reporting choice is to publish only forecast horizons whose interval is narrower than some application-specific tolerance — for example, only publish when the 95% half-width is below a chosen threshold. The example below applies that rule with a tolerance of `1.5` percentage points. ```{r width-tolerance} forecast_table <- dplyr::tibble( time = fc$time, mean = as.numeric(fc$mean), lower_95 = fc$lower[, "95%"], upper_95 = fc$upper[, "95%"] ) |> dplyr::mutate( half_width_95 = (.data$upper_95 - .data$lower_95) / 2 ) tolerance <- 1.5 dplyr::filter(forecast_table, .data$half_width_95 <= tolerance) ``` Tighten or relax `tolerance` depending on how much uncertainty is acceptable in the use case. # Summary Output The same uncertainty configuration also feeds into `summary()`. ```{r summary-output} summary(boot_model) ``` The printed summary keeps the same base layout as a point-estimate model and adds the uncertainty section only when uncertainty output is available. # Scenario Forecasting with `xreg` If you want to forecast the target under a different future regressor path, pass a custom `xreg` object to `forecast()`. The custom regressor names must match the ones used in the fitted target equation. The example below defines a *baseline* scenario (the indicator continues on its model-implied path) and a *negative shock* scenario (the indicator drops by 5 points relative to baseline). Both scenarios share the fitted target equation and the same uncertainty method. ```{r scenarios} # `model.frame(which = "forecast")` returns the regressor path the baseline # forecast is built from; shifting it is all a scenario needs. baseline_xreg <- model.frame(boot_model, which = "forecast") xreg_names <- variable.names(boot_model, which = "xreg") make_xreg <- function(level_shift) { dplyr::tibble( id = rep(xreg_names, each = nrow(baseline_xreg)), time = rep(baseline_xreg$time, times = length(xreg_names)), value = c( baseline_xreg$baro + level_shift, baseline_xreg$baro_lag1 + level_shift ) ) } fc_baseline <- forecast(boot_model, xreg = make_xreg(0)) fc_shock <- forecast(boot_model, xreg = make_xreg(-5)) scenario_df <- dplyr::bind_rows( dplyr::tibble( scenario = "baseline", time = fc_baseline$time, mean = as.numeric(fc_baseline$mean), lower = fc_baseline$lower[, "95%"], upper = fc_baseline$upper[, "95%"] ), dplyr::tibble( scenario = "shock (-5)", time = fc_shock$time, mean = as.numeric(fc_shock$mean), lower = fc_shock$lower[, "95%"], upper = fc_shock$upper[, "95%"] ) ) ggplot2::ggplot( scenario_df, ggplot2::aes(x = .data$time, color = .data$scenario, fill = .data$scenario) ) + ggplot2::geom_ribbon( ggplot2::aes(ymin = .data$lower, ymax = .data$upper), alpha = 0.2, color = NA ) + ggplot2::geom_line(ggplot2::aes(y = .data$mean), linewidth = 0.8) + ggplot2::geom_point(ggplot2::aes(y = .data$mean), size = 2) + ggplot2::labs( title = "Forecast under baseline and shock scenarios", x = NULL, y = "GDP growth forecast" ) + theme_bridgr() ``` Scenario forecasts reuse the same uncertainty method and evaluate it on the supplied regressor path. # Optional Full-System Bootstrap If you want to propagate uncertainty through the full mixed-frequency workflow, including indicator completion and aggregation, set `full_system_bootstrap = TRUE`. This can be substantially slower than the default residual-resampling intervals because each bootstrap draw re-estimates the full pipeline. In that mode, both the reported coefficient standard errors and the forecast intervals are based on the bootstrap draws. ```{r full-system-bootstrap} full_model <- mf_model( target = gdp_growth, indic = baro, indic_predict = "auto.arima", indic_aggregators = "mean", indic_lags = 1, target_lags = 1, h = 2, se = TRUE, full_system_bootstrap = TRUE, bootstrap = list(N = 20, block_length = NULL) ) forecast(full_model)$bootstrap ``` # Point Forecasts Only If you are only interested in point estimates, leave `se = FALSE`. In that case, `bootstrap` is ignored and `forecast()` still returns the same object shape, with `NA` uncertainty fields. ```{r point-only} point_model <- mf_model( target = gdp_growth, indic = baro, indic_predict = "auto.arima", indic_aggregators = "mean", indic_lags = 1, target_lags = 1, h = 1, se = FALSE, bootstrap = list(N = 20) ) forecast(point_model) ``` # Interpretation With the default `se = TRUE`, intervals are residual-resampling prediction intervals for the fitted target equation, and coefficient standard errors come from HAC or Delta-HAC estimation. When `full_system_bootstrap = TRUE`, both the coefficient standard errors and the forecast intervals come from a full-system block bootstrap that resamples target-period blocks from the aligned mixed-frequency system, refits the workflow on each draw, and evaluates the resulting future paths.