Skip to contents

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, 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.
  • 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                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.

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:

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 possible

Split-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.

License

MIT, plus the upstream Rust crate license bundle in inst/LICENSE.note.