---
title: Migrating from lme4
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{Migrating from lme4}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r setup, include = FALSE}
knitr::opts_chunk$set(
  collapse = TRUE,
  comment = "#>",
  message = FALSE,
  warning = FALSE
)
has_lme4 <- requireNamespace("lme4", quietly = TRUE)
```

```{r load}
library(mixeff)
```

```{r sleepstudy-helper, include = FALSE}
# vignette-local convenience so chunks run whether or not lme4 is attached;
# use lme4::sleepstudy directly in your own code.
sleepstudy_data <- function() {
  if (requireNamespace("lme4", quietly = TRUE)) {
    e <- new.env()
    utils::data("sleepstudy", package = "lme4", envir = e)
    e$sleepstudy
  } else {
    set.seed(1)
    data.frame(
      Reaction = rnorm(180, 300, 50),
      Days = rep(0:9, 18),
      Subject = factor(rep(seq_len(18), each = 10))
    )
  }
}
```

`mixeff` aims to be *functionally equivalent* to `lme4`: the same formula
language, the same extractor surface, and statistical answers that agree within
documented tolerances. It is **not** a literal drop-in — you call `lmm()` /
`glmm()` rather than `lmer()` / `glmer()`, results are not bit-exact, and the
package is audit-first (it reports or refuses rather than silently transforming
a model). This vignette is the verb-for-verb, argument-for-argument map.

## The two edits

An `lmer` script becomes an `lmm` script with two changes: the fitting verb and
the control object.

```{r fit, eval = TRUE}
fit <- lmm(Reaction ~ Days + (Days | Subject), sleepstudy_data(),
           control = mm_control(verbose = -1))
fixef(fit)
```

```{r fit-lme4, eval = has_lme4}
m <- lme4::lmer(Reaction ~ Days + (Days | Subject), lme4::sleepstudy)
lme4::fixef(m)
```

(`sleepstudy_data()` above just returns `lme4::sleepstudy` when lme4 is
installed; use `lme4::sleepstudy` directly in your own code.)

## Verb map

| lme4 | mixeff | Notes |
|------|--------|-------|
| `lmer(y ~ x + (x \| g), data)` | `lmm(y ~ x + (x \| g), data)` | same formula language, incl. `(x\|\|g)`, `(1\|g1/g2)`, crossed |
| `glmer(y ~ ..., family = binomial)` | `glmm(y ~ ..., family = binomial())` | pass a family **object** (`binomial()`), not a string |
| `lmerControl(...)` / `glmerControl(...)` | `mm_control(verbose=, max_feval=)` | optimizer/tolerance knobs are engine-chosen (see below) |
| `fixef`, `ranef`, `VarCorr`, `coef`, `sigma`, `vcov` | identical | same generics |
| `logLik`, `AIC`, `BIC`, `deviance`, `nobs`, `confint` | identical | `confint` supports Wald, profile (LMM), bootstrap (LMM) |
| `predict`, `fitted`, `residuals`, `simulate`, `refit` | identical | `predict()` supports `re.form = NULL/NA`, `se.fit`, `interval` (population) |
| `update(fit, . ~ . - x)` | identical | formula edits, `REML=`, `weights=`, etc. |
| `anova(m1, m2)`, `drop1`, `getME`, `ngrps`, `isSingular` | identical | `isSingular()` is `is_singular()` |
| `broom.mixed::tidy/glance/augment` | identical | registered for `mm_lmm`/`mm_glmm` |
| `emmeans::emmeans(fit, ~ x)` | identical | mixeff registers an emmeans basis |
| `lmerTest` p-values in `summary()` | built in | Satterthwaite/Kenward-Roger native, no extra package |

## Argument map for `lmm()` / `glmm()`

| lme4 argument | mixeff | Notes |
|---------------|--------|-------|
| `REML` | `lmm(..., REML=)` | same |
| `weights` | `weights=` | LMM and GLMM |
| `offset` | `glmm(..., offset=)` | GLMM only; LMM in-fit offset is not yet supported |
| `subset` | `lmm(..., subset=)` | supported for `lmm()` |
| `na.action` | `lmm(..., na.action=)` | **default refuses NA**; pass `na.action = na.omit` for lme4's complete-case behaviour |
| `contrasts` | partial | unordered factors use treatment coding, ordered factors `contr.poly` (both matching R/lme4 defaults); other codings are refused — recode the factor |
| `family = "binomial"` | `family = binomial()` | string families are not accepted |
| `nAGQ` | `glmm(..., nAGQ=)` | `>1` on the profiled path |
| `control = lmerControl(optimizer=, optCtrl=)` | `mm_control(optimizer=, max_feval=, ...)` | the engine picks a default optimizer; `mm_control()` can override it or cap the evaluation budget |
| `start` | `mm_control(start=)` | theta warm starts |

## Four things that will bite, and the fix

**1. Coefficient names match lme4 exactly.** Since 0.2.0, `fixef()`,
`summary()` tables, `vcov()` dimnames, and `mm_lincomb()` weight names use
lme4's naming and column order (`"recipeB"`, `"temperature.L"`,
`"recipeB:temperature.L"`), so name-keyed lme4 code is drop-in compatible.
(Earlier versions used an engine encoding like `"recipe: B"`; if you wrote
normalisation shims for those, delete them.)

**2. Grouped binomial responses.** `glmm()` accepts the
`cbind(successes, failures)` spelling like `glmer`:

```{r cbind, eval = FALSE}
glmm(cbind(incidence, size - incidence) ~ period + (1 | herd),
     lme4::cbpp, family = binomial())
```

**3. The default GLMM estimator is not glmer's.** `glmm()` defaults to a fast
profiled (PIRLS) estimator whose coefficients do **not** match `glmer()`
exactly; it prints a notice saying so. For glmer-equivalent (joint Laplace)
estimates, ask for them:

```{r joint, eval = FALSE}
glmm(y ~ x + (1 | g), data, family = binomial(),
     method = "joint_laplace")
```

**4. `||` with a factor means full decorrelation.** In mixeff, `||` fixes
*every* covariance in the block at zero — including the covariances among a
factor's level contrasts (each treatment-coded contrast gets an independent
variance). lme4's `||` does **not** split factor terms: a factor keeps its
full within-factor covariance block. So `(1 + cond + x || subj)` with a
factor `cond` fits a strictly larger model in lme4 than in mixeff, and the
two disagree on the parameter count (hence `df`, AIC, and — when the fitted
within-factor covariance is non-zero — the optimum itself). mixeff announces
the situation at compile time with an info diagnostic
(`covariance_assumption`, reason `double_bar_factor_term`). To reproduce
lme4's model family exactly, write the expansion explicitly and give the
factor its own correlated block:

```{r doublebar-factor, eval = FALSE}
# mixeff `||`: independent variances for every column, factor levels included
glmm(y ~ cond * x + (1 + cond + x || subj), data, family = binomial())

# lme4-equivalent family: the factor keeps its within-factor covariance block
glmm(y ~ cond * x + (1 | subj) + (0 + cond | subj) + (0 + x | subj),
     data, family = binomial())
```

## What is `NA`-with-a-reason (and why)

`mixeff` never fabricates inference it cannot certify. Where lme4 would silently
return a number (or silently drop data), mixeff returns `NA` with a machine-
readable reason or raises a typed condition:

| Situation | lme4 | mixeff |
|-----------|------|--------|
| `NA` in a model variable | silently dropped | refused unless `na.action = na.omit` |
| Boundary (singular) fit | one-time warning | persistent `[boundary]` tag + effective rank |
| Satterthwaite df at a boundary | may print unreliable df | refused with a reason; use bootstrap |
| Conditional prediction SE | not provided | `NA` with reason (population SE *is* provided) |
| GLMM `confint(method="profile")` | computed | refused (only Wald is certified for GLMMs) |

Use `inference_options(fit)` to see, before you run anything, which inference
routes are available on a given fit and why.
```
