Skip to contents

This vignette shows how to calculate and visualize marginal effects of covariates (Z) on the systematic component of coefficients (beta) using bayesm.HART. We compare marginal effects from a HART specification against a linear hierarchical specification.

Load Packages and Setup

We load bayesm.HART and other necessary packages for data manipulation and plotting.

Data Simulation

First, we simulate hierarchical multinomial logit data. The data generating process uses a nonlinear Friedman function linking Z to the systematic component of beta.

## ---- Dimensions ----
nlgt = 300      # num of cross sectional units
nT = 20         # num of periods (observations) per unit
p = 3           # num of choices (incl. outside option, j=0..p-1)
nz = 10         # num of characteristics variables in Z

## ---- Variables per choice alternative ----
nXa = 1         # num of alternative-specific variables
nXd = 1         # num of variables fixed across alternatives
const = TRUE   # Include intercepts?

## ---- Calculate total number of coefficients per unit (beta_i) ----
# This is calculated based on the parameters above
ncoef_calc <- const * (p - 1) + (p - 1) * nXd + nXa

## ---- Mixture components for individual heterogeneity ----
ncomp = 1       # num of mixture components for eps_i
sigma_inv_diag_val = 1   # Precision for eps_i draws (assuming diagonal Sigma)
## ---- Simulate Data using Friedman function ----
sim_output <- sim_hier_mnl(
    nlgt = nlgt,
    nT = nT,
    p = p,
    nz = nz,
    nXa = nXa,
    nXd = nXd,
    const = const,
    beta_func_type = "friedman", # Specify friedman
    beta_func_args = list(coefs = c(10, 20, 10, 1), shift = -30, scale = 10), # Pass the function arguments
    ncomp = ncomp,
    sigma_inv_diag = sigma_inv_diag_val, 
    seed = 1234 # Use seed from setup chunk
)

# Prepare data list in the format required by the estimation functions
Data1 <- list(p = sim_output$p, lgtdata = sim_output$lgtdata, Z = sim_output$Z)
true_values <- sim_output$true_values
Z <- sim_output$Z # Keep Z easily accessible
ncoef <- true_values$dimensions$ncoef # Get actual ncoef from output

# message(paste("Simulated data for", nlgt, "individuals."))
message(paste("Number of coefficients per individual (ncoef):", ncoef))

Model Estimation

We estimate two models:

  1. Linear Model: beta_i = Delta^\top Z_i + epsilon_i.
  2. HART Model: beta_i = Delta(Z_i) + epsilon_i.

We use reduced MCMC iterations for faster vignette execution.

Linear Model Estimation

## ---- MCMC Parameters (Reduced for Vignette) ----
R_linear = 2000  # Low iterations for vignette build speed
keep_linear = 10 # Keep every 10th draw
burn_linear = 1000 # Burn-in (applied to raw draws)

model_cache_file <- file.path(
  vignette_cache_dir,
  sprintf("mfx-models-R%s-%s-keep%s-%s.rds", R_linear, 2000, keep_linear, 10)
)

model_cache_ok <- FALSE
if (file.exists(model_cache_file)) {
  model_cache <- tryCatch(readRDS(model_cache_file), error = function(e) NULL)
  model_cache_ok <- !is.null(model_cache) &&
    !is.null(model_cache$out_linear) && !is.null(model_cache$out_bart)
  if (model_cache_ok) {
    out_linear <- model_cache$out_linear
    out_bart   <- model_cache$out_bart
    cat("Loaded cached model draws from:", model_cache_file, "\n")
  }
}
#> Loaded cached model draws from: C:\Users\twiem\AppData\Local/R/cache/R/bayesm.HART/vignettes/mfx-models-R2000-2000-keep10-10.rds

if (!model_cache_ok) {
  ## ---- Prior Specification ----
  Prior_linear = list(ncomp = 1) # Assumes single component for simplicity

  ## ---- Run MCMC Sampler ----
  out_linear = bayesm.HART::rhierMnlRwMixture(
      Data = Data1,
      Prior = Prior_linear,
      Mcmc = list(R = R_linear, 
                  keep = keep_linear, 
                  nprint = 0), # Suppress printing
      r_verbose = FALSE
  )
}
# Set the class attribute for S3 method dispatch
class(out_linear) <- "rhierMnlRwMixture" 
fit_linear_model <- TRUE

# Calculate burn-in based on *kept* draws for marginal effects later
burn_linear_kept <- ceiling(burn_linear / keep_linear) 
message("Linear model estimation complete.")

BART Model Estimation

## ---- MCMC Parameters (Reduced for Vignette) ----
R_bart = 2000       # Low iterations for vignette build speed
keep_bart = 10      # Keep every draw
burn_bart = 1000     # Burn-in (applied to raw draws) 

if (!model_cache_ok) {
  ## ---- Prior Specification (including BART hyperparameters) ----
  Prior_bart = list(
    ncomp = 1,
    bart = list(num_trees = 50, # Number of trees in the BART sum
                power = 2,       # Tree depth penalization
                base = 0.95,     # Tree depth penalization
                # Prior sd for terminal node params (scaled by num_trees)
                tau = 2 / sqrt(50), 
                numcut = 50,     # Max number of cutpoints per variable
                sparse = FALSE    # Use sparsity inducing prior?
               ) # BART list end
  ) # Prior list end

  ## ---- Run MCMC Sampler ----
  out_bart = bayesm.HART::rhierMnlRwMixture(
    Data = Data1,
    Prior = Prior_bart,
    Mcmc = list(R = R_bart, keep = keep_bart, nprint = 0),
    r_verbose = FALSE
  )

  saveRDS(
    list(out_linear = out_linear, out_bart = out_bart, generated_at = Sys.time()),
    model_cache_file
  )
  cat("Saved model draws cache to:", model_cache_file, "\n")
}

# Calculate burn-in based on *kept* draws for marginal effects later
burn_bart_kept <- ceiling(burn_bart / keep_bart) 
message("BART model estimation complete.")

Marginal Effects Calculation

We now calculate the marginal effect of one specific covariate (Z[3]) on one specific coefficient (beta_1). We define a grid of values for Z[3] while holding other covariates at their means.

## ---- Setup Parameters ----
# target_covariate_index_mfx <- 3     # Now defined inside loop
n_grid_points <- 10                # Number of points for the covariate grid
target_coef_index <- 1             # Coefficient index of beta to plot (beta_1)
ci_level_plot <- 0.95              # Credible interval level for plot
Z_mean <- t(as.matrix(colMeans(Data1$Z))) # Calculate Z_mean once

# Define the covariates we want to plot against
covariate_indices_to_plot <- c(2, 3, 4)

Now we loop through the selected covariates, calculate the marginal effects for each, and plot the results.

# Initialize an empty list to store the plots
plot_list <- list()

# Loop through each target covariate index
for (target_covariate_index_mfx in covariate_indices_to_plot) {
  
  message(paste0("\n--- Processing Marginal Effects for Z[", target_covariate_index_mfx, "] ---"))
  
  ## ---- Construct z_values Matrix for current covariate ----
  target_col <- Data1$Z[, target_covariate_index_mfx]
  grid_vec <- seq(min(target_col, na.rm = TRUE), 
                  max(target_col, na.rm = TRUE), 
                  length.out = n_grid_points)
                  
  z_values_loop <- matrix(rep(Z_mean, each=n_grid_points), 
                              nrow=n_grid_points, ncol=ncol(Data1$Z)) 
  z_values_loop[, target_covariate_index_mfx] <- grid_vec
  
  if (!is.null(colnames(Data1$Z))) {
      colnames(z_values_loop) <- colnames(Data1$Z)
  } else {
      colnames(z_values_loop) <- paste0("Z", 1:ncol(Data1$Z))
  }
  
  # Determine the name for the plot axis variable
  plot_var_name <- paste0("z_val_", target_covariate_index_mfx) 
  
  ## ---- Calculate Marginal Effects for current covariate ----
  mfx_bart_loop <- marginal_effects(
      object = out_bart,
      Z = Z_mean, 
      z_values = z_values_loop, 
      burn = burn_bart_kept, 
      verbose = FALSE
  )
  mfx_summary_bart_loop <- summary(mfx_bart_loop)
  
  mfx_linear_loop <- marginal_effects(
      object = out_linear,
      Z = Z_mean, 
      z_values = z_values_loop, 
      burn = burn_linear_kept, 
      verbose = FALSE
  )
  mfx_summary_linear_loop <- summary(mfx_linear_loop)
  
  ## ---- Plot Marginal Effects for current covariate ----
  plot_objects_loop <- list(BART = mfx_summary_bart_loop, Linear = mfx_summary_linear_loop)
  
  color_vals <- c("BART" = "blue", "Linear" = "red", "True" = "black")
  linetype_vals <- c("BART" = "solid", "Linear" = "solid", "True" = "dashed")
  fill_vals <- c("BART" = "blue", "Linear" = "red")
  fill_vals <- fill_vals[names(fill_vals) %in% names(plot_objects_loop)]

  plot_args_loop <- list(
      x = plot_objects_loop[[1]], 
      x_name = names(plot_objects_loop)[1], 
      plot_axis_var = plot_var_name, 
      coef_index = target_coef_index,
      ci_level = ci_level_plot,
      color_values = color_vals,
      linetype_values = linetype_vals,
      fill_values = fill_vals,
      xlab = paste("Value of Z[",target_covariate_index_mfx,"]", sep=""), 
      ylab = paste("Avg. Effect (Coefficient", target_coef_index, ")"),
      title = paste("Marginal Effect on Coefficient", target_coef_index, 
                    "vs Z[", target_covariate_index_mfx, "]", sep="")
  )
  plot_args_loop[[names(plot_objects_loop)[2]]] <- plot_objects_loop[[2]]
  
  base_plot_loop <- do.call(plot, plot_args_loop)
  
  ## ---- Calculate and Add True Marginal Effect Line ----
  true_avg_betabar_on_grid_loop <- sapply(grid_vec, function(grid_val) {
    Z_counterfactual <- Z_mean
    Z_counterfactual[, target_covariate_index_mfx] <- grid_val
    true_val_i <- tryCatch({
        bayesm.HART:::.beta_Z_friedman(
           Z_counterfactual[1, ], 
           coefs = c(10, 20, 10, 1), shift = -30, scale = 10) 
        }, error = function(e) { NA })
    return(true_val_i)
  })
  
  if (!anyNA(true_avg_betabar_on_grid_loop)){
      true_line_data_loop <- data.frame(
          plot_var_col = grid_vec, 
          true_mean = true_avg_betabar_on_grid_loop,
          model = "True"
      )
      names(true_line_data_loop)[1] <- plot_var_name 
  
      final_plot_loop <- base_plot_loop +
          geom_line(data = true_line_data_loop, 
                    aes(x = .data[[plot_var_name]], y = true_mean, 
                        color = .data$model, linetype = .data$model), 
                    linewidth = 1) +
          scale_color_manual(values = color_vals, name = "Marginal Effects") +
          scale_linetype_manual(values = linetype_vals, name = "Marginal Effects") + 
          scale_fill_manual(values = fill_vals, 
                            name = paste0(ci_level_plot * 100, "% Credible Interval"),
                            guide = guide_legend(override.aes = list(alpha = 0.2))) + 
          theme_classic(base_size = 14) +
          theme(legend.position = "bottom", legend.box = "vertical")
  } else {
      warning(paste0("True marginal effect line could not be calculated for Z[", 
                     target_covariate_index_mfx, "]. Plotting estimated effects only."))
      final_plot_loop <- base_plot_loop + theme_classic(base_size = 14) +
                    theme(legend.position = "bottom", legend.box = "vertical")
  }
  
  ## ---- Store Plot (don't print yet) ----
  plot_list[[paste0("Z", target_covariate_index_mfx)]] <- final_plot_loop
  # print(final_plot_loop) # Removed print from inside loop

} # End loop over covariate indices
#> 
#> --- Processing Marginal Effects for Z[2] ---
#> Scale for colour is already present.
#> Adding another scale for colour, which will replace the existing scale.
#> Scale for linetype is already present.
#> Adding another scale for linetype, which will replace the existing scale.
#> Scale for fill is already present.
#> Adding another scale for fill, which will replace the existing scale.
#> 
#> --- Processing Marginal Effects for Z[3] ---
#> 
#> Scale for colour is already present.
#> Adding another scale for colour, which will replace the existing scale.
#> Scale for linetype is already present.
#> Adding another scale for linetype, which will replace the existing scale.
#> Scale for fill is already present.
#> Adding another scale for fill, which will replace the existing scale.
#> 
#> --- Processing Marginal Effects for Z[4] ---
#> 
#> Scale for colour is already present.
#> Adding another scale for colour, which will replace the existing scale.
#> Scale for linetype is already present.
#> Adding another scale for linetype, which will replace the existing scale.
#> Scale for fill is already present.
#> Adding another scale for fill, which will replace the existing scale.
# --- Now print all stored plots --- 
message("\n--- Displaying Stored Plots ---")
#> 
#> --- Displaying Stored Plots ---
for (plot_name in names(plot_list)){
    message(paste("Plotting results for:", plot_name))
    print(plot_list[[plot_name]])
}
#> Plotting results for: Z2
#> Plotting results for: Z3
plot of chunk display-mfx-plots
plot of chunk display-mfx-plots
#> Plotting results for: Z4

plot of chunk display-mfx-plotsplot of chunk display-mfx-plots

These plots show estimated average effects of Z[2], Z[3], and Z[4] on the target coefficient (beta_1) across each covariate range. In this simulation setup, the HART curve (blue) tracks the nonlinear true curve (dashed black) more closely than the linear specification (red) over the displayed grid. Shaded regions are posterior credible intervals for estimated effects.