--- title: "Ragged-Edge Nowcasting with bridgr" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Ragged-Edge Nowcasting with bridgr} %\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(7) library(bridgr) ``` # Why Ragged Edges Matter Mixed-frequency nowcasting usually happens at the ragged edge: the lower-frequency target has not been released yet, and the higher-frequency indicators are only partially observed for the current target period. `bridgr` handles that situation through `indic_predict`, which controls how the missing high-frequency tail is completed before the target equation is estimated or forecast. The currently supported options are: - `"last"`: extend the latest available high-frequency observation. - `"mean"`: extend the mean of the latest available high-frequency block. - `"auto.arima"`: fit an automatic ARIMA model to each indicator and forecast the missing observations. - `"ets"`: fit an ETS model to each indicator and forecast the missing observations. - `"direct"`: do not forecast the indicator at all. Instead, align the latest observed complete high-frequency blocks directly to the target periods and average them within each target period. # A Ragged-Edge Example We start from the package's quarterly GDP growth series and monthly barometer, then remove the last quarterly GDP observation and the last two monthly indicator observations. That creates a clean nowcast setup: the final quarter is now forecasted rather than estimated, and the monthly indicator is only partially observed for that forecast quarter. ```{r data} gdp_growth <- suppressMessages(tsbox::ts_na_omit(tsbox::ts_pc(gdp))) gdp_nowcast <- gdp_growth |> dplyr::slice_head(n = nrow(gdp_growth) - 1) baro_ragged <- baro |> dplyr::slice_head(n = nrow(baro) - 2) tail(gdp_growth) tail(gdp_nowcast) tail(baro_ragged) ``` The target history now ends one quarter earlier, while the monthly indicator contains only a partial block for the quarter we want to nowcast. # Comparing Indicator Completion Rules ```{r compare-methods} predict_methods <- c("last", "mean", "auto.arima", "ets") models <- lapply( predict_methods, function(method) { mf_model( target = gdp_nowcast, indic = baro_ragged, indic_predict = method, indic_aggregators = "mean", target_lags = 1, h = 1 ) } ) names(models) <- predict_methods forecast_df <- dplyr::bind_rows( lapply(predict_methods, function(method) { fc <- forecast(models[[method]]) dplyr::tibble( method = method, forecast_time = fc$time, forecast = as.numeric(fc$mean) ) }) ) forecast_df ``` The model specification is the same in each case. The only difference is how the missing monthly observations are completed before the indicator is aggregated to the quarterly frequency. ## Visualizing the completed indicator paths The plot below shows the tail of the observed monthly indicator together with the two extrapolated months that each `indic_predict` rule produces for the partially observed forecast quarter. ```{r extension-plot} last_obs <- max(baro_ragged$time) last_obs_value <- baro_ragged$values[baro_ragged$time == last_obs] future_months <- seq(last_obs, by = "month", length.out = 3)[-1] observed_tail <- baro_ragged |> dplyr::slice_tail(n = 9) |> dplyr::transmute( time = .data$time, value = .data$values, method = "observed" ) extended <- dplyr::bind_rows( lapply(predict_methods, function(method) { indicator_tbl <- models[[method]]$indic extension <- indicator_tbl |> dplyr::filter(.data$time %in% future_months) |> dplyr::transmute( time = .data$time, value = .data$values, method = method ) # join to the last observed point for a continuous line dplyr::bind_rows( dplyr::tibble(time = last_obs, value = last_obs_value, method = method), extension ) }) ) ggplot2::ggplot( dplyr::bind_rows(observed_tail, extended), ggplot2::aes(x = .data$time, y = .data$value, color = .data$method) ) + ggplot2::geom_line(linewidth = 0.8) + ggplot2::geom_point(size = 1.6) + ggplot2::labs( title = "Indicator completion at the ragged edge", x = NULL, y = "KOF barometer", color = NULL ) + theme_bridgr() ``` The differences across rules are exactly what their names suggest: `"last"` holds the last value flat, `"mean"` extends the mean of the recent block, and `auto.arima` / `ets` produce model-based paths. # Direct Alignment `"direct"` takes a different route. The indicator is not forecasted. Instead, `bridgr` assigns the latest complete high-frequency blocks *backward* to the target periods and averages them within each target period. ```{r direct} direct_model <- mf_model( target = gdp_nowcast, indic = baro_ragged, indic_predict = "direct", h = 1 ) forecast(direct_model) ``` This is especially useful when you want to avoid a separate indicator forecasting step and prefer to work only with observed high-frequency data. # Explicit Missing Values Real-time indicator data sometimes contain explicit `NA` cells inside the sample, for example because of late releases or reporting holidays. Implicit ragged edges (a shorter indicator tail with no `NA` cells) are always supported through `indic_predict`. By default `bridgr` errors on explicit `NA` values; the `missing` argument controls what to do instead. `missing = "impute"` interpolates linearly inside each series and is the most generally useful choice. The example below introduces a single internal gap and re-fits the model under both `"error"` (default, captured here) and `"impute"`. ```{r missing-impute, warning = TRUE} baro_internal_na <- baro_ragged internal_gap <- seq(nrow(baro_internal_na) - 14, nrow(baro_internal_na) - 13) baro_internal_na$values[internal_gap] <- NA_real_ invisible(tryCatch( mf_model( target = gdp_nowcast, indic = baro_internal_na, indic_predict = "last", indic_aggregators = "mean", h = 1 ), error = function(e) message("Error captured: ", conditionMessage(e)) )) imputed_model <- mf_model( target = gdp_nowcast, indic = baro_internal_na, indic_predict = "last", indic_aggregators = "mean", h = 1, missing = "impute" ) forecast(imputed_model) ``` `missing = "drop"` removes `NA` rows entirely and is useful when those rows do not break the per-period completeness required by the inferred frequency ladder — for example when they sit beyond the estimation sample. In most ragged-edge workflows `indic_predict` is the better mechanism for handling trailing gaps. # When to Use Which Option As a rough guide: - Use `"last"` when a simple carry-forward rule is acceptable. - Use `"mean"` when you want a stable deterministic fill based on the latest high-frequency block. - Use `"auto.arima"` or `"ets"` when the indicator has meaningful time-series structure and a separate extrapolation step is sensible. - Use `"direct"` when you want direct alignment without indicator forecasting and want to rely only on the latest observed complete high-frequency blocks. The good part is that the downstream workflow does not change. The same `summary()` and `forecast()` interface works across all of these nowcasting choices.