PS 818 - Data Analysis with Statistical Models
October 19, 2026
\[ \DeclareMathOperator*{\argmin}{arg\,min} \DeclareMathOperator*{\argmax}{arg\,max} \]
data, parameters, modelIn our regression models so far, we assumed a common intercept and slope for all of the data.
We also assumed that observations were completely exchangeable
This may be a poor modeling choice if states are very heterogeneous and information about the state in which an observation is located would provide additional predictive power beyond knowing \(X_i\).
Hierarchical or Multilevel linear models try to address this by incorporating additional structure on the regression parameters.
We can incorporate group membership into the data-generating process.
Instead of a common prior on \(\beta \sim \text{Normal}(b_0, B_0^{-1})\), we may do something like:
\[\beta_j \sim \text{Normal}(\mu_{\beta}, \Sigma_{\beta})\]
\[Y_{ij} \sim \text{Normal}(X_{ij}^{\prime}\beta_{j}, \sigma^2)\]
This model assumes that each group has its own set of group-level coefficients \(\beta_j\) that are drawn from a common distribution centered at \(\mu_{\beta}\).
The variance \(\Sigma_{\beta}\) then dictates the degree of pooling across groupings.
Let’s start with a single logistic regression for everyone.
\[Y_{ij} \sim \text{Bernoulli}(\pi_{ij})\] \[\log\bigg(\frac{\pi_{ij}}{1 - \pi_{ij}}\bigg) = \beta_0 + \beta_1 \text{college}_{ij}\] \[\beta_0, \beta_1 \sim \text{Normal}(0, 5^2)\]
Our only predictors are state and education, so every respondent in the same state \(\times\) education cell has the same \(\pi_{ij}\)
voters <- voters %>% mutate(state_num = as.numeric(as.factor(state)))
X_mat <- model.matrix(~ college, data = voters)
K <- ncol(X_mat) # Number of beta parameters
cells <- voters %>% group_by(state, state_num, college) %>%
summarize(n = n(), y = sum(clinton), .groups = "drop")
X_cell <- model.matrix(~ college, data = cells)| state | state_num | college | n | y |
|---|---|---|---|---|
| AK | 1 | 0 | 27 | 11 |
| AK | 1 | 1 | 28 | 18 |
| AL | 2 | 0 | 181 | 71 |
| AL | 2 | 1 | 108 | 58 |
pooled_model <- "
data {
int N; // number of respondents
int K; // number of covariates
matrix[N, K] X; // covariate matrix
array[N] int<lower=0, upper=1> y; // 1 = Clinton, 0 = Trump
int M; // number of cells
matrix[M, K] X_cell; // covariates for each cell
array[M] int n_cell; // number of respondents in each cell
}
parameters {
vector[K] beta; // regression coefficients
}
model {
beta ~ normal(0, 5); // normal prior
y ~ bernoulli_logit(X * beta); // logistic regression
}
generated quantities {
vector[M] eta_cell = X_cell * beta; // linear predictor for each cell
array[M] int y_rep = binomial_rng(n_cell, inv_logit(eta_cell)); // replicated Clinton voters in each cell
vector[M] ll_clinton = log_inv_logit(eta_cell); // log-likelihood of one Clinton voter in each cell
vector[M] ll_trump = log1m_inv_logit(eta_cell); // log-likelihood of one Trump voter in each cell
}
"pooled_fit <- stan(
model_code = pooled_model, # Stan code
data = stan_data, # named list of data (Stan ignores the elements it doesn't use)
chains = 4, # number of Markov chains
warmup = 500, # number of warmup iterations per chain
iter = 2500, # total number of iterations per chain
cores = 4, # number of cores
refresh = 0, # no progress shown
seed = 53706
)Inference for Stan model: anon_model.
4 chains, each with iter=2500; warmup=500; thin=1;
post-warmup draws per chain=2000, total post-warmup draws=8000.
mean se_mean sd 2.5% 25% 50% 75% 97.5% n_eff Rhat
beta[1] -0.10 0 0.02 -0.13 -0.11 -0.10 -0.09 -0.07 3090 1
beta[2] 0.71 0 0.03 0.67 0.70 0.71 0.73 0.76 3112 1
Samples were drawn using NUTS(diag_e) at Wed Oct 21 11:37:11 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).
A very useful quantity to obtain is the predictive distribution of \(y\) given the model.
\[f(\tilde{y} | \mathbf{Y}) =\int f(\tilde{y}, \theta | \mathbf{Y}) d\theta = \int f(\tilde{y} | \mathbf{Y}, \theta) f(\theta | \mathbf{Y}) d\theta\]
Suppose our prediction target is the in-sample distribution of the covariates
(e.g.) for the logit
y_rep is a replicated count of Clinton voters for every cell
state_n <- tapply(cells$n, cells$state, sum) # Respondents per state
state_share_ppc <- function(fit){
y_rep <- rstan::extract(fit, "y_rep")$y_rep # draws x cells
share_rep <- sweep(t(rowsum(t(y_rep), cells$state)), 2, state_n, "/") # draws x states
tibble(state = names(state_n), obs = tapply(cells$y, cells$state, sum)/state_n, mean = colMeans(share_rep),
lower = apply(share_rep, 2, quantile, .025), upper = apply(share_rep, 2, quantile, .975))
}
ppc_pooled <- state_share_ppc(pooled_fit)Our regression from before is an example of a fully pooled regression.
Assume:
\[\log\bigg(\frac{\pi_{ij}}{1 - \pi_{ij}}\bigg) = X_{ij}^{\prime}\beta + \alpha_{j}\] \[\alpha_j \sim \text{Normal}(0, \sigma^2_\alpha)\]
And keep the same prior as before on \(\beta\), which still contains the intercept. For \(\sigma_\alpha\) we’ll use a Half-Cauchy prior (a Cauchy with the constraint \(\sigma_\alpha > 0\)).
\(\alpha_j\) can be interpreted as the group-specific “shift” in the intercept from the “grand” intercept \(\beta_0\)
data blockdata{
int N; // number of respondents
int K; // number of covariates
matrix[N, K] X; //matrix of covariates
array[N] int<lower=0, upper=1> y; // 1 = Clinton, 0 = Trump
int J; // Number of groups
array[N] int state; // group membership indicator
int M; // number of cells
matrix[M, K] X_cell; // covariates for each cell
array[M] int n_cell; // number of respondents in each cell
array[M] int cell_state; // group of each cell
}generated quantities block computes the same cell-level quantities as beforemodel_var_intercept <- stan(
model_code = varying_intercepts_model, # Stan code
data = stan_data, # named list of data
chains = 4, # number of Markov chains
warmup = 500, # number of warmup iterations per chain
iter = 2500, # total number of iterations per chain
cores = 4, # number of cores (this is much slower if = 1)
refresh = 0, # no progress shown
seed = 53706 # random seed
)Inference for Stan model: anon_model.
4 chains, each with iter=2500; warmup=500; thin=1;
post-warmup draws per chain=2000, total post-warmup draws=8000.
mean se_mean sd 2.5% 25% 50% 75% 97.5% n_eff Rhat
beta[1] -0.13 0 0.05 -0.22 -0.16 -0.13 -0.10 -0.04 658 1
beta[2] 0.69 0 0.03 0.64 0.68 0.69 0.71 0.75 12103 1
sigma_a 0.31 0 0.04 0.24 0.28 0.31 0.34 0.40 6578 1
alpha[1] -0.06 0 0.21 -0.47 -0.20 -0.06 0.08 0.34 10426 1
Samples were drawn using NUTS(diag_e) at Wed Oct 21 11:39:31 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).
Frequentist/MLE Inference - the Akaike Information Criterion
\[\text{AIC} = 2k - 2\ell(\hat{\theta})\]
Bayesian Inference - the Deviance Information Criterion
\[\text{DIC} = 2p_{DIC} - 2\log p(y | \hat{\theta}_{\text{PM}})\]
In general, a very common objective function for evaluating the quality of model “fit” in a Bayesian setting is the log-predictive density
For a new “out-of-sample” \(\tilde{y}_i\), the log posterior predictive density is
\[\log p_{\text{post}} (\tilde{y}_i) = \log \int p(\tilde{y}_i | \theta) \times p_{\text{post}}(\theta)d\theta\]
We want to pick a model that has a high expected log pointwise-predictive density – for a “new” size \(n\) sample of observation drawn from a common distribution \(f(\tilde{y}_i)\),
\[\text{elppd} = \sum_{i=1}^n E_f[\log p_{\text{post}} (\tilde{y}_i)] = \sum_{i=1}^n\int \log p_{\text{post}} (\tilde{y}_i)f(\tilde{y}_i)d\tilde{y}_i\]
Our goal is to try to estimate this from the data that we have.
One approach would be to use the “in-sample” fit criterion using in-sample observations \(y_i\)
\[\text{lppd} = \sum_{i=1}^n \log p_{\text{post}} (y_i)\]
We can calculate it by averaging over our \(S\) posterior samples
\[\widehat{\text{lppd}} = \sum_{i=1}^n \log \bigg( \frac{1}{S} \sum_{s=1}^S p(y_i | \theta^s)\bigg)\]
But here we know that it’s going to be biased (too optimistic) for the “out-of-sample” predictive accuracy due to over-fitting.
From Vehtari, Gelman, and Gabry (2017), our “leave-one-out” estimate of the expected log predictive density is:
\[\text{elpd}_{\text{loo}} = \sum_{i=1}^n \log p(y_i | y_{-i})\]
In principle, this requires re-fitting the model to \(N\) datasets (with a single observation removed). However, it is possible to obtain an estimate of \(\log p(y_i | y_{-i})\) using the full model fit. Note:
\[p(y_i | y_{-i}) = \int p(y_i | \theta) p(\theta | y_{-i}) d\theta\]
We can calculate \(p(y_i | \theta)\) for a given draw of theta.
Ultimately, we need to re-weight by \(\frac{p(\theta_s | y_{-i})}{p(\theta_s | y)}\). Gelfand, Dey, and Chang (1992) show that under conditional independence in the model:
\[\frac{p(\theta_s | y_{-i})}{p(\theta_s | y)} = \frac{p(\theta_s | y_{-i})}{p(\theta_s | y_i, y_{-i})} \propto \frac{p(\theta_s | y_{-i})}{p(y_i | \theta_s)p(\theta_s | y_{-i})} = \frac{1}{p(y_i | \theta_s)}\]
Let \(r^s_i = \frac{1}{p(y_i | \theta^s)}\) or the importance weight calculated for monte carlo sample \(s\)
Then our monte carlo estimate of \(p(y_i | y_{-i})\) is
\[p(y_i | y_{-i}) \approx \frac{\sum_{s=1}^S r_i^s p(y_i | \theta^s)}{\sum_{s=1}^S r_i^s}\]
Unfortunately, these importance weights are very poorly behaved (high variance).
Vehtari, Gelman, and Gabry (2017) propose an alternative method for estimating the importance weights: pareto-smoothed-importance-sampling (PSIS)
This PSIS-LOO cross-validation method is implemented in the loo R package.
loo, we need the log-likelihood of each observation \(\log p(y_i | \theta^s)\), evaluated at each posterior draw \(s\). This comes by default in canned rstanarm or brms models but for custom models we need to specify to return it in the generated quantities blockloo() then computes PSIS-LOO from this \(N \times S\) matrix, and loo_compare() compares models by summing the pointwise differences in \(\log p(y_i | y_{-i})\)For our example: \(N \times S\) is 28,462 respondents \(\times\) 8,000 draws \(\approx\) 228 million numbers (1.8 GB) for each model
So we’ll instead do the same calculation by state x education cell.
ll_clinton); every Trump voter has \(\log(1 - \pi_m)\) (ll_trump)loo once for each cell \(\times\) vote, then replicate each result once per respondent who falls in that cell.loo_respondents <- function(fit){
# draws x chains x (cell, vote) combinations
ll <- abind::abind(extract_log_lik(fit, "ll_clinton", merge_chains = F),
extract_log_lik(fit, "ll_trump", merge_chains = F), along = 3)
loo_types <- loo(ll, r_eff = relative_eff(exp(ll)))
rep(loo_types$pointwise[, "elpd_loo"], c(cells$y, cells$n - cells$y)) # one entry per respondent
}
elpd_summary <- function(pointwise) c(elpd_loo = sum(pointwise), se = sqrt(length(pointwise)*var(pointwise)))state_gap_ppc <- function(fit){
y_rep <- rstan::extract(fit, "y_rep")$y_rep
col <- cells$college == 1
gap_rep <- sweep(y_rep[, col], 2, cells$n[col], "/") - sweep(y_rep[, !col], 2, cells$n[!col], "/")
tibble(state = cells$state[col], obs = cells$y[col]/cells$n[col] - cells$y[!col]/cells$n[!col], mean = colMeans(gap_rep),
lower = apply(gap_rep, 2, quantile, .025), upper = apply(gap_rep, 2, quantile, .975))
}
gap_varint <- state_gap_ppc(model_var_intercept)What if we allow all of the parameters to vary by group?
\[\beta_j \sim \text{Normal}(\mu_{\beta}, \Sigma_{\beta})\] \[\log\bigg(\frac{\pi_{ij}}{1 - \pi_{ij}}\bigg) = X_{ij}^{\prime}\beta_{j}\]
This is a more general and flexible hierarchical model
The conjugate prior is the inverse-Wishart, the multivariate extension of the inverse-gamma.
Our prior on \(\Sigma_{\beta}\) starts by decomposing \(\Sigma_{\beta}\) into a diagonal scaling matrix \(\text{diag}(\tau)\) and a correlation matrix \(\Omega\)
\[\Sigma_{\beta} = \text{diag}(\tau)\Omega\text{diag}(\tau)\]
For each \(\tau_k\) we’ll use a Half-Cauchy distribution (Cauchy with the constraint \(\tau_k > 0\))
For \(\Omega\), we’ll use the Lewandowski-Kurowicka-Joe (LKJ) distribution which defines a distribution over correlation matrices
Write each state’s coefficients as the national average plus a state-specific deviation
\[\beta_j = \mu_{\beta} + e_j, \qquad e_j \sim \text{Normal}(0, \Sigma_{\beta})\] \[\log\bigg(\frac{\pi_{ij}}{1 - \pi_{ij}}\bigg) = \underbrace{X_{ij}^{\prime}\mu_{\beta}}_{\text{same in every state}} + \underbrace{X_{ij}^{\prime}e_j}_{\text{state deviation}}\]
Same setup as above, but keeping the part that’s the same in every state separate will make it easy to add state-level predictors later
X_fix for the terms that are the same in every state (here, \(X_{ij}^{\prime}\mu_{\beta}\))
X again - but it will get more columns laterdata blockparameters blockmodel blockgenerated quantities compute the same cell-level quantities as beforemodel_var_slope <- stan(
model_code = varying_slopes_model, # Stan code
data = stan_data, # named list of data
chains = 4, # number of Markov chains
warmup = 500, # number of warmup iterations per chain
iter = 2500, # total number of iterations per chain
cores = 4,
refresh = 0, # no progress shown
seed = 53706 # random seed
)Inference for Stan model: anon_model.
4 chains, each with iter=2500; warmup=500; thin=1;
post-warmup draws per chain=2000, total post-warmup draws=8000.
mean se_mean sd 2.5% 25% 50% 75% 97.5% n_eff Rhat
b[1] -0.13 0.00 0.04 -0.22 -0.16 -0.13 -0.10 -0.05 1283 1
b[2] 0.69 0.00 0.04 0.62 0.67 0.69 0.72 0.76 4428 1
tau[1] 0.28 0.00 0.04 0.21 0.25 0.28 0.31 0.38 3088 1
tau[2] 0.15 0.00 0.05 0.06 0.12 0.15 0.18 0.24 342 1
Omega[1,2] 0.31 0.01 0.27 -0.24 0.13 0.33 0.51 0.80 1025 1
Samples were drawn using NUTS(diag_e) at Wed Oct 21 20:25:51 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).
b is \(\mu_{\beta}\), the national average intercept and education slopetau[2], the standard deviation of the education slopes across states, is clearly above zerob plus that state’s eelpd_loo se
-19036.7 36.3
So far, every state’s coefficients are pooled towards the same national mean \(\mu_{\beta}\)
Let the mean of each state’s coefficients depend on a vector of \(L\) state-level predictors \(Z_j\) (including a 1 for the intercept)
\[\beta_j \sim \text{Normal}(\Gamma^{\prime}Z_j, \Sigma_{\beta})\]
Gelman et al. (2007) model the state intercepts and income slopes using each state’s average income and region
As before, write each state’s coefficients as a mean plus a deviation: \(\beta_j = \Gamma^{\prime}Z_j + e_j\), with \(e_j \sim \text{Normal}(0, \Sigma_{\beta})\)
Plug this into the linear predictor
\[\begin{aligned}\log\bigg(\frac{\pi_{ij}}{1 - \pi_{ij}}\bigg) = &\underbrace{(\Gamma_{11} + \Gamma_{21}\text{obama}_j + \Gamma_{31}\text{coll}_j + e_{0j})}_{\beta_{0j}\text{: intercept}} \\ &+ \underbrace{(\Gamma_{12} + \Gamma_{22}\text{obama}_j + \Gamma_{32}\text{coll}_j + e_{1j})}_{\beta_{1j}\text{: education slope}} \times \text{college}_{ij}\end{aligned}\]
The state-level predictors are just more regression terms
X_fix with their main effects and interactions with collegestate_preds <- read_csv("data/state_predictors.csv") %>% arrange(state)
state_preds_z <- state_preds %>% transmute(state, state_obama = as.numeric(scale(qlogis(obama_2012))),
state_college = as.numeric(scale(college_share)))
X_fix <- model.matrix(~ college * (state_obama + state_college), data = voters %>% left_join(state_preds_z, by = "state"))
X_fix_cell <- model.matrix(~ college * (state_obama + state_college), data = cells %>% left_join(state_preds_z, by = "state"))
stan_data$P <- ncol(X_fix)
stan_data$X_fix <- X_fix
stan_data$X_fix_cell <- X_fix_cell
colnames(X_fix)[1] "(Intercept)" "college" "state_obama"
[4] "state_college" "college:state_obama" "college:state_college"
model_group <- stan(
model_code = varying_slopes_model, # same Stan code
data = stan_data, # with the new X_fix
chains = 4, # number of Markov chains
warmup = 500, # number of warmup iterations per chain
iter = 2500, # total number of iterations per chain
cores = 4,
refresh = 0, # no progress shown
seed = 53706, # random seed
control = list(adapt_delta = 0.95) # smaller steps (default is 0.8) - see next slide
)adapt_delta so the “leapfrog” solver takes smaller steps
brms avoids the problem by reparameterizing the model (more later)X_fix mean 2.5% 97.5% n_eff Rhat
(Intercept) -0.13 -0.17 -0.09 6031 1
college 0.69 0.62 0.76 5344 1
state_obama 0.32 0.24 0.40 4499 1
state_college 0.04 -0.03 0.10 4491 1
college:state_obama -0.02 -0.16 0.12 4126 1
college:state_college 0.08 -0.04 0.19 4325 1
Inference for Stan model: anon_model.
4 chains, each with iter=2500; warmup=500; thin=1;
post-warmup draws per chain=2000, total post-warmup draws=8000.
mean se_mean sd 2.5% 25% 50% 75% 97.5% n_eff Rhat
tau[1] 0.07 0.00 0.02 0.03 0.05 0.07 0.08 0.11 569 1.00
tau[2] 0.13 0.00 0.04 0.05 0.11 0.13 0.16 0.23 432 1.02
Omega[1,2] -0.07 0.01 0.35 -0.66 -0.33 -0.10 0.17 0.65 678 1.00
Samples were drawn using NUTS(diag_e) at Wed Oct 21 20:44:40 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).
state_obama: states that voted more for Obama in 2012 have higher interceptscollege:state_college: states with more college graduates may have somewhat larger education gaps - but it’s uncertainelpd_loo se
18.7 5.3
Two ways to think of post-stratification:
First - Let \(\hat{\theta}_{y}^{c}\) denote our estimator for the population mean within cell \(c\)
\[\hat{\theta}_{y}^{PS} = \sum_{c=1}^C \tilde{P}_{c} \hat{\theta}_{y}^{c}\]
Alternatively, we’ll sometimes write \(\tilde{P}_c = \frac{\tilde{N}_c}{\tilde{N}}\) where \(\tilde{N}\) is the size of the population and \(\tilde{N}_c\) is the number of units in that cell.
\[\hat{\theta}_{y}^{PS} = \sum_{c=1}^C \frac{\tilde{N}_c}{\tilde{N}} \hat{\theta}_{y}^{c}\]
Fixed “constant” weights on the within-cell estimators.
Second - We can think of it as weighting individual observations using our adjustment weights \(\tilde{w}^{PS}_i\)
Let \(c(i)\) denote the class to which unit \(i\) belongs. Then the post-stratification weights are
\[\tilde{w}^{PS}_i = (\tilde{P}_{c(i)}/\hat{P}^{S}_{c(i)}) \times d_i\]
where \(\hat{P}^{S}_{c(i)}\) is the estimated in-sample proportion of observations in class \(c\) and \(d_i\) is unit \(i\)’s design weight
When design weights are constant, \(\hat{P}^{S}_{c(i)}\) is just the sample mean of the indicator of class membership \(\mathbf{1}(i \in c)\)
Intuition
pres <- read_csv("data/county_pres_2012_2016.csv")
state_results <- pres %>% group_by(state) %>% summarize(actual = sum(dem_2016)/sum(dem_2016 + rep_2016))
state_raw <- voters %>% group_by(state) %>% summarize(n = n(), raw = mean(clinton)) %>%
inner_join(state_results, by = "state") # Alaska doesn't report results by countybrms package which has pre-existing implementations of popular models like the random slopes/random intercepts generalized linear mixed modelbrms formula syntax: (1 + college | state) is our varying slopes model from before Estimate Est.Error l-95% CI u-95% CI Rhat
sd(Intercept) 0.295 0.0445 0.2190 0.391 1
sd(college) 0.143 0.0494 0.0443 0.244 1
cor(Intercept,college) 0.257 0.3088 -0.3445 0.845 1
Estimate Est.Error l-95% CI u-95% CI Rhat
Intercept 0.507 0.0882 0.331 0.676 1
female 0.411 0.0253 0.361 0.461 1
age25M34 -0.380 0.0859 -0.546 -0.212 1
age35M44 -0.647 0.0832 -0.806 -0.484 1
age45M64 -0.939 0.0763 -1.088 -0.790 1
age65P -1.096 0.0781 -1.245 -0.947 1
college 0.679 0.0365 0.608 0.752 1
| county_fips | state | female | age | college | n |
|---|---|---|---|---|---|
| 01001 | AL | 0 | 18-24 | 1 | 188 |
| 01001 | AL | 0 | 18-24 | 0 | 1432 |
| 01001 | AL | 0 | 18-24 | 1 | 6 |
| 01001 | AL | 0 | 18-24 | 0 | 898 |
| 01001 | AL | 0 | 25-34 | 1 | 584 |
state_frame <- acs %>% group_by(state, female, age, college) %>%
summarize(N = sum(n), .groups = "drop") %>% mutate(n = 1) # n = 1: predict a probability
state_frame$p_turnout <- colMeans(posterior_epred(turnout_mlm, newdata = state_frame))
state_frame$p_clinton <- colMeans(posterior_epred(state_mlm, newdata = state_frame))MrP barely reduces the average bias, and its RMSE is worse than the raw means
DC: raw 97%, MrP 78%, actual 96%
Wyoming: raw 48%, MrP 50%, actual 24%
But we know there are some extreme states out there!
brmsIn brms (and lme4), a group-level predictor is just a population-level term: a column that’s constant within each group
y ~ college * (state_obama + state_college) + (1 + college | state)
| Term | Role | In \(\Gamma\) |
|---|---|---|
1, college |
national average intercept and education slope | \(\Gamma_{11}, \Gamma_{12}\) |
state_obama, state_college |
shift the state intercepts | \(\Gamma_{21}, \Gamma_{31}\) |
college:state_obama, college:state_college |
shift the state education slopes | \(\Gamma_{22}, \Gamma_{32}\) |
(1 + college \| state) |
zero-mean state deviations | \(e_j \sim \text{Normal}(0, \Sigma_{\beta})\) |
brms has slightly different default priors)
brms also avoids divergent transitions by not sampling \(\tau\) directly (a “non-centered” parameterization)brmsOnly the interactions move the slopes: main effects of the state-level predictors shift the intercepts
Every varying term needs a population-level counterpart
(1 + college | state) centers the state slopes at zero, so college has to be in the population-level part tooMerge the group-level predictors onto every row of the data (they must be constant within each group - brms won’t check!)
This works with varying intercepts, but not with state fixed effects
state_obama would be perfectly collinear with the dummiesbrmsbrms can also write the model literally as \(\beta_j = \Gamma^{\prime}Z_j + e_j\) with its bf() formula syntaxstate_frame <- state_frame %>% left_join(state_preds_z, by = "state")
state_frame$p_clinton2 <- colMeans(posterior_epred(state_mlm2, newdata = state_frame))
mrp_states <- state_frame %>% group_by(state) %>%
summarize(mrp_statepred = sum(N * p_turnout * p_clinton2)/sum(N * p_turnout)) %>%
inner_join(mrp_states, by = "state")
mrp_states %>% pivot_longer(c(raw, mrp, mrp_statepred), names_to = "estimate") %>% group_by(estimate) %>%
summarize(rmse = sqrt(mean((value - actual)^2)), bias = mean(value - actual))# A tibble: 3 × 3
estimate rmse bias
<chr> <dbl> <dbl>
1 mrp 0.0865 0.0488
2 mrp_statepred 0.0681 0.0485
3 raw 0.0774 0.0518
# A tibble: 1 × 2
counties_with_respondents median_respondents
<int> <dbl>
1 2290 4
[1] 3112
mrp_counties <- acs %>% left_join(state_frame %>% select(state, female, age, college, p_turnout, p_clinton2),
by = c("state", "female", "age", "college")) %>%
group_by(county_fips) %>% summarize(mrp_state = sum(n * p_turnout * p_clinton2)/sum(n * p_turnout)) %>%
inner_join(pres %>% select(county_fips, state, dem_share_2016), by = "county_fips")The solution is to give the model information about the small areas themselves
In brms, a group-level predictor is just another column that’s constant within each group: obama_2012 + (1 | county_fips) pools each county’s intercept towards \(\gamma_0 + \gamma_1 \times \text{(2012 vote)}_c\)
The county’s own 2012 vote takes the place of the state-level predictors
Estimate Est.Error l-95% CI u-95% CI Rhat
sd(Intercept) 0.153 0.0288 0.0965 0.208 1
Estimate Est.Error l-95% CI u-95% CI Rhat
sd(Intercept) 0.0611 0.0291 0.00666 0.118 1
sd(college) 0.1042 0.0501 0.01114 0.206 1
cor(Intercept,college) -0.1938 0.4854 -0.90368 0.865 1
Estimate Est.Error l-95% CI u-95% CI Rhat
Intercept 0.470 0.0816 0.308 0.630 1
female 0.434 0.0265 0.382 0.485 1
age25M34 -0.374 0.0875 -0.545 -0.204 1
age35M44 -0.628 0.0868 -0.793 -0.452 1
age45M64 -0.901 0.0799 -1.058 -0.744 1
age65P -1.047 0.0814 -1.212 -0.889 1
college 0.608 0.0340 0.542 0.678 1
obama_2012 0.836 0.0247 0.789 0.884 1
allow_new_levels: counties with no respondents draw their county effect from the fitted distributioncounty_frame <- county_frame %>% left_join(state_frame %>% select(state, female, age, college, p_turnout),
by = c("state", "female", "age", "college"))
mrp_counties <- mrp_counties %>% left_join(county_frame %>% group_by(county_fips) %>%
summarize(mrp_county = sum(n_adults * p_turnout * p_clinton)/sum(n_adults * p_turnout)), by = "county_fips")# A tibble: 2 × 4
model rmse bias correlation
<chr> <dbl> <dbl> <dbl>
1 mrp_county 0.0979 0.0848 0.964
2 mrp_state 0.205 0.153 0.555
mrp_states <- county_frame %>% group_by(state) %>%
summarize(mrp_county = sum(n_adults * p_turnout * p_clinton)/sum(n_adults * p_turnout)) %>%
inner_join(mrp_states, by = "state")
mrp_states %>% pivot_longer(c(raw, mrp, mrp_statepred, mrp_county), names_to = "estimate") %>% group_by(estimate) %>%
summarize(rmse = sqrt(mean((value - actual)^2)), bias = mean(value - actual))# A tibble: 4 × 3
estimate rmse bias
<chr> <dbl> <dbl>
1 mrp 0.0865 0.0488
2 mrp_county 0.0595 0.0461
3 mrp_statepred 0.0681 0.0485
4 raw 0.0774 0.0518
PS 818 - University of Wisconsin-Madison