## ----setup, include = FALSE--------------------------------------------------- knitr::opts_chunk$set( collapse = TRUE, comment = "#>", message = FALSE, warning = FALSE, fig.width = 7, fig.height = 4.3, dpi = 150, fig.align = "center" ) set.seed(2025) ## ----libs--------------------------------------------------------------------- library(mixqrgate) library(ggplot2) pal <- c("Supportive" = "#1b6ca8", "Under-resourced" = "#e07b39") theme_set(theme_minimal(base_size = 12)) ## ----data--------------------------------------------------------------------- raw <- sim_gate2(n = 800, gamma = c(-0.3, 1.3), b1 = c(48, 7), b2 = c(55, 2), sigma = c(6, 7)) schools <- data.frame(score = raw$y, ses = raw$x, funding = raw$z) head(round(schools, 2)) ## ----rawplot, fig.alt = "Scatter of score against SES, two latent regimes overlapping."---- # class 1 is, by construction, the steep-gradient (under-resourced) regime schools$truth <- ifelse(raw$class == 1, "Under-resourced", "Supportive") ggplot(schools, aes(ses, score, colour = truth)) + geom_point(alpha = 0.5, size = 1.3) + scale_colour_manual(values = pal, name = "True regime") + labs(x = "Student SES (standardised)", y = "Test score", title = "Two regimes are present but entangled") + theme(legend.position = "top") ## ----fit---------------------------------------------------------------------- fit <- mixqrgate(score ~ ses, data = schools, gating = ~ funding, G = 2, tau = 0.5, variance = "louis") fit ## ----summary------------------------------------------------------------------ summary(fit) ## ----oddsratio---------------------------------------------------------------- g <- coef(fit, "gating")["funding", 1, 1] cat(sprintf("odds ratio per 1 SD of funding: %.2f\n", exp(g))) confint(fit) # gate-coefficient confidence intervals ## ----vcompare----------------------------------------------------------------- se <- function(v) sqrt(diag(vcov(mixqrgate(score ~ ses, data = schools, gating = ~ funding, G = 2, tau = 0.5, variance = v, control = mixqrgate_control(seed = 1))))) rbind(sandwich = se("sandwich"), louis = se("louis")) ## ----lvdata------------------------------------------------------------------- lv <- sim_gate2(n = 800, gamma = c(-0.3, 1.3), b1 = c(48, 7), b2 = c(55, 2), sigma = c(6, 7), loc_vary = 3) schools_lv <- data.frame(score = lv$y, ses = lv$x, funding = lv$z) ## ----vary--------------------------------------------------------------------- grid <- c(0.1, 0.25, 0.5, 0.75, 0.9) fitv <- mixqrgate(score ~ ses, data = schools_lv, gating = ~ funding, G = 2, tau = grid, vary_gating = "discrete", variance = "louis") round(fitv$gate_prob, 3) ## ----gatevtau, fig.alt = "Average gate probability against the quantile level with uncertainty bands."---- band <- do.call(rbind, lapply(seq_along(grid), function(g) { V <- fitv$gate_vcov[[g]]; gam <- as.numeric(fitv$gamma[, , g]) L <- chol(V + 1e-8 * diag(nrow(V))) d <- replicate(500, { gd <- matrix(gam + as.numeric(crossprod(L, rnorm(length(gam)))), length(fitv$znames)) mean(mixqrgate:::gate_predict(gd, fitv$z)[, 2]) }) data.frame(tau = grid[g], p = mean(d), lo = quantile(d, .025), hi = quantile(d, .975)) })) ggplot(band, aes(tau, p)) + geom_ribbon(aes(ymin = lo, ymax = hi), fill = pal[2], alpha = 0.2) + geom_line(colour = pal[2], linewidth = 1.1) + geom_point(colour = pal[2], size = 2.3) + ylim(0, 1) + labs(x = expression(tau), y = "P(under-resourced regime)", title = "The mix shifts across the score distribution", subtitle = "Per-quantile gate estimates, with simulated uncertainty") ## ----classify, fig.alt = "Posterior probability of the under-resourced regime across the data."---- schools$p_under <- fit$posterior[, 2, 1] ggplot(schools, aes(ses, score, colour = p_under)) + geom_point(size = 1.4) + scale_colour_gradient(low = pal[1], high = pal[2], name = "P(under-resourced)") + labs(x = "Student SES", y = "Test score", title = "Soft classification: confident at the edges, unsure in the middle") ## ----gatecurve, fig.alt = "Fitted probability of each regime as a function of school funding."---- nd <- data.frame(funding = seq(-2.5, 2.5, length.out = 100)) pp <- predict(fit, newdata = nd, type = "prob") gd <- data.frame(funding = rep(nd$funding, 2), prob = c(pp[, 1], pp[, 2]), regime = rep(c("Supportive", "Under-resourced"), each = 100)) ggplot(gd, aes(funding, prob, colour = regime)) + geom_line(linewidth = 1.2) + scale_colour_manual(values = pal, name = NULL) + labs(x = "School funding (standardised)", y = "Gate probability", title = "The gate: funding shifts the regime mix") + theme(legend.position = "top") ## ----diag--------------------------------------------------------------------- c(converged = all(fit$converged), min_gate_prob = round(min(fit$gate_prob), 3), gate_condition = round(fit$gate_cond, 1)) ## ----report------------------------------------------------------------------- set.seed(2025) fit_final <- mixqrgate(score ~ ses, data = schools, gating = ~ funding, G = 2, tau = 0.5, variance = "louis", control = mixqrgate_control(seed = 2025)) list(slopes = round(fit_final$beta["ses", , 1], 2), gate_funding = round(coef(fit_final, "gating")["funding", 1, 1], 3), gate_prob = round(fit_final$gate_prob[, 1], 3), se_method = fit_final$se_method)