Skip to contents

Overview

The imuGAP package estimates the underlying vaccination process model across nested population hierarchies (e.g. a state divided into counties, counties into schools) and uses the fitted model to predict coverage across resolution levels. A key strength of imuGAP is support for flexible location layers: the process model dynamically scales its hierarchical depth to match the population structure you provide. If you supply only aggregate state-wide data, the model estimates a 1-layer structure; adding counties expands it to 2 layers; adding schools creates a 3-layer hierarchy, and so forth.

Whatever the data resolution, imuGAP canonicalizes the population tree, maps parent-child relationships across layers, and estimates hierarchical location offset parameters for each resolution level (beyond fully aggregated populations).

This vignette demonstrates:

  1. Exploring arbitrary location hierarchies and verifying layer depths.
  2. Constructing models for 1-layer (state-level only), 2-layer (state and county), and 3-layer (state, county, and school) datasets.
  3. Fitting and comparing parameter recovery and projected cohort trajectories across different layer resolutions.

The Example Location Hierarchy

Let’s first inspect the full hierarchy available in locations_sim:

Scruggs County Simone County Watson County
Chickadee Elementary Egret Elementary Meadowlark School
Nuthatch Academy Cardinal Academy Goldfinch Elementary
Blue Heron School Bunting School Mockingbird Academy
Flycatcher Elementary Tanager Academy Kinglet Learning Center
Bluebird Learning Center Oriole Youth Academy Vireo School
Catbird Academy Grosbeak Learning Center Kingfisher Academy
Finch Elementary Junco Elementary Cormorant Elementary
Sparrow School
Towhee Children’s Academy
Warbler Elementary

Most of our examples focus on using the higher resolution dataset (i.e. including 3 layers, or down to schools). For this demonstration, let’s create some alternative location data corresponding to different data availability. Here, we’re using the convenient layer information created by canonicalize_locations, but in real applications you would of course only have the locations you have. These filtered views simulate having high to low resolution data, correspond to all the locations in the table (locs_3layer), the state and county locations (locs_2layer), or just the state (locs_1layer).

locs_3layer <- canonicalize_locations(locations_sim) # all the data
locs_2layer <- locs_3layer[layer <= 2] # state and county only
locs_1layer <- locs_3layer[layer <= 1] # state only

Similarly, you can filter observations and associated metadata (i.e. populations) to match each location resolution:

data("observations_sim", package = "imuGAP")
data("populations_sim", package = "imuGAP")
data("latent_params_sim", package = "imuGAP")

# 3-Layer views (State, County, School)
# 1. Filter populations to locations present in locs_3layer
pops_3layer <- populations_sim[loc_id %in% locs_3layer$loc_id]
# 2. Extract relevant observations corresponding to filtered populations
obs_3layer <- observations_sim[obs_id %in% pops_3layer$obs_id]
# 3. Confirm extracted observations don't have unpresent locations in original population data
stopifnot(!populations_sim[obs_id %in% obs_3layer$obs_id, any(!loc_id %in% locs_3layer$loc_id)])

# 2-Layer views (State and County)
# 1. Filter populations to locations present in locs_2layer
pops_2layer <- populations_sim[loc_id %in% locs_2layer$loc_id]
# 2. Extract relevant observations corresponding to filtered populations
obs_2layer <- observations_sim[obs_id %in% pops_2layer$obs_id]
# 3. Confirm extracted observations don't have unpresent locations in original population data
stopifnot(!populations_sim[obs_id %in% obs_2layer$obs_id, any(!loc_id %in% locs_2layer$loc_id)])

# 1-Layer views (State only)
# 1. Aggregate and filter populations to State level (locs_1layer)
pops_1layer <- copy(populations_sim)[, loc_id := "State"]
pops_1layer <- pops_1layer[, .(weight = sum(weight)), by = .(obs_id, loc_id, cohort, age, dose)]
pops_1layer <- pops_1layer[loc_id %in% locs_1layer$loc_id]
# 2. Extract relevant observations corresponding to filtered populations
obs_1layer <- observations_sim[obs_id %in% pops_1layer$obs_id]
# 3. Confirm extracted observations don't have unpresent locations in population data
stopifnot(!pops_1layer[obs_id %in% obs_1layer$obs_id, any(!loc_id %in% locs_1layer$loc_id)])

2. Fitting the Model

To fit the model at different resolutions looks basically the same: just use 1-layer, 2-layer, or 3-layer location inputs. Note that no other setting tweaks are required, but these can take some time to run; if following along, you probably want to load the results from the package data.

st_opts <- stan_options(iter = 1000, chains = 4, seed = 1L)

# various layer fits
fit_3layer <- sampling(obs_3layer, pops_3layer, locs_3layer, stan_opts = st_opts)
fit_2layer <- sampling(obs_2layer, pops_2layer, locs_2layer, stan_opts = st_opts)
fit_1layer <- sampling(obs_1layer, pops_1layer, locs_1layer, stan_opts = st_opts)

To load precomputed results, get the following items from package data:

data("fit_sim", package = "imuGAP")
data("fit_sim_2layer", package = "imuGAP")
data("fit_sim_1layer", package = "imuGAP")

fit_sim_2layer
#> An imuGAP model fit (`imugap_fit`):
#>   Hierarchy:    4 locations across 2 layers (root: 'State')
#>   Observations: 841 total (775 uncensored, 66 right-censored)
#> 
#> Inference for Stan model: bspline_static_zero_halley2_logit.
#> 4 chains, each with iter=125; warmup=0; thin=1; 
#> post-warmup draws per chain=125, total post-warmup draws=500.
#> 
#>                       mean se_mean   sd      2.5%       25%       50%       75%
#> beta_bs[1]           -1.67    0.00 0.03     -1.74     -1.69     -1.67     -1.64
#> beta_bs[2]           -1.88    0.00 0.05     -2.00     -1.92     -1.88     -1.84
#> beta_bs[3]           -2.47    0.00 0.09     -2.64     -2.53     -2.47     -2.41
#> beta_bs[4]           -3.23    0.00 0.11     -3.47     -3.30     -3.23     -3.15
#> beta_bs[5]           -2.61    0.00 0.09     -2.80     -2.67     -2.61     -2.55
#> sigma_layer[1]        0.66    0.02 0.33      0.24      0.41      0.58      0.85
#> lambda_raw[1]         1.01    0.00 0.03      0.96      0.99      1.01      1.03
#> lambda_raw[2]         1.01    0.00 0.01      0.98      1.00      1.01      1.02
#> raw_phi_root[1]      -1.67    0.00 0.03     -1.74     -1.69     -1.67     -1.64
#> raw_phi_root[2]      -1.71    0.00 0.03     -1.77     -1.73     -1.71     -1.70
#> raw_phi_root[3]      -1.76    0.00 0.02     -1.81     -1.78     -1.76     -1.74
#> raw_phi_root[4]      -1.81    0.00 0.02     -1.86     -1.83     -1.81     -1.79
#> raw_phi_root[5]      -1.86    0.00 0.02     -1.91     -1.88     -1.86     -1.84
#> raw_phi_root[6]      -1.92    0.00 0.03     -1.97     -1.93     -1.92     -1.90
#> raw_phi_root[7]      -1.97    0.00 0.03     -2.02     -1.99     -1.97     -1.95
#> raw_phi_root[8]      -2.03    0.00 0.03     -2.08     -2.05     -2.03     -2.01
#> raw_phi_root[9]      -2.09    0.00 0.03     -2.14     -2.11     -2.09     -2.07
#> raw_phi_root[10]     -2.15    0.00 0.03     -2.21     -2.17     -2.15     -2.13
#> raw_phi_root[11]     -2.21    0.00 0.03     -2.27     -2.23     -2.21     -2.19
#> raw_phi_root[12]     -2.28    0.00 0.03     -2.34     -2.30     -2.28     -2.25
#> raw_phi_root[13]     -2.34    0.00 0.03     -2.41     -2.36     -2.34     -2.32
#> raw_phi_root[14]     -2.41    0.00 0.04     -2.48     -2.43     -2.41     -2.38
#> raw_phi_root[15]     -2.48    0.00 0.04     -2.55     -2.50     -2.48     -2.45
#> raw_phi_root[16]     -2.55    0.00 0.04     -2.62     -2.57     -2.55     -2.52
#> raw_phi_root[17]     -2.62    0.00 0.04     -2.70     -2.64     -2.62     -2.59
#> raw_phi_root[18]     -2.68    0.00 0.04     -2.77     -2.71     -2.68     -2.65
#> raw_phi_root[19]     -2.75    0.00 0.05     -2.84     -2.78     -2.75     -2.71
#> raw_phi_root[20]     -2.81    0.00 0.05     -2.91     -2.84     -2.81     -2.77
#> raw_phi_root[21]     -2.86    0.00 0.06     -2.97     -2.89     -2.86     -2.82
#> raw_phi_root[22]     -2.90    0.00 0.06     -3.02     -2.93     -2.90     -2.86
#> raw_phi_root[23]     -2.92    0.00 0.06     -3.05     -2.96     -2.92     -2.88
#> raw_phi_root[24]     -2.94    0.00 0.06     -3.06     -2.98     -2.94     -2.90
#> raw_phi_root[25]     -2.94    0.00 0.06     -3.06     -2.98     -2.93     -2.90
#> raw_phi_root[26]     -2.92    0.00 0.06     -3.04     -2.95     -2.91     -2.88
#> raw_phi_root[27]     -2.88    0.00 0.06     -3.00     -2.91     -2.87     -2.84
#> raw_phi_root[28]     -2.81    0.00 0.06     -2.94     -2.85     -2.81     -2.77
#> raw_phi_root[29]     -2.73    0.00 0.07     -2.87     -2.77     -2.72     -2.68
#> raw_phi_root[30]     -2.61    0.00 0.09     -2.80     -2.67     -2.61     -2.55
#> mu_offset[1,1]       -0.04    0.00 0.00     -0.05     -0.04     -0.04     -0.04
#> mu_offset[2,1]       -0.04    0.00 0.00     -0.05     -0.04     -0.04     -0.04
#> mu_offset[3,1]       -0.04    0.00 0.00     -0.05     -0.05     -0.04     -0.04
#> mu_offset[4,1]       -0.04    0.00 0.00     -0.05     -0.05     -0.04     -0.04
#> mu_offset[5,1]       -0.04    0.00 0.00     -0.05     -0.05     -0.04     -0.04
#> mu_offset[6,1]       -0.05    0.00 0.00     -0.06     -0.05     -0.05     -0.04
#> mu_offset[7,1]       -0.05    0.00 0.00     -0.06     -0.05     -0.05     -0.04
#> mu_offset[8,1]       -0.05    0.00 0.00     -0.06     -0.05     -0.05     -0.04
#> mu_offset[9,1]       -0.05    0.00 0.01     -0.06     -0.05     -0.05     -0.04
#> mu_offset[10,1]      -0.05    0.00 0.01     -0.06     -0.05     -0.05     -0.04
#> mu_offset[11,1]      -0.05    0.00 0.01     -0.06     -0.05     -0.05     -0.05
#> mu_offset[12,1]      -0.05    0.00 0.01     -0.06     -0.05     -0.05     -0.05
#> mu_offset[13,1]      -0.05    0.00 0.01     -0.06     -0.05     -0.05     -0.05
#> mu_offset[14,1]      -0.05    0.00 0.01     -0.06     -0.05     -0.05     -0.05
#> mu_offset[15,1]      -0.05    0.00 0.01     -0.06     -0.06     -0.05     -0.05
#> mu_offset[16,1]      -0.05    0.00 0.01     -0.06     -0.06     -0.05     -0.05
#> mu_offset[17,1]      -0.05    0.00 0.01     -0.06     -0.06     -0.05     -0.05
#> mu_offset[18,1]      -0.05    0.00 0.01     -0.07     -0.06     -0.05     -0.05
#> mu_offset[19,1]      -0.05    0.00 0.01     -0.07     -0.06     -0.05     -0.05
#> mu_offset[20,1]      -0.05    0.00 0.01     -0.07     -0.06     -0.05     -0.05
#> mu_offset[21,1]      -0.06    0.00 0.01     -0.07     -0.06     -0.05     -0.05
#> mu_offset[22,1]      -0.06    0.00 0.01     -0.07     -0.06     -0.06     -0.05
#> mu_offset[23,1]      -0.06    0.00 0.01     -0.07     -0.06     -0.06     -0.05
#> mu_offset[24,1]      -0.06    0.00 0.01     -0.07     -0.06     -0.06     -0.05
#> mu_offset[25,1]      -0.06    0.00 0.01     -0.07     -0.06     -0.06     -0.05
#> mu_offset[26,1]      -0.06    0.00 0.01     -0.07     -0.06     -0.06     -0.05
#> mu_offset[27,1]      -0.06    0.00 0.01     -0.07     -0.06     -0.06     -0.05
#> mu_offset[28,1]      -0.05    0.00 0.01     -0.07     -0.06     -0.05     -0.05
#> mu_offset[29,1]      -0.05    0.00 0.01     -0.07     -0.06     -0.05     -0.05
#> mu_offset[30,1]      -0.05    0.00 0.01     -0.06     -0.06     -0.05     -0.05
#> lp__             -79949.37    0.10 2.37 -79955.03 -79950.67 -79949.02 -79947.67
#>                      97.5% n_eff Rhat
#> beta_bs[1]           -1.60  1079 1.00
#> beta_bs[2]           -1.78   863 1.00
#> beta_bs[3]           -2.29   853 1.00
#> beta_bs[4]           -3.02   728 1.00
#> beta_bs[5]           -2.42  1049 1.00
#> sigma_layer[1]        1.47   390 1.01
#> lambda_raw[1]         1.06  1220 1.00
#> lambda_raw[2]         1.04   899 1.00
#> raw_phi_root[1]      -1.60  1079 1.00
#> raw_phi_root[2]      -1.66  1081 1.00
#> raw_phi_root[3]      -1.72  1009 1.00
#> raw_phi_root[4]      -1.77   950 1.00
#> raw_phi_root[5]      -1.82   932 1.00
#> raw_phi_root[6]      -1.87   946 1.00
#> raw_phi_root[7]      -1.92   978 1.00
#> raw_phi_root[8]      -1.98  1021 1.00
#> raw_phi_root[9]      -2.04  1062 1.00
#> raw_phi_root[10]     -2.10  1096 1.00
#> raw_phi_root[11]     -2.16  1119 1.00
#> raw_phi_root[12]     -2.22  1117 1.00
#> raw_phi_root[13]     -2.28  1101 1.00
#> raw_phi_root[14]     -2.34  1081 1.00
#> raw_phi_root[15]     -2.41  1057 1.00
#> raw_phi_root[16]     -2.47  1026 1.00
#> raw_phi_root[17]     -2.54   982 1.00
#> raw_phi_root[18]     -2.60   936 1.00
#> raw_phi_root[19]     -2.66   898 1.00
#> raw_phi_root[20]     -2.71   866 1.00
#> raw_phi_root[21]     -2.75   839 1.00
#> raw_phi_root[22]     -2.79   819 1.00
#> raw_phi_root[23]     -2.81   809 1.00
#> raw_phi_root[24]     -2.82   813 1.00
#> raw_phi_root[25]     -2.82   836 1.00
#> raw_phi_root[26]     -2.81   891 1.00
#> raw_phi_root[27]     -2.77   995 1.00
#> raw_phi_root[28]     -2.70  1118 1.00
#> raw_phi_root[29]     -2.59  1137 1.00
#> raw_phi_root[30]     -2.42  1049 1.00
#> mu_offset[1,1]       -0.03  1298 1.00
#> mu_offset[2,1]       -0.03  1297 1.00
#> mu_offset[3,1]       -0.03  1296 1.00
#> mu_offset[4,1]       -0.03  1296 1.00
#> mu_offset[5,1]       -0.04  1297 1.00
#> mu_offset[6,1]       -0.04  1299 1.00
#> mu_offset[7,1]       -0.04  1302 1.00
#> mu_offset[8,1]       -0.04  1305 1.00
#> mu_offset[9,1]       -0.04  1307 1.00
#> mu_offset[10,1]      -0.04  1309 1.00
#> mu_offset[11,1]      -0.04  1310 1.00
#> mu_offset[12,1]      -0.04  1310 1.00
#> mu_offset[13,1]      -0.04  1309 1.00
#> mu_offset[14,1]      -0.04  1308 1.00
#> mu_offset[15,1]      -0.04  1305 1.00
#> mu_offset[16,1]      -0.04  1303 1.00
#> mu_offset[17,1]      -0.04  1299 1.00
#> mu_offset[18,1]      -0.04  1296 1.00
#> mu_offset[19,1]      -0.04  1292 1.00
#> mu_offset[20,1]      -0.04  1290 1.00
#> mu_offset[21,1]      -0.04  1287 1.00
#> mu_offset[22,1]      -0.04  1286 1.00
#> mu_offset[23,1]      -0.04  1285 1.00
#> mu_offset[24,1]      -0.04  1285 1.00
#> mu_offset[25,1]      -0.04  1286 1.00
#> mu_offset[26,1]      -0.04  1288 1.00
#> mu_offset[27,1]      -0.04  1292 1.00
#> mu_offset[28,1]      -0.04  1298 1.00
#> mu_offset[29,1]      -0.04  1307 1.00
#> mu_offset[30,1]      -0.04  1318 1.00
#> lp__             -79945.81   536 1.01
#> 
#> Samples were drawn using NUTS(diag_e) at Wed Nov  4 17:29:03 2026.
#> For each parameter, n_eff is a crude measure of effective sample size,
#> and Rhat is the potential scale reduction factor on split chains (at 
#> convergence, Rhat=1).

3. Using the Fits to Predict Coverage

Predicting coverage from fitted models involves providing a target grid for the desired locations, ages, cohorts, and doses, and then calling predict(). You can load the example target dataset bundled with imuGAP package data and filter it to match each location resolution:

data("target_sim", package = "imuGAP")

# 3-Layer Prediction (State, County, School)
target_3layer <- target_sim[loc_id %in% locs_3layer$loc_id]
predict_3layer <- predict(object = fit_3layer, target = target_3layer, posterior_size = 100)

# 2-Layer Prediction (State and County)
target_2layer <- target_sim[loc_id %in% locs_2layer$loc_id]
predict_2layer <- predict(object = fit_2layer, target = target_2layer, posterior_size = 100)

# 1-Layer Prediction (State only)
target_1layer <- target_sim[loc_id %in% locs_1layer$loc_id]
predict_1layer <- predict(object = fit_1layer, target = target_1layer, posterior_size = 100)

If you are working along through this vignette, you can simply load the pre-computed prediction results bundled with imuGAP package data:

data("predict_sim", package = "imuGAP")
data("predict_sim_2layer", package = "imuGAP")
data("predict_sim_1layer", package = "imuGAP")

predict_sim_2layer
#> An imuGAP predictions object (`imugap_predict`):
#>   Targets:   144 target population slices across 4 locations
#>   Posterior: 100 draws (4 chains x 25 iterations)
#> 
#> Use summary() to compute quantiles or as.data.frame() to convert to a long table.

You can summarize the predictions using summary() to obtain posterior median (q50) and 95% credible intervals (q2_5, q97_5):

summary_3layer <- summary(predict_sim)
summary_2layer <- summary(predict_sim_2layer)
summary_1layer <- summary(predict_sim_1layer)

4. Comparing Different Resolution Fits

As seen in previous sections, the actual fitting and prediction steps are identical irrespective of data resolution. Let’s see how the different resolutions affect parameter and coverage estimation.

The following plot compares estimated State-level coverage from the 1-layer, 2-layer, and 3-layer model fits against the latent coverage stored in latent_params_sim$coverage. Faceting by data resolution (columns) shows that macro State trends are estimated better with more data, essentially because the as-observed state data had a bit of bias (just due to noise) but that noise was dampened by additional independent observations of higher resolution populations:

Show plot code
state_name <- locations_sim[is.na(parent_id), loc_id]

state_1layer_pred <- summary_1layer[
  loc_id == state_name & dose == 2 & age > 4
][, hierarchy := "1-Layer (State Only)"]
state_2layer_pred <- summary_2layer[
  loc_id == state_name & dose == 2 & age > 4
][, hierarchy := "2-Layer (State/County)"]
state_3layer_pred <- summary_3layer[
  loc_id == state_name & dose == 2 & age > 4
][, hierarchy := "3-Layer (State/County/School)"]

state_cov_pred <- rbindlist(list(
  state_1layer_pred,
  state_2layer_pred,
  state_3layer_pred
))
h_levels_3 <- c(
  "1-Layer (State Only)",
  "2-Layer (State/County)",
  "3-Layer (State/County/School)"
)
state_cov_pred[, hierarchy := factor(hierarchy, levels = h_levels_3)]

state_idx <- predict_sim$target[
  loc_id == state_name & dose == 2 & age > 4,
  which = TRUE
]
true_state_df <- data.table(
  age = predict_sim$target[state_idx, age],
  coverage = latent_params_sim$coverage[state_idx]
)

true_state_faceted <- rbindlist(lapply(h_levels_3, function(h) {
  df <- copy(true_state_df)
  df[, hierarchy := factor(h, levels = h_levels_3)]
  df
}))

ggplot() +
  geom_ribbon(
    data = state_cov_pred,
    aes(x = age, ymin = q2_5, ymax = q97_5, fill = "Estimated (95% CI)"),
    alpha = 0.2
  ) +
  geom_line(
    data = state_cov_pred,
    aes(x = age, y = q50, color = "Estimated (Median)"),
    linewidth = 1
  ) +
  geom_point(
    data = true_state_faceted,
    aes(x = age, y = coverage, shape = "True Coverage"),
    color = "black",
    size = 2.2
  ) +
  facet_grid(. ~ hierarchy) +
  scale_shape_manual(name = "", values = c("True Coverage" = 17)) +
  scale_color_manual(name = "", values = c("Estimated (Median)" = "#1b9e77")) +
  scale_fill_manual(name = "", values = c("Estimated (95% CI)" = "#1b9e77")) +
  theme_bw() +
  scale_x_continuous(breaks = seq(5, 18, by = 3), minor_breaks = NULL) +
  coord_cartesian(ylim = c(0.8, 1.0)) +
  theme(
    legend.position = "inside",
    legend.position.inside = c(0.02, 0.05),
    legend.justification.inside = c(0, 0),
    legend.direction = "horizontal",
    legend.box = "horizontal"
  ) +
  labs(
    x = "Age",
    y = "State-Level Two-Dose Coverage"
  )
State-level coverage comparison across 1-layer, 2-layer, and 3-layer model fits against true values.

State-level coverage comparison across 1-layer, 2-layer, and 3-layer model fits against true values.

County-Level Coverage Across Data Resolutions

The following plot compares County-level coverage estimates between the 2-layer and 3-layer model fits across counties (Scruggs, Simone, Watson):

Show plot code
counties <- locations_sim[parent_id == state_name, loc_id]

county_2layer_pred <- summary_2layer[
  loc_id %in% counties & dose == 2 & age > 4
][, hierarchy := "2-Layer (State/County)"]
county_3layer_pred <- summary_3layer[
  loc_id %in% counties & dose == 2 & age > 4
][, hierarchy := "3-Layer (State/County/School)"]

county_cov_pred <- rbindlist(list(county_2layer_pred, county_3layer_pred))
h_levels_county <- c("2-Layer (State/County)", "3-Layer (State/County/School)")
county_cov_pred[, hierarchy := factor(hierarchy, levels = h_levels_county)]
county_cov_pred[, loc_id := factor(
  loc_id,
  levels = c("Simone", "Watson", "Scruggs")
)]

county_idx <- predict_sim$target[
  loc_id %in% counties & dose == 2 & age > 4,
  which = TRUE
]
true_county_df <- data.table(
  loc_id = factor(
    predict_sim$target[county_idx, loc_id],
    levels = c("Simone", "Watson", "Scruggs")
  ),
  age = predict_sim$target[county_idx, age],
  coverage = latent_params_sim$coverage[county_idx]
)

true_county_faceted <- rbindlist(lapply(h_levels_county, function(h) {
  df <- copy(true_county_df)
  df[, hierarchy := factor(h, levels = h_levels_county)]
  df
}))

ggplot() +
  geom_ribbon(
    data = county_cov_pred,
    aes(x = age, ymin = q2_5, ymax = q97_5, fill = loc_id),
    alpha = 0.15
  ) +
  geom_line(
    data = county_cov_pred,
    aes(x = age, y = q50, color = loc_id),
    linewidth = 0.9
  ) +
  geom_point(
    data = true_county_faceted,
    aes(x = age, y = coverage, color = loc_id, shape = "True Coverage"),
    size = 2
  ) +
  facet_grid(. ~ hierarchy) +
  scale_shape_manual(name = "", values = c("True Coverage" = 18)) +
  scale_color_discrete(NULL, aesthetics = c("color", "fill")) +
  scale_x_continuous(breaks = seq(5, 18, by = 3), minor_breaks = NULL) +
  coord_cartesian(ylim = c(0.8, 1.0)) +
  theme_bw() +
  theme(
    legend.position = "inside",
    legend.position.inside = c(0.02, 0.05),
    legend.justification.inside = c(0, 0),
    legend.direction = "horizontal",
    legend.box = "horizontal"
  ) +
  labs(
    x = "Age",
    y = "County-Level Two-Dose Coverage"
  )
County-level coverage comparison: 2-layer vs. 3-layer model estimates against true values.

County-level coverage comparison: 2-layer vs. 3-layer model estimates against true values.

Force of Vaccination (λ\lambda) Estimation

The following plot shows force of vaccination parameter estimates (λ\lambda) across different location data resolutions compared against true values:

Show plot code
extract_lambda_summary <- function(fit, label) {
  draws <- extract_imugap(fit, pars = "lambda_raw")$lambda_raw
  doses_factor <- factor(c(
    rep("Dose 1", nrow(draws)),
    rep("Dose 2", nrow(draws))
  ))
  data.table(
    hierarchy = label,
    dose = doses_factor,
    lambda_raw = c(draws[, 1], draws[, 2])
  )[, .(
    q50 = stats::median(lambda_raw),
    q2_5 = stats::quantile(lambda_raw, 0.025),
    q97_5 = stats::quantile(lambda_raw, 0.975)
  ), by = .(hierarchy, dose)]
}

lambda_est <- rbindlist(list(
  extract_lambda_summary(fit_sim_1layer, "1-Layer"),
  extract_lambda_summary(fit_sim_2layer, "2-Layer"),
  extract_lambda_summary(fit_sim, "3-Layer")
))
h_levels_lambda <- c("1-Layer", "2-Layer", "3-Layer")
lambda_est[, hierarchy := factor(hierarchy, levels = h_levels_lambda)]

true_lambda <- data.table(
  dose = factor(c("Dose 1", "Dose 2")),
  hierarchy = factor("1-Layer", levels = h_levels_lambda),
  true_val = log(latent_params_sim$lambda),
  label = sprintf("True~lambda == %.1f", latent_params_sim$lambda)
)

ggplot(lambda_est, aes(x = hierarchy, y = q50)) +
  geom_hline(
    data = true_lambda,
    aes(yintercept = true_val),
    color = "firebrick",
    linetype = "dashed",
    linewidth = 0.8
  ) +
  geom_label(
    data = true_lambda,
    aes(x = hierarchy, y = true_val, label = label),
    parse = TRUE,
    color = "firebrick",
    fill = ggplot2::alpha("white", 0.75),
    linewidth = NA,
    vjust = -0.3,
    hjust = 0.1,
    size = 3.2
  ) +
  geom_pointrange(
    aes(ymin = q2_5, ymax = q97_5),
    size = 0.7
  ) +
  facet_wrap(~dose) +
  coord_cartesian(ylim = c(0.5, 1.5)) +
  scale_y_continuous(
    transform = "exp",
    labels = function(x) sprintf("%.2f", exp(x))
  ) +
  theme_bw() +
  labs(
    x = "Data Resolution (Model Hierarchy)",
    y = "Uptake Rate (exponential scale)"
  )
Force of vaccination (lambda) estimates across data resolutions compared to true values.

Force of vaccination (lambda) estimates across data resolutions compared to true values.


Conclusion

Support for flexible location layers allows imuGAP to adapt to whatever population hierarchy you provide:

  • If you provide only state-level aggregated data (1 layer), macro trends are estimated accurately by the process model.
  • If you provide sub-populations like counties (2 layers) or sub-sub-populations like schools (3 layers), the model estimates hierarchical random offsets down the tree, allowing you to project cohort trajectories and resolve finer-grained geographic variation.