Mixed-effects models in R, with lme4-style formulas and a Rust engine.
mixeff fits linear and generalized linear mixed-effects models through the mixeff-rs engine. The R interface uses familiar formulas and extractors — fixef, ranef, VarCorr, predict, anova, summary, update, broom::tidy, and (for LMMs) simulate — while keeping the compiled model, optimizer result, and inference metadata available for inspection.
It is not a drop-in replacement for lme4: use lmm() or glmm() rather than lmer() or glmer(), and do not expect bit-identical estimates. The target is statistical agreement within documented tolerances on the package’s parity datasets.
Documentation: https://bbuchsbaum.github.io/mixeff/
Why mixeff?
Three parts of the design are useful in practice:
-
Fast repeated fitting. On the LMM scaling benchmark shipped with the package,
mixeffwas faster thanlme4at every tested size, with small absolute timings either way; the measured numbers are in the benchmarking article. This matters most for bootstrap, simulation, and sensitivity analyses, where the same model may be refit hundreds or thousands of times. -
You can inspect the model before fitting it.
compile_model()turns the formula into an explicit model specification.audit()shows the canonical formula, random-effects structure, covariance parameterization, and design diagnostics before optimization starts. -
Fit diagnostics are data, not only console text. Convergence status, optimizer details, inference method, reliability, and reason codes live in structured objects. A fitted model is serializable with
saveRDS()and can be inspected after reloading without a live Rust handle.
These features do not remove the need to evaluate a model statistically. They make the computation and the software’s decisions easier to examine.
Installation
From R-Universe:
install.packages(
"mixeff",
repos = c("https://bbuchsbaum.r-universe.dev", getOption("repos"))
)From GitHub (requires Rust 1.85 or newer):
remotes::install_github("bbuchsbaum/mixeff")A short example
Compile and inspect a model before fitting it:
library(mixeff)
spec <- compile_model(
Reaction ~ Days + (Days | Subject),
lme4::sleepstudy
)
audit(spec)
#> Audit Summary:
#> overall [OK]: clean: no warnings or attention items
#> attention [OK]: no warnings or unchecked inference-critical items
#>
#> Requested Model:
#> formula [INFO]: Reaction ~ 1 + Days + (1 + Days | Subject)
#> model kind [INFO]: linear_mixed_model
#> distribution/link [INFO]: gaussian/identity
#> objective [INFO]: exact_gaussian
#> convergence certificate [INFO]: exact_objective
#> fixed terms [INFO]: 1, Days
#> random terms [INFO]: 1
#> covariance parameter maps [INFO]: 1 map(s)Then fit the model with the Rust engine and use the usual R extractors:
fit <- lmm(
Reaction ~ Days + (Days | Subject),
data = lme4::sleepstudy,
control = mm_control(verbose = -1)
)
summary(fit)
fixef(fit)
VarCorr(fit)
ranef(fit)The optimizer certificate is a data frame you can subset like any other. Here is a compact view of the sleep-study fit:
cert <- optimizer_certificate(fit)$table
subset(cert, metric %in% c("status", "optimizer", "iterations"))
#> metric value
#> status converged_interior
#> optimizer trust_bq
#> iterations 457Other inspection functions expose different parts of the same fitted object:
changes(fit) # changes made while compiling the request
diagnostics(fit) # structured warnings and status information
parameterization(fit) # random-effects covariance parameterization
inference_table(fit) # method, status, and reliability by coefficient
reproducibility(fit) # schema and engine metadataBecause the fitted quantities are stored in the R object, ordinary R serialization works:
saveRDS(fit, path <- tempfile())
restored <- readRDS(path)
fixef(restored)
optimizer_certificate(restored)revive(restored) resets the object’s process-local cache after serialization; it is cheap and safe to call after every readRDS(). The current bridge intentionally leaves the Rust handle absent.
Performance
The committed scaling benchmark fits the same LMM with mixeff::lmm() and lme4::lmer() across five common random-effects designs. On that benchmark, mixeff was faster at every tested size, with small absolute timings in every cell — the behavior of one harness, not a universal speed guarantee. The measured timings live in the benchmarking article, and the scripts and full CSV are included so the comparison can be rerun on a relevant machine and model:
inst/benchmarks/lme4-scaling.Rinst/extdata/lme4-scaling-summary.csv- the benchmarking article
Inspecting random-effects structure
explain_model() describes what a random-effects formula estimates and what the design can support. It does not rank or replace candidate models.
explain_model(compile_model(score ~ week + (1 | clinic), df))
#> Random effects explanation:
#> formula: score ~ 1 + week + (1 | clinic)
#>
#> Random effects:
#> r0:
#> wrote: (1 | clinic)
#> canonical: (1 | clinic)
#> named form: re(group = clinic, intercept = TRUE, slopes = NULL, cov = "scalar")
#> scope: `clinic` units may differ in average outcome.
#> covariance: scalar; theta parameters: 1
#> support: sufficient; group levels: 8; min rows/group: 6; median rows/group: 6
#> variation: intercept=not_assessed
#>
#> Design notes:
#> scope_note: `week` varies within `clinic`, so a `clinic`-level slope is structurally possibleSplit-block, double-bar, and nested formulas are expanded explicitly and labeled as syntax expansions; see vignette("lmm").
Numerical compatibility with lme4
mixeff does not target bit-exact reproduction of lme4; it targets statistical agreement within documented tolerances on the parity datasets, and the family/link table ships as a machine-readable registry (supported_models()).
glmm() defaults to method = "joint_laplace", glmer’s joint Laplace estimator, with certified Wald inference. The opt-in fast profiled PIRLS estimator (method = "pirls_profiled", lme4’s nAGQ = 0) is not the same estimator as glmer(), and it returns point estimates only — standard errors, z tests, p-values, and confidence intervals are withheld with a note naming the alternative. Negative-binomial families, which have no joint route yet, use the profiled path (announced). See the GLMM vignette for the current boundaries.
Acknowledgements
The mixeff-rs engine is modeled on Julia’s MixedModels.jl, and follows its staged design: formula parser, semantic representation, covariance parameterization, optimizer, inference rules.