---
title: "Your First Mixed Model with mixeff"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{Your First Mixed Model with mixeff}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

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

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

This vignette fits one model, end-to-end, and explains every number. After
reading it you will be able to fit a linear mixed model, read its output,
run an inference test, and save the result. The other vignettes go deeper on
any step.

## The data

`lme4::sleepstudy` records reaction times (ms) for 18 subjects over ten days
of sleep deprivation. Each subject was deprived of sleep starting on Day 0;
by Day 9 most show substantial slowing.

```{r load-data}
sleep <- lme4::sleepstudy
str(sleep)
```

The key structure: `Subject` appears 10 times in the data — once per day.
Those 10 observations are *not* independent. They share the subject's
baseline speed, their individual sensitivity to sleep loss, and any other
between-person trait that we haven't measured. If you fit a plain OLS
regression, those correlations inflate your Type I error rate and make
standard errors too small.

A mixed model handles this by giving each subject its own intercept *and*
its own slope — their personal baseline and their personal rate of slowing —
and then estimating a population distribution over those person-level
parameters.

```{r individual-trajectories, fig.width = 6, fig.height = 4}
library(ggplot2)
ggplot(sleep, aes(Days, Reaction, group = Subject)) +
  geom_line(alpha = 0.4) +
  geom_smooth(aes(group = 1), method = "lm", se = FALSE, colour = "steelblue") +
  labs(x = "Days of sleep deprivation", y = "Reaction time (ms)",
       title = "Individual subject trajectories") +
  theme_minimal()
```

The grey lines are individuals; the blue line is the population average.
Subjects vary in their starting point *and* in how fast they slow down —
which is exactly what a random-intercept-and-slope model captures.

## Step 1: Compile the model

Before fitting, compile the model to see what `mixeff` understands about your
formula. This separates formula interpretation from optimization; you can
catch mis-specified random effects without paying the cost of a full fit.

```{r compile}
spec <- compile_model(Reaction ~ Days + (Days | Subject), sleep)
explain_model(spec)
```

This pre-fit explanation confirms:

- one fixed effect (`Days`) plus an intercept,
- one random-effects block with a correlated intercept and slope per subject,
- the formula has been canonicalized to the explicit `(1 + Days | Subject)`
  form that the optimizer will receive.

Use `audit(spec)` when you want the deeper design audit. Its compact
print starts with the audit summary and requested model;
`print(audit(spec), full = TRUE)` shows the complete upstream report.

If you had written `(1 | Subject)` by mistake (random intercepts only), the
explanation would show only one random-effects column per subject — useful to
verify before a long fit.

## Step 2: Fit

```{r fit}
fit <- lmm(Reaction ~ Days + (Days | Subject), sleep)
```

`lmm()` returns an `mm_lmm` object. The actual optimization is performed by
the bundled Rust engine; `lmm()` is the R entry point and result container.

## Step 3: Read the summary

```{r summary}
summary(fit, tests = "coefficients")
```

The summary has four blocks, in print order.

**Fit status.** Check this line first. `converged_interior` is the good
outcome: the optimizer found a clean solution and every variance component
is comfortably positive, so you can read the rest of the summary at face
value. The status only changes when something needs your attention — for
example, if a variance component collapses to zero (the data show no
detectable variation for that term), the status reports a *boundary* fit,
the affected row is flagged in the variance-components table, and the
p-values below switch to a more conservative, clearly labeled method. In
short: a healthy fit says `converged_interior`, and an unhealthy one tells
you what went wrong instead of leaving you to notice.

**Variance components.** `Subject (Intercept)` is the between-subject spread
in baseline reaction time (SD ≈ 25 ms); `Subject Days` is the between-subject
spread in sensitivity to sleep loss (SD ≈ 6 ms per day); `correlation` is
their association (+0.07 — essentially none). The residual standard deviation
is the within-subject noise left over.

**Fixed effects.** `Estimate` is the population-level coefficient.
`Days = 10.47` means that, on average, reaction time increases by about
10.5 ms per day of sleep deprivation. The `method` column names how each
p-value was computed — here `satterthwaite`, a finite-sample *t* test whose
`df` column (≈ 17) is on the scale of the 18 subjects, not the 180 raw rows.
`mixeff` never reports a number without naming the method behind it. On fits
where Satterthwaite degrees of freedom cannot be computed (for example a
variance component at the boundary), the summary shows clearly labeled
asymptotic Wald *z* rows instead; the `method` column always tells you which
one you got.

**Inference status.** One row per coefficient stating how much to trust the
test: `status` says whether it was computed, `reliability` grades it, and
`reliability_reason` names the engine's warrant for that grade — here
`satterthwaite_finite_difference_approximation`, meaning the degrees of
freedom come from a finite-difference approximation, the standard route for
this method (graded `moderate`). Any further engine notes print underneath.
Grades and warrants are authored by the Rust engine, not by R-side
heuristics.

## Step 4: Extract components

The lme4-compatible extractors work on `mm_lmm` objects:

```{r extractors}
fixef(fit)
```

```{r varcorr}
VarCorr(fit)
```

```{r confint}
confint(fit, method = "asymptotic")
```

`confint()` with `method = "asymptotic"` gives Wald intervals (fast; use
`"bootstrap"` for small samples). The `(Intercept)` interval is the
population mean reaction time on Day 0; the `Days` interval is the
range of plausible slopes.

## Step 5: Test a specific claim

`contrast()` evaluates a linear combination of fixed-effect coefficients —
the mixeff equivalent of `lme4::fixef()` one-row hypothesis tests, but with
method labelling.

```{r contrast}
ct <- contrast(fit, c("(Intercept)" = 0, Days = 1))
ct$table[, c("estimate", "std_error", "p_value", "method", "status", "reliability")]
```

The `estimate` is the Days slope; `status = "available"` and
`reliability = "certified"` confirm that the covariance payload was present
and the inference method was verified as appropriate for this fit. If a method
cannot be certified (e.g. you request Kenward-Roger on a singular fit),
`reliability` becomes `"indicative"` and `reason` names the problem.

## Step 6: Compare specific conditions

`mm_comparisons()` makes it easy to answer "how much slower is a subject on
Day 9 compared to Day 0?" without computing the arithmetic yourself.

```{r comparisons}
cmp <- mm_comparisons(fit, specs = "Days", at = list(Days = c(0, 9)))
cmp$table[, c("label", "estimate", "conf_low", "conf_high", "p_value")]
```

The `at` argument pins `Days` to exactly those two values. The difference
`Days=9 - Days=0` is 9 × the slope: about 94 ms, with a confidence interval.

## Step 7: Save and reload

Mixed-model fits can take minutes to hours on large data sets. `mixeff` stores
everything needed to revive the result later.

```{r save-reload}
tmp <- tempfile(fileext = ".rds")
saveRDS(fit, tmp)

fit2 <- revive(readRDS(tmp))
stopifnot(isTRUE(all.equal(fixef(fit), fixef(fit2))))
```

`revive()` reconnects the R object to the Rust handle so the full inference
surface is available. Plain `readRDS()` without `revive()` is sufficient when
you only need the JSON-stored extractor values (coefficients, variance
components, fitted values); call `revive()` when you want to run new
`contrast()`, `mm_means()`, or other live-inference calls on the reloaded fit.
See `vignette("saving-and-reviving")` for the full discussion.

## Where to go next

| Question | Vignette |
|---|---|
| What do the random-effects formula options mean? | `vignette("demystifying-formulas")` |
| How are p-values computed and which method is right for my fit? | `vignette("inference")` |
| How do I compute marginal means and treatment contrasts? | `vignette("marginal-effects")` |
| How do I write up the results for a paper? | `vignette("reporting-lmms")` |
| I'm coming from lme4 — what's different? | `vignette("lme4-migration")` |
| Can I fit a binomial/Poisson GLMM? | `vignette("glmm")` |
