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/
Three parts of the design are useful in practice:
mixeff was faster than lme4 at 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.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.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.
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")
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 457
Other 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 metadata
Because 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.
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.csvexplain_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 possible
Split-block, double-bar, and nested formulas are expanded explicitly and
labeled as syntax expansions; see vignette("lmm").
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.
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.
MIT, plus the upstream Rust crate license bundle in inst/LICENSE.note.