--- title: "Aggregation and Mixed-Frequency Modeling in bridgr" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Aggregation and Mixed-Frequency Modeling in 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(2024) library(bridgr) ``` # The Bridge-to-MIDAS Spectrum The key design choice in `bridgr` is that classic bridge equations, unrestricted mixed-frequency regressions, and parametric MIDAS-style models all share the same estimation, forecasting, and summary workflow. You can move between them without switching to a different API. `bridgr` supports several ways to map higher-frequency observations into a lower-frequency target equation. Those choices span a continuum: - Bridge-style deterministic aggregation: `"mean"`, `"last"`, `"sum"`, or a fixed numeric weight vector. - Unrestricted mixed-frequency regression: `"unrestricted"`. - Parametric MIDAS-style weighting: `"expalmon"` and `"beta"`. This vignette uses a simple monthly-to-quarterly simulation so the differences between these approaches are easy to see. # A Small Monthly-to-Quarterly Example ```{r simulation} n_quarters <- 40 quarter_index <- rep(seq_len(n_quarters), each = 3) slot <- rep(1:3, times = n_quarters) monthly_time <- seq( as.Date("2010-01-01"), by = "month", length.out = n_quarters * 3 ) monthly_indicator <- dplyr::tibble( time = monthly_time, value = 15 + quarter_index * 0.35 + ifelse(slot == 1, 0.8 * sin(quarter_index / 2), 0) + ifelse(slot == 2, -0.6 * cos(quarter_index / 3), 0) + ifelse(slot == 3, 0.7 * sin(quarter_index / 4 + 0.3), 0) ) quarter_time <- monthly_time[seq(1, length(monthly_time), by = 3)] quarter_target <- dplyr::tibble( time = quarter_time, value = 0.5 + vapply( seq_along(quarter_time), function(i) { block <- monthly_indicator$value[((i - 1) * 3 + 1):(i * 3)] 0.2 * block[[1]] + 0.6 * block[[2]] + 0.2 * block[[3]] }, numeric(1) ) + rep(c(0.1, -0.05, 0.08, -0.02), length.out = length(quarter_time)) ) ``` The data-generating process is intentionally driven more by the middle month of each quarter — the within-quarter weights are `(0.2, 0.6, 0.2)` — so we can see how the different aggregation schemes react. The monthly indicator also has slot-specific within-quarter movements, so the unrestricted specification can estimate three distinct monthly coefficients. # Deterministic Bridge Aggregation ```{r deterministic} mean_model <- mf_model( target = quarter_target, indic = monthly_indicator, indic_predict = "last", indic_aggregators = "mean", h = 1 ) last_model <- mf_model( target = quarter_target, indic = monthly_indicator, indic_predict = "last", indic_aggregators = "last", h = 1 ) summary(mean_model) ``` These are classic bridge-model choices. They are easy to interpret and often work well when you want a transparent rule for within-period aggregation. ## Fixed numeric weights If you already have a prior about the within-period shape, pass a numeric weight vector in a `list()` instead. Weights must have the right length for the inferred target-period block size and must sum to one. ```{r numeric-weights} fixed_model <- mf_model( target = quarter_target, indic = monthly_indicator, indic_predict = "last", indic_aggregators = list(c(0.1, 0.7, 0.2)), h = 1 ) stats::coef(fixed_model) ``` # Unrestricted Mixed-Frequency Regression ```{r unrestricted} unrestricted_model <- mf_model( target = quarter_target, indic = monthly_indicator, indic_predict = "last", indic_aggregators = "unrestricted", h = 1 ) stats::coef(unrestricted_model) ``` `"unrestricted"` estimates one coefficient for each within-quarter monthly observation. In a monthly-on-quarterly example this means three separate regressors. Notice that the recovered coefficients are close to the true `(0.2, 0.6, 0.2)` weights used in the simulated DGP — the second slot dominates, as designed. This example keeps `indic_predict = "last"` so the comparison isolates the aggregation choice. The ragged-edge vignette covers `indic_predict = "direct"`, which skips indicator forecasting and instead works from the latest observed complete high-frequency blocks. # Parametric MIDAS-Style Weighting `bridgr` also supports parametric weighting rules that estimate the within-period shape from the data while keeping the number of free parameters small. The `solver_options` list controls the optimizer — see `?mf_model` for the full set of available controls. ```{r parametric} expalmon_model <- mf_model( target = quarter_target, indic = monthly_indicator, indic_predict = "last", indic_aggregators = "expalmon", solver_options = list(seed = 123, n_starts = 1, maxiter = 100), h = 1 ) beta_model <- mf_model( target = quarter_target, indic = monthly_indicator, indic_predict = "last", indic_aggregators = "beta", solver_options = list( seed = 123, n_starts = 1, maxiter = 100, start_values = c(2, 2) ), h = 1 ) ``` The estimated weight profile is available through `weights()`, and the underlying parametric coefficients through `aggregation_parameters()`. Both accept an indicator name or position; `indicators()` lists the available names. ```{r weight-accessors} indicators(expalmon_model) weights(expalmon_model, indicator = 1) aggregation_parameters(expalmon_model, indicator = 1) ``` ```{r weight-comparison} equal_weights <- rep(1 / 3, 3) last_weights <- c(0, 0, 1) true_weights <- c(0.2, 0.6, 0.2) weights_df <- dplyr::bind_rows( dplyr::tibble(model = "mean", month = 1:3, weight = equal_weights), dplyr::tibble(model = "last", month = 1:3, weight = last_weights), dplyr::tibble(model = "true DGP", month = 1:3, weight = true_weights), dplyr::tibble(model = "expalmon", month = 1:3, weight = weights(expalmon_model, indicator = 1)), dplyr::tibble(model = "beta", month = 1:3, weight = weights(beta_model, indicator = 1)) ) ggplot2::ggplot( weights_df, ggplot2::aes(x = .data$month, y = .data$weight, color = .data$model) ) + ggplot2::geom_line(linewidth = 0.8) + ggplot2::geom_point(size = 2) + ggplot2::scale_x_continuous(breaks = 1:3) + ggplot2::labs( title = "Within-quarter weight profiles", x = "Month within quarter", y = "Weight" ) + theme_bridgr() ``` The parametric profiles concentrate mass on the middle month, matching the simulated DGP. The fixed `"mean"` rule spreads mass evenly across months and `"last"` puts all of it on the final month. # Forecast Comparison All of these models share the same downstream interface. ```{r forecast-comparison} forecasts_df <- dplyr::bind_rows( dplyr::tibble(model = "mean", forecast = as.numeric(forecast(mean_model)$mean)), dplyr::tibble(model = "last", forecast = as.numeric(forecast(last_model)$mean)), dplyr::tibble(model = "unrestricted", forecast = as.numeric(forecast(unrestricted_model)$mean)), dplyr::tibble(model = "expalmon", forecast = as.numeric(forecast(expalmon_model)$mean)), dplyr::tibble(model = "beta", forecast = as.numeric(forecast(beta_model)$mean)) ) ggplot2::ggplot( forecasts_df, ggplot2::aes(x = .data$model, y = .data$forecast, fill = .data$model) ) + ggplot2::geom_col(width = 0.6, show.legend = FALSE) + ggplot2::labs( title = "One-step-ahead forecast by aggregation strategy", x = NULL, y = "Forecast" ) + theme_bridgr() ``` # Picking an Aggregator on Out-of-Sample Performance A rough rule-of-thumb guide to aggregator choice is given at the end of this vignette. In practice the best way to choose is to evaluate candidate models on out-of-sample forecasts. The example below uses a short pseudo-real-time rolling-origin scheme on the simulated data, with one-quarter-ahead forecasts. ```{r evaluation, message = FALSE, warning = FALSE} origins <- (n_quarters - 13):(n_quarters - 4) methods <- c("mean", "unrestricted", "expalmon") eval_rows <- list() for (m in methods) { for (origin in origins) { train <- dplyr::slice(quarter_target, seq_len(origin)) truth <- quarter_target$value[[origin + 1]] fit <- mf_model( target = train, indic = monthly_indicator, indic_predict = "last", indic_aggregators = m, h = 1, solver_options = if (m == "expalmon") { list(seed = 1, n_starts = 1, maxiter = 100) } else { NULL } ) fc <- as.numeric(forecast(fit)$mean)[[1]] eval_rows[[length(eval_rows) + 1]] <- dplyr::tibble( method = m, origin = origin, forecast = fc, actual = truth ) } } eval_df <- dplyr::bind_rows(eval_rows) eval_summary <- eval_df |> dplyr::group_by(.data$method) |> dplyr::summarise( rmse = sqrt(mean((.data$forecast - .data$actual)^2)), mae = mean(abs(.data$forecast - .data$actual)), .groups = "drop" ) eval_summary ``` Even on this small example the unrestricted and parametric specifications recover the within-quarter shape and outperform the deterministic mean. For a real application you would typically use a longer evaluation window, multiple horizons, and richer scoring rules. # Choosing an Aggregation Strategy As a rough guide: - Use deterministic bridge aggregation like `"mean"`, `"last"`, or `"sum"` when you want a transparent and stable nowcasting rule. - Use `"unrestricted"` when the frequency gap is small and you want each within-period observation to have its own coefficient. - Use `"expalmon"` or `"beta"` when you want data-driven within-period weights but would like a more parsimonious parameterization than `"unrestricted"`. - Use `indic_predict = "direct"` when you want direct alignment based only on the latest observed complete high-frequency blocks. ## Related packages If you have used [`midasr`](https://CRAN.R-project.org/package=midasr), the closest `bridgr` analogues to its MIDAS regressors are `indic_aggregators = "expalmon"` and `indic_aggregators = "beta"`. The `"unrestricted"` aggregator plays a role similar to `midasr`'s U-MIDAS specification. Compared with [`midasml`](https://CRAN.R-project.org/package=midasml), which targets high-dimensional regularized mixed-frequency models, `bridgr` is aimed at lower-dimensional applied forecasting workflows.