Implement group level mixtures - #1905
Conversation
…ner#1659) Allow finite mixture models to be computed over the levels of a grouping variable rather than over individual observations, so that all observations within a group are assigned to the same (unknown) mixture component. This is requested via the new `gr` argument of `mixture()` (e.g. entire participants belonging to different latent classes). The mixture is marginalized once per group via `log_sum_exp` in the Stan likelihood. Predicted mixing proportions must be constant within each group. `log_lik`, `loo`, and `waic` are computed per group (leave-one-group-out). Not supported (with informative errors): within-chain threading, censoring, truncation, observation weights, multivariate models, `kfold`, `reloo`, `loo_moment_match`, and `loo_subsample`.
|
Thank you very much for this PR! I will review it when I find the time, hopefully during August. Just a quick note: please disclose the use of AI within contributions to brms. Based on the way comments and details are written, I can see AI at play here and I would like to keep a bit track of it where it enters brms. Nothing bad about it obviously (perhaps apart from the very wordy style of commenting), more like a matter of principle here. |
|
Hi Paul, sure, sorry for not directly disclosing the AI use. I sketched out the math myself based on the issue report and explored some additional considerations for the posterior predictive and then used Claude Code for implementation and then checked the code against the math for verification. I am also happy to go over the comments to focus them on the relevant points. If you need more details, feel free to say so. |
Closes #1659.
The changes in this PR implement the feature requested in #1659:
mixture()can compute the mixture over the levels of a grouping variable instead of over individual observations, so a whole group (e.g. a participant) is assigned to one component. The motivation, the likelihood distinction, and the Stan sketch are available in the issue and its linked note.In two places I departed from the proposals from the issue:
mixture(), not in amix()aterm in thebrmsformula. The issue proposedy | mix(gr = "ID") ~ .... That works, but amixaddition term collides with the existingmiterm through R's$partial matching (adforms$miresolves to amixelement when nomiis present), which would force$mi→[["mi"]]edits across ~8 unrelated files. Puttinggron the family avoids that and keeps the change scoped. The only thing the family route doesn't get for free is the grouping column reaching the model frame (built from formula variables), so it is added toallvarsexplicitly inR/brmsterms.R. Feel free to say if you would prefer the grouping specification as anatermin thebrmsformulainstead.log_sum_expover aps[]array, instead of thelog_mixproposed in the issue. This follows other brms code that already builds every mixture aslog_sum_expover per-component terms rather thanlog_mix, so the group-level path reuses that and generalizes to N components for free.Background on some coding choices
During implementation I noted some things that I want to shortly elaborate on. If you notice errors in my thinking, please say so, and I am happy to adapt the PR accordingly.
_lpdfin the accumulator is required. Normalization constants cancel out of an observation-levellog_sum_expbut are summed over the group before mixing here, so dropping them (_lupdf) would change the relative component weights. The grouped path therefore never uses the unnormalized form even whennormalize = FALSE.thetais restricted to group-constant predictors. A per-observation-varying mixing proportion is not meaningful when the component is shared across a group. This is validated instandata; when constant, the proportion is read from a representative observation per group (Jmixrep).log_lik/looneed to change unit for grouped mixtures. The likelihood for grouped mixtures doesn't factorize over observations, solog_likreturns one column per group andloo/waicbecome leave-one-group-out — the correct CV unit here. Baseloo/waic/loo_compare/r_effare column-count agnostic and handle it. The changes in the PR follow the same unit:kfoldforms folds over whole groups,relooandloo_moment_matchrefit/match per group, andloo_subsampleworks because the pointwiselog_likis already per group.kfold_predictworks on saved grouped kfolds; predictions stay per observation while the elpd unit is the group.log_lik(pointwise = TRUE) was broken for all multivariate models withoutrescor, independent of this feature:log_lik_pointwisepassed each response's prepared draws asdata_iinstead ofdraws(lapply(draws$resps, log_lik_pointwise, i = i)). Onlyloo_subsampleexercises this path, which is presumably why it went unnoticed until now.brmsdid already support independent per-response grouped mixtures. So, in the supported multivariate case, each response carries its own mixing proportion and its own per-group component assignment; responses can be coupled only through shared/correlated group-level effects (e.g.(1 | p | ID)across formulas). Thus, a group can be in component 1 for one response and component 2 for another: the joint per-group likelihood is the product of per-response mixtures, not a mixture of products. A single latent class driving all responses (one shared assignment) would require cross-response accumulation into oneLmixbefore a singlelog_sum_exp, cutting against the one-likelihood-block-per-response architecture; I felt that this is out of scope here. I added information to themixture()docs, so they state the independent-assignment semantics explicitly.grouped mixtures over the same grouping variable with
rescor = FALSE(rescoris independently excluded for mixtures byallow_rescor). Then the group remains the pointwise unit jointly: its log-likelihood is the sum of the per-response per-group terms, and every CV method above carries over unchanged. Asymmetric models (grouped mixture + ordinary response) and mismatchedgrvariables are refused with errors that say why: they leavelog_lik/loowithout a common pointwise unit.Evaluation of the implementation
To test the behavior of the implementation, I reproduced Musfeld, Souza & Oberauer (2023, PNAS 120:e2218042120), a per-participant Hebb-repetition mixture (learner vs. non-learner):
log_mixaccumulation, including a shared baseline across components (shared non-linear parameters) and a non-linear hinged-onset learner curve. Fitting all three visual conditions on the authors' OSF data recovers the learner proportion at 0.58–0.59 vs. the published ~0.60, near-identical between groups (their central between-group null), with learning curves matching their Fig 3D.Betaplus endpoint compression;zero_one_inflated_betais currently not allowed insidemixture().)