[1] 24500 320
PS 818 - Data Analysis with Statistical Models
October 26, 2026
\[ \DeclareMathOperator*{\argmin}{arg\,min} \DeclareMathOperator*{\argmax}{arg\,max} \]
In conventional regression models, we assume a known distribution for the outcome given a set of covariates
In this setting, conditional on \(X_i\), we would model each \(Y_i\) with a known distribution (e.g. Normal)
\[Y_i | X_i = x \sim \text{Normal}(\mu_{x}, \sigma^2_{x})\]
\(\mu_{x}\) and \(\sigma^2_{x}\) are the mean and variance parameters associated with the group \(X_i = x\).
Now imagine that the group indicators are not observed but instead are latent variables
In a finite mixture model, we assume that the outcome distribution for each observation \(i\) is governed by some discrete latent indicator \(z_i \in \{1, 2, \dotsc, K\}\)
Conditional on the latent variable \(z_i = k\), the outcome distribution is known.
But unconditionally, \(Y_i\) has a mixture distribution: its density is a weighted average of the component densities. In the gaussian case:
\[Y_i \sim \sum_{k=1}^K \pi_k \times \text{Normal}(\mu_{k}, \sigma^2_{k})\]
where \(\pi_k = Pr(z_i = k)\)
Our goal is to estimate the parameters of each component and the mixing proportions \(\pi\) using likelihood inference.
[1] 24500 320
1 = Support, 2 = Oppose, 8 = skipped, 9 = not asked# A tibble: 3 × 2
CC23_324c n
<dbl+lbl> <int>
1 1 [Support] 4539
2 2 [Oppose] 19955
3 8 [skipped] 6
data/CES 2023 CC Questionnaire.pdf) has the full wording of every itemitems <- c(ban_assault_rifles = "CC23_321a", concealed_carry = "CC23_321b", background_checks = "CC23_321c",
more_police = "CC23_321d", fewer_police = "CC23_321e", legal_status = "CC23_323a",
border_patrols = "CC23_323b", border_wall = "CC23_323c", dreamers = "CC23_323d",
abortion_choice = "CC23_324a", abortion_exceptions = "CC23_324b", abortion_ban = "CC23_324c",
abortion_expand = "CC23_324d", epa_co2 = "CC23_326a", renewable_mandate = "CC23_326b",
epa_enforcement = "CC23_326c", more_fossil_fuels = "CC23_326d", halt_leases = "CC23_326e",
relax_zoning = "CC23_328a", housing_credits = "CC23_328b", medicaid_work_req = "CC23_328c",
repeal_aca = "CC23_328d", expand_medicaid = "CC23_328e", forgive_loans = "CC23_328f")
ces <- ces_raw %>%
transmute(caseid, commonweight, pid3 = as.character(as_factor(pid3)), ideo5 = as.character(as_factor(ideo5)),
across(all_of(items), ~ case_when(.x == 1 ~ 1, .x == 2 ~ 0))) %>% # skipped/not asked become NA
drop_na(all_of(names(items)))
Y <- as.matrix(select(ces, all_of(names(items)))) # 1 = Support, 0 = Oppose| Item | Support | Item | Support |
|---|---|---|---|
| Ban assault rifles | 63% | Expand abortion access | 59% |
| Easier concealed-carry permits | 40% | EPA regulates CO2 | 64% |
| Background checks on all gun sales | 91% | Renewable fuel mandate | 57% |
| 10% more police | 57% | Stronger EPA enforcement | 61% |
| 10% fewer police | 31% | More fossil fuel production | 58% |
| Legal status for undocumented workers | 64% | Halt new oil and gas leases | 47% |
| More border patrols | 72% | Relax zoning | 49% |
| Build a border wall | 52% | Tax credits for low-income housing | 73% |
| Citizenship path for Dreamers | 71% | Medicaid work requirements | 56% |
| Always allow abortion | 62% | Repeal the ACA | 44% |
| Abortion only for rape, incest, life | 52% | Expand Medicaid | 80% |
| Make abortion illegal | 19% | Forgive student loans | 59% |
kmeans() implements the algorithm; nstart re-runs it from multiple random starts and keeps the result with smallest within-cluster sum of squares. party
cluster Democrat Independent Not sure Other Republican
1 569 2605 348 450 4831
2 8257 3812 1164 467 1950
We observe \(N\) respondents each giving binary responses to \(J\) questions. \(Y_i\) is the vector of responses for unit \(i\) with components \(Y_{ij}\).
Assume latent variables \(z = \{z_1, z_2, \dotsc, z_N\}\)
\[z_i \underset{\text{i.i.d.}}{\sim} \text{Categorical}(\pi)\]
Conditional on \(z_i = k\), each response is independently Bernoulli with a cluster-and-question-specific probability \(\beta_{kj}\)
\[Y_{ij} | z_i = k \sim \text{Bernoulli}(\beta_{kj})\]
Our targets of inference are the \(K \times J\) matrix of response probabilities \(\beta\) and the cluster prevalences \(\pi\)
The key assumption: conditional on cluster membership, a respondent’s answers are independent
\[f(Y_i | z_i = k) = \prod_{j=1}^J \beta_{kj}^{Y_{ij}}(1 - \beta_{kj})^{1 - Y_{ij}}\]
Answers are certainly not independent marginally
The mixture model says all of that correlation comes from respondents belonging to different clusters
Let’s write down the marginal likelihood, summing out the latent variables:
\[\mathcal{L}(\beta, \pi; \mathbf{Y}) = \prod_{i=1}^N \sum_{k=1}^K f(Y_i| \beta_{k}, z_i = k) \times p(z_i = k | \pi)\]
This is a tricky likelihood to maximize - if we take the log, we get a log of a sum (which doesn’t simplify as neatly as a log of a product)
\[\ell(\beta, \pi ; \mathbf{Y}) = \sum_{i=1}^N \log\bigg(\sum_{k=1}^K \pi_k \prod_{j=1}^J \beta_{kj}^{Y_{ij}}(1 - \beta_{kj})^{1 - Y_{ij}} \bigg)\]
Suppose we knew the \(z_i\) as well (they were like “data”), then the “complete” log-likelihood would be easier to maximize!
\[\ell(\beta, \pi ; \mathbf{Y}, \mathbf{Z}) = \sum_{i=1}^N \sum_{k=1}^K I(z_i = k) \bigg[\log \pi_k + \sum_{j=1}^J \Big(Y_{ij} \log \beta_{kj} + (1 - Y_{ij}) \log (1 - \beta_{kj})\Big)\bigg]\]
The Expectation-Maximization algorithm (Dempster, Laird, and Rubin 1977) is an iterative method for obtaining a maximum likelihood estimate (or maximum a posteriori (MAP) in a Bayesian setting) when the likelihood consists of a sum/integral over the distribution of some latent variables
\[\log(E[X]) \ge E[\log(X)]\]
Consider the general setting where we have a parameter \(\theta\), data \(\mathbf{X}\) and discrete latent variables \(\mathbf{Z}\)
We want to optimize the log-likelihood:
\[\ell(\theta; \mathbf{X}) = \log \sum_{\mathbf{Z}} f(\mathbf{X}, \mathbf{Z} | \theta) = \log \sum_{\mathbf{Z}} f(\mathbf{X} | \mathbf{Z}, \theta) p(\mathbf{Z} | \theta)\]
Instead of optimizing the log-likelihood directly, we’ll optimize its lower bound. Define some arbitrary distribution on the latent variables \(q(\mathbf{Z})\).
Multiply by 1:
\[\ell(\theta; \mathbf{X}) = \log \sum_{\mathbf{Z}} f(\mathbf{X} | \mathbf{Z}, \theta) p(\mathbf{Z} | \theta) \frac{q(\mathbf{Z})}{q(\mathbf{Z})}\]
Rearranging terms, we can see that this can be written as an expectation over the distribution \(q\)
\[\ell(\theta; \mathbf{X}) = \log E_{q} \bigg[\frac{ f(\mathbf{X} | \mathbf{Z}, \theta) p(\mathbf{Z} | \theta)}{q(\mathbf{Z})}\bigg]\]
By Jensen’s inequality, we have a lower bound
\[\ell(\theta; \mathbf{X}) \ge E_{q} \bigg[ \log \bigg(\frac{ f(\mathbf{X} | \mathbf{Z}, \theta) p(\mathbf{Z} | \theta)}{q(\mathbf{Z})}\bigg)\bigg]\]
And by properties of logs, our lower-bound is:
\[\ell(\theta; \mathbf{X}) \ge E_{q}[\log f(\mathbf{X}|\mathbf{Z}, \theta)] + E_{q}[\log p(\mathbf{Z}| \theta)] - E_{q}[\log q(\mathbf{Z})]\]
We can optimize this iteratively by switching between finding an optimal distribution \(q^{(t+1)}\) given parameter values \(\theta^{(t)}\) and finding parameter values \(\theta^{(t+1)}\) given an existing choice of \(q^{(t+1)}\).
Can we find a closed-form “optimal” solution for our update \(q^{(t+1)}\) given \(\theta^{(t)}\)?
\[\ell(\theta^{(t)}; \mathbf{X}) \ge E_{q} \bigg[ \log \bigg(\frac{ f(\mathbf{X} | \mathbf{Z}, \theta^{(t)}) p(\mathbf{Z} | \theta^{(t)})}{q(\mathbf{Z})}\bigg)\bigg]\]
\[\ell(\theta^{(t)}; \mathbf{X}) \ge \sum_{\mathbf{Z}}\log \bigg(\frac{ f(\mathbf{X}, \mathbf{Z} | \theta^{(t)}) }{q(\mathbf{Z})}\bigg) q(\mathbf{Z})\]
Let’s try to get the \(\mathbf{Z}\) out of the log. Start by factoring the joint distribution in the numerator conditioning on \(\mathbf{X}\) instead of \(\mathbf{Z}\)
\[\ell(\theta^{(t)}; \mathbf{X}) \ge \sum_{\mathbf{Z}}\log \bigg(\frac{ f(\mathbf{Z} | \mathbf{X}, \theta^{(t)}) p(\mathbf{X} | \theta^{(t)})}{q(\mathbf{Z})}\bigg) q(\mathbf{Z})\]
Now we can see the optimal choice for \(q^{(t+1)}\) revealed to us. Suppose we plug in \(q = f(\mathbf{Z} | \mathbf{X}, \theta^{(t)})\), the conditional distribution of \(\mathbf{Z}\) given \(\mathbf{X}\) and \(\theta^{(t)}\):
\[\ell(\theta^{(t)}; \mathbf{X}) \ge \sum_{\mathbf{Z}}\log p(\mathbf{X} | \theta^{(t)}) \times f(\mathbf{Z} | \mathbf{X}, \theta^{(t)})\]
The logged term no longer depends on \(\mathbf{Z}\), so pull it out of the sum
\[\ell(\theta^{(t)}; \mathbf{X}) \ge \log p(\mathbf{X} | \theta^{(t)}) \times \sum_{\mathbf{Z}} f(\mathbf{Z} | \mathbf{X}, \theta^{(t)})\]
The sum is equal to \(1\) (sum over a PMF/PDF) and we’re left with the definition of the marginal log-likelihood, so this expression holds with equality
\[\ell(\theta^{(t)}; \mathbf{X}) = \log p(\mathbf{X} | \theta^{(t)})\]
So our choice of distribution over which to take the expectation of the joint likelihood is \(q^{(t+1)} = f(\mathbf{Z} | \mathbf{X}, \theta^{(t)})\)
Let’s go back to our lower bound - for a given value of \(q^{(t+1)}\), we want to find the \(\theta^{(t+1)}\) that maximize the “complete data” log-likelihood
\[\ell(\theta; \mathbf{X}) \ge E_{q^{(t+1)}}[\log f(\mathbf{X}|\mathbf{Z}, \theta)] + E_{q^{(t+1)}}[\log p(\mathbf{Z}| \theta)] - E_{q^{(t+1)}}[\log q^{(t+1)}(\mathbf{Z})]\]
Since the third term doesn’t depend on \(\theta\), we only need to worry about the first two.
\[Q(\theta | \theta^{(t)}) = E_{\mathbf{Z}|\mathbf{X}, \theta^{(t)}}[\log f(\mathbf{X}, \mathbf{Z}| \theta)]\]
The M-step sets \(\theta^{(t+1)}\) to the value of \(\theta\) that maximizes this Q-function
\[\theta^{(t+1)} = \argmax_{\theta} Q(\theta | \theta^{(t)})\]
Each E-step makes the bound touch the log-likelihood at \(\theta^{(t)}\), and each M-step pushes the bound up, so the log-likelihood never decreases from one iteration to the next
For current values \(\beta^{(t)}\), \(\pi^{(t)}\), let’s derive the conditional distribution \(z_i | Y_i, \beta^{(t)}, \pi^{(t)}\) using Bayes’ rule
\[p(z_i = k | Y_i, \beta^{(t)}, \pi^{(t)}) = \frac{p(z_i = k | \pi^{(t)}) f(Y_i | \beta^{(t)}, z_i = k)}{f(Y_i | \beta^{(t)}, \pi^{(t)})}\]
Given \(z_i = k\), the responses are independent Bernoulli draws. And the denominator is just the sum of the numerator over \(k\)
\[\gamma_{ik} = p(z_i = k | Y_i, \beta^{(t)}, \pi^{(t)}) = \frac{\overbrace{\pi_k^{(t)}}^{\text{prior}} \overbrace{\prod_{j=1}^J \big(\beta_{kj}^{(t)}\big)^{Y_{ij}}\big(1 - \beta_{kj}^{(t)}\big)^{1 - Y_{ij}}}^{\text{likelihood of } Y_i \text{ under cluster } k}}{\underbrace{\sum_{l=1}^K \pi_l^{(t)} \prod_{j=1}^J \big(\beta_{lj}^{(t)}\big)^{Y_{ij}}\big(1 - \beta_{lj}^{(t)}\big)^{1 - Y_{ij}}}_{\text{prior} \times \text{likelihood, summed over all clusters}}}\]
These weights, \(\gamma_{ik}\) are sometimes called the “responsibility” parameters as they denote the extent to which each cluster is “responsible” for an observation.
Recall the “complete” log-likelihood
\[\ell(\beta, \pi ; \mathbf{Y}, \mathbf{Z}) = \sum_{i=1}^N \sum_{k=1}^K I(z_i = k) \bigg[\log \pi_k + \sum_{j=1}^J \Big(Y_{ij} \log \beta_{kj} + (1 - Y_{ij}) \log (1 - \beta_{kj})\Big)\bigg]\]
Now, taking the expectation over \(z_i\), the only component that is not a constant is \(I(z_i = k)\) and \(E[I(z_i = k)] = p(z_i = k)\) (fundamental bridge).
So our Q function is
\[Q(\beta, \pi | \beta^{(t)}, \pi^{(t)}) = \sum_{i=1}^N \sum_{k=1}^K \gamma_{ik} \bigg[\log \pi_k + \sum_{j=1}^J \Big(Y_{ij} \log \beta_{kj} + (1 - Y_{ij}) \log (1 - \beta_{kj})\Big)\bigg]\]
\(\beta^{(t)}\) and \(\pi^{(t)}\) only enter through the responsibilities \(\gamma_{ik}\)
Closed form solutions for the M-step: each \(\beta_{kj}\) is a weighted Bernoulli MLE, and \(\pi\) comes from maximizing \(\sum_k n_k \log \pi_k\) subject to \(\sum_k \pi_k = 1\)
\[n_k = \sum_{i=1}^N \gamma_{ik}\] \[\pi_k = \frac{n_k}{N}\] \[\beta_{kj} = \frac{1}{n_k}\sum_{i=1}^N \gamma_{ik} Y_{ij}\]
Compare to K-means: \(\beta_{kj}\) is the share supporting policy \(j\) in cluster \(k\), but every respondent counts toward every cluster in proportion to \(\gamma_{ik}\)
EM works just as well for finding the posterior mode – add the log prior to the Q-function
\[Q(\beta, \pi | \beta^{(t)}, \pi^{(t)}) + \log p(\beta) + \log p(\pi)\]
With conjugate priors, the M-step stays in closed form
\[\beta_{kj} = \frac{\sum_{i=1}^N \gamma_{ik} Y_{ij} + a - 1}{n_k + a + b - 2} \qquad \pi_k = \frac{n_k + \alpha - 1}{N + K(\alpha - 1)}\]
The prior acts like pseudo-observations added to each cluster
Below we use \(a = b = 2\) and a flat prior on \(\pi\) (\(\alpha = 1\))
bmm_em <- function(Y, K, a = 2, b = 2, maxit = 5000, tol = 1e-6){
## Initialize with random responsibilities
gamma <- matrix(runif(nrow(Y) * K), ncol = K)
gamma <- gamma / rowSums(gamma)
curr_post <- -Inf
for (iter in 1:maxit){
# M-step
params <- bmm_mstep(Y, gamma, a, b)
# E-step
gamma <- bmm_estep(Y, params$beta, params$pi)
# Check convergence: change in the per-observation log posterior (up to a constant)
log_lik <- bmm_loglik(Y, params$beta, params$pi)
new_post <- log_lik + (a - 1) * sum(log(params$beta)) + (b - 1) * sum(log(1 - params$beta))
if (abs(new_post - curr_post) / nrow(Y) < tol) break
curr_post <- new_post
}
if (iter == maxit) warning("Maxit reached")
list(beta = params$beta, pi = params$pi, gamma = gamma, log_lik = log_lik, log_post = new_post, iter = iter)
}conservative_items <- c("concealed_carry", "more_police", "border_patrols", "border_wall", "abortion_exceptions",
"abortion_ban", "more_fossil_fuels", "medicaid_work_req", "repeal_aca")
liberal_answer <- 1 - colnames(Y) %in% conservative_items # 1 = "Support" is the liberal answer
order_clusters <- function(fit){
liberal_share <- colMeans(liberal_answer * t(fit$beta) + (1 - liberal_answer) * (1 - t(fit$beta)))
o <- order(liberal_share, decreasing = T)
list(beta = fit$beta[o, , drop = F], pi = fit$pi[o], gamma = fit$gamma[, o, drop = F], log_lik = fit$log_lik, log_post = fit$log_post)
} mixture
kmeans 1 2
1 698 8105
2 15650 0
[1] -296879 -296879 -296879 -296879 -296978 -296879 -296879 -296879 -296879
[10] -296879
ideology
cluster Very liberal Liberal Moderate Conservative Very conservative Not sure
1 0.39 0.36 0.18 0.01 0.00 0.05
2 0.10 0.29 0.46 0.06 0.01 0.07
3 0.07 0.14 0.44 0.16 0.08 0.11
4 0.03 0.09 0.40 0.23 0.09 0.16
5 0.00 0.01 0.17 0.44 0.36 0.02
commonweight) [,1] [,2] [,3] [,4] [,5]
unweighted 0.189 0.159 0.193 0.246 0.213
weighted 0.186 0.152 0.194 0.264 0.204
bmm_stan_code <- "
data {
int N; int J; int K;
matrix[N, J] Y; // 1 = Support, 0 = Oppose
}
parameters {
simplex[K] pi; // cluster prevalences (flat prior by default)
matrix<lower=0, upper=1>[J, K] beta; // response probabilities
}
model {
matrix[N, K] lp = Y * log(beta) + (1 - Y) * log1m(beta); // log f(Y_i | z_i = k), as in bmm_complete_loglik
to_vector(beta) ~ beta(2, 2);
for (i in 1:N) target += log_sum_exp(log(pi) + lp[i]'); // log of the sum over clusters
}
"bmm_stan_fit <- stan(
model_code = bmm_stan_code, # Stan code
data = list(N = nrow(Y), J = ncol(Y), K = 5, Y = Y), # named list of data
chains = 4, # number of Markov chains
warmup = 500, # number of warmup iterations per chain
iter = 1000, # total number of iterations per chain
cores = 4, # number of cores
refresh = 0, # no progress shown
seed = 53706 # random seed
)relabel <- function(beta, pi){ # beta is J x K in Stan
liberal_share <- colMeans(liberal_answer * beta + (1 - liberal_answer) * (1 - beta))
o <- order(liberal_share, decreasing = T)
list(beta = t(beta[, o]), pi = pi[o]) # K x J, like the EM estimates
}
relabel_draws <- function(fit){
draws <- rstan::extract(fit)
relabeled <- map(1:nrow(draws$pi), function(s) relabel(draws$beta[s, , ], draws$pi[s, ]))
list(beta = simplify2array(map(relabeled, "beta")), # K x J x draws
pi = simplify2array(map(relabeled, "pi"))) # K x draws
}
stan_random <- relabel_draws(bmm_stan_fit) [,1] [,2] [,3] [,4] [,5]
EM (best start) 0.189 0.159 0.193 0.246 0.213
EM (worst start) 0.276 0.179 0.149 0.172 0.224
Stan (posterior mean) 0.276 0.177 0.149 0.173 0.225
em_init <- list(pi = bmm5$pi, beta = t(bmm5$beta)) # beta is J x K in Stan
bmm_stan_fit_em <- stan(
model_code = bmm_stan_code, data = list(N = nrow(Y), J = ncol(Y), K = 5, Y = Y),
chains = 4, warmup = 500, iter = 1000, cores = 4, refresh = 0, seed = 53706,
init = rep(list(em_init), 4) # the same starting values for all four chains
)
stan_em <- relabel_draws(bmm_stan_fit_em) [,1] [,2] [,3] [,4] [,5]
EM (best start) 0.189 0.159 0.193 0.246 0.213
Stan (started at EM) 0.188 0.159 0.194 0.246 0.213
set.seed(60637)
n_folds <- 5
cv_fold <- sample(rep(1:n_folds, length.out = nrow(Y)))
heldout <- expand_grid(K = K_choice, fold = 1:n_folds) %>%
mutate(ll = map2_dbl(K, fold, function(K, f){
fold_fit <- bmm_em_runs(Y[cv_fold != f, ], K, runs = 3)
bmm_loglik(Y[cv_fold == f, ], fold_fit$beta, fold_fit$pi) / sum(cv_fold == f)
}))Under the mixture model, the implied covariance between questions is \[\text{Cov}(Y_{ij}, Y_{il}) = \sum_{k=1}^K \pi_k \beta_{kj}\beta_{kl} - p_j p_l\]
Like a posterior predictive check, but plugging in the EM estimates: does the model reproduce the observed correlations among the various pairs of questions?
fewer_police
more_police 0 1
0 4869 5572
1 11970 2042
| Cluster | Implied by local independence | Observed |
|---|---|---|
| 1 | 3% | 2% |
| 2 | 6% | 3% |
| 3 | 30% | 25% |
| 4 | 15% | 10% |
| 5 | 2% | 1% |
Mixture model (“latent class”): a discrete \(z_i\) for each respondent
\[Pr(Y_{ij} = 1 | z_i = k) = \beta_{kj}\]
Item response theory (“latent trait”): a continuous \(\theta_i\) for each respondent, and a slope and intercept for each question
\[Pr(Y_{ij} = 1 | \theta_i) = F(\beta_j\theta_i - \tau_j)\]
Both assume local independence conditional on the underlying latent variable
But IRT uses a single continuous trait rather than a discrete classification.
Maybe a single dimension works better here?
Next week: fit IRT to the same 24 questions and see what we learn.
PS 818 - University of Wisconsin-Madison