--- title: "Reproducing the Loo Aphantasia GLMMs" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Reproducing the Loo Aphantasia GLMMs} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r setup, include = FALSE} run_live <- identical(tolower(Sys.getenv("MIXEFF_RUN_APHANTASIA_VIGNETTE")), "true") knitr::opts_chunk$set( collapse = TRUE, comment = "#>", message = FALSE, warning = FALSE ) ``` ```{r load-package} library(mixeff) ``` The fixture bundled with `mixeff` is an anonymized copy of the trial-level data used by the revision 3 Loo aphantasia manuscript analysis. It is intended as a real GLMM reproduction target: large enough to expose optimizer and naming drift, but small enough to ship as package test data. ```{r load-fixture} fixture_candidates <- c( system.file("extdata", "aphantasia", package = "mixeff"), file.path("..", "inst", "extdata", "aphantasia"), file.path("inst", "extdata", "aphantasia") ) fixture_dir <- fixture_candidates[ dir.exists(fixture_candidates) & nzchar(fixture_candidates) ][1L] trials <- readRDS(file.path(fixture_dir, "trials.rds")) metadata <- readRDS(file.path(fixture_dir, "metadata.rds")) reference <- jsonlite::fromJSON(file.path(fixture_dir, "reference.json")) c( trials = nrow(trials), participants = length(unique(trials$participant)), metadata_rows = nrow(metadata) ) ``` The primary analysis uses the occluded trials, excludes the four intermediate VVIQ controls, and models accuracy with crossed participant and item effects. ```{r prepare-data} excluded <- unlist(reference$excluded_participants, use.names = FALSE) primary <- subset( trials, bubbled == "yes" & !is.na(correct) & !participant %in% excluded ) prepare_model_data <- function(dat, stimtype = FALSE) { out <- transform( dat, participant = factor(participant), item = factor(trial_image), group = factor(ifelse(aphantasia == "yes", "aphant", "control"), levels = c("control", "aphant")), mask = factor(ifelse(back_masked == "yes", "masked", "unmasked"), levels = c("unmasked", "masked")), block = factor(block_num), soa_log = log(SOA) ) out$soa_s <- as.numeric(scale(out$soa_log)) if (stimtype) { out$stimtype <- factor( ifelse(out$bubbled == "yes", "occluded", "intact"), levels = c("intact", "occluded") ) } out } primary_dat <- prepare_model_data(primary) table(primary_dat$group, primary_dat$mask) ``` The live fits are opt-in because the primary GLMM alone takes several minutes on a laptop. Set `MIXEFF_RUN_APHANTASIA_VIGNETTE=true` to execute the model chunks while rendering. The test suite uses the same principle: the core reproduction runs under `MIXEFF_RUN_APHANTASIA=true`, and the slower S1 random-effects stability variants run under `MIXEFF_RUN_APHANTASIA_STRESS=true`. ```{r primary-fit, eval = run_live} primary_fit <- glmm( correct ~ group * mask * soa_s + block + (1 + mask + soa_s || participant) + (1 | item), primary_dat, family = binomial(), control = mm_control(verbose = -1) ) c( logLik = as.numeric(logLik(primary_fit)), AIC = AIC(primary_fit) ) fixef(primary_fit) ``` Without live refitting, the frozen lme4 reference records the target values the integration test compares against. ```{r primary-reference} unlist(reference$models$primary[c("nobs", "logLik", "AIC")]) unlist(reference$models$primary$fixef) ``` The same fixture supports the manuscript sensitivity and specificity fits: the sensitivity model assigns the four intermediate VVIQ participants to the control group. The intact high-baseline Bernoulli model defaults to the full-budget joint-Laplace route (`method = "joint_laplace"`) in the opt-in reproduction gate, reaching near-exact lme4 fixed-effect and log-likelihood parity on a release build (~40 s per fit; only the AIC parameter-count semantics for the double-bar factor expansion remain ledgered). The combined model stays on the profiled ledger path: the engine rejects its joint candidate for that case and falls back to fast-PIRLS with an explicit `documented_divergence` diagnostic. ```{r additional-glmms, eval = run_live} sensitivity <- subset(trials, bubbled == "yes" & !is.na(correct)) sensitivity$aphantasia[sensitivity$participant %in% excluded] <- "no" sensitivity_dat <- prepare_model_data(sensitivity) intact <- subset( trials, bubbled == "no" & !is.na(correct) & !participant %in% excluded ) intact_dat <- prepare_model_data(intact) combined <- subset(trials, !is.na(correct) & !participant %in% excluded) combined_dat <- prepare_model_data(combined, stimtype = TRUE) sensitivity_fit <- glmm( correct ~ group * mask * soa_s + block + (1 + mask + soa_s || participant) + (1 | item), sensitivity_dat, family = binomial(), control = mm_control(verbose = -1) ) intact_fit <- glmm( correct ~ group * mask * soa_s + block + (1 + mask + soa_s || participant) + (1 | item), intact_dat, family = binomial(), control = mm_control(verbose = -1) ) combined_fit <- glmm( correct ~ group * mask * soa_s * stimtype + block + (1 + mask + soa_s || participant) + (1 | item), combined_dat, family = binomial(), control = mm_control(verbose = -1) ) rbind( sensitivity = c(logLik = as.numeric(logLik(sensitivity_fit)), AIC = AIC(sensitivity_fit)), intact = c(logLik = as.numeric(logLik(intact_fit)), AIC = AIC(intact_fit)), combined = c(logLik = as.numeric(logLik(combined_fit)), AIC = AIC(combined_fit)) ) ``` The RT sensitivity is a Gaussian LMM over correct trials with finite positive reaction times. ```{r rt-lmm, eval = run_live} rt_dat <- subset(primary_dat, correct == 1 & is.finite(rt) & rt > 0) rt_dat$log_rt <- log(rt_dat$rt) rt_fit <- lmm( log_rt ~ group * mask * soa_s + block + (1 | participant) + (1 | item), rt_dat, REML = FALSE, control = mm_control(verbose = -1) ) c(logLik = as.numeric(logLik(rt_fit)), AIC = AIC(rt_fit)) fixef(rt_fit) ``` ## Inferential surfaces Since `mixeff-rs` started serializing the GLMM fixed-effect covariance artifact (pin 5e72e0b), the inferential surfaces the manuscript actually reports are available through three `mixeff` primitives. The chunks below run only when live fitting is enabled (`MIXEFF_RUN_APHANTASIA_VIGNETTE=true`); otherwise the same calls remain valid against any locally-built fit. `summary(fit, tests = "coefficients")` returns a Wald-z fixed-effect table built from the PIRLS/Laplace working-Hessian covariance: ```{r primary-summary, eval = run_live} sm <- summary(primary_fit, tests = "coefficients") round(sm$coefficients, 3) sm$vcov_status ``` The status block flags `reliability = "moderate"`: the working-Hessian flavor is close to but not bit-identical with `lme4::vcov(glmer_fit)`. SE estimates on this dataset drift by ~5-10% in absolute terms, without flipping any of the manuscript's qualitative conclusions. The manuscript's primary estimand --- the difference-in-differences contrast at the centered SOA and at the focal 25 ms SOA --- is a linear combination of fixed effects, and `mm_lincomb()` is its front door: ```{r primary-lincomb, eval = run_live} soa_s_25 <- (log(0.025) - mean(primary_dat$soa_log)) / sd(primary_dat$soa_log) dd_center <- mm_lincomb( primary_fit, c("groupaphant:maskmasked" = 1) ) dd_25 <- mm_lincomb( primary_fit, c("groupaphant:maskmasked" = 1, "groupaphant:maskmasked:soa_s" = soa_s_25) ) list(centered_soa = dd_center, ms25 = dd_25) ``` Compare directly against the `lme4` reference frozen in the fixture: ```{r dd-vs-reference} as.data.frame(reference$inference$primary_dd) ``` Both contrasts reproduce the manuscript's sign and significance class: negative (larger masking cost in aphantasia), CIs excluding zero, and *p* below the conventional α = .05 at both 25 ms and the centered SOA. `emmeans` works on `mm_glmm` via `emm_basis.mm_glmm`. Population-level cell means at the centered SOA, on the response (probability) scale, recover the same group × mask × group pattern the manuscript reports: ```{r primary-emm, eval = run_live && requireNamespace("emmeans", quietly = TRUE)} em <- emmeans::emmeans( primary_fit, ~ mask | group, at = list(soa_s = 0), type = "response" ) as.data.frame(summary(em)) ``` The reverse-pairwise contrast inside each group is the group-conditional masking cost on the log-odds scale: ```{r primary-emm-contrast, eval = run_live && requireNamespace("emmeans", quietly = TRUE)} emmeans::contrast( emmeans::emmeans(primary_fit, ~ mask | group, at = list(soa_s = 0)), method = "revpairwise" ) ``` ## Out of scope here The full sensitivity, intact-stimulus, combined-stimtype, log-RT, S1 random-effects-spec stability, S7 age-covariate, and S9 folder-based age-matched analyses are reproducible against the same fixture and reference via `tests/testthat/test-aphantasia-reproduction.R` when `MIXEFF_RUN_APHANTASIA=true` is set. The S3 leave-one-participant-out sweep, S4 specification curve, and S5 rstanarm posterior are intentionally not part of the regular reproduction surface: the first two are heavy opt-in jobs, and `mixeff` is not a Bayesian engine. ## Caveats * GLMM Wald inference in `mixeff` is the PIRLS/Laplace working-Hessian flavor, advertised as `mm_reliability = "moderate"`. Absolute SEs drift 5--10% versus `lme4::vcov()` on this dataset; a hold-the-point experiment attributes ~73% of that to the native `||` random-effect family, ~26% to a uniform working-Hessian scale factor (tracked upstream), and ~1% to optimizer drift. * Population-level GLMM prediction (`re.form = NA`, `type = "link"` or `"response"`) is supported and matches `predict(glmer, re.form = NA)` on joint-Laplace fits; `emmeans(..., type = "response")` remains the route for averaged marginal probabilities. * Coefficient names are lme4-identical since 0.2.0 (`"groupaphant"`), so hand-written lincombs copy straight across from `lme4` code. ## Citation If you use the bundled fixture or reproduce these analyses in downstream work, please cite: > Loo, C., & Buchsbaum, B. R. (2026). Fragile recurrent processing in > aphantasia: Evidence from visual pattern completion. *Manuscript > submitted for publication.*