Mixed-Effects Models Exercises in R: 18 Practice Problems
Eighteen practice problems on mixed-effects models in R, covering random intercepts, random slopes, nested and crossed designs, generalized linear mixed models, REML versus ML inference, and post-fit diagnostics. Every problem ships with the expected output and a hidden solution you can reveal after attempting it.
Section 1. Random Intercepts with lme4 (3 problems)
Exercise 1.1: Fit a random-intercept model for reaction times by subject
Task: The built-in sleepstudy dataset (from lme4) tracks reaction time for 18 subjects over 10 nights of progressive sleep restriction. Fit a model with a fixed effect for Days and a random intercept per Subject using lmer(Reaction ~ Days + (1 | Subject), data = sleepstudy). Save the fitted model to ex_1_1.
Expected result:
#> Linear mixed model fit by REML ['lmerMod']
#> Formula: Reaction ~ Days + (1 | Subject)
#> Data: sleepstudy
#> REML criterion at convergence: 1786.46
#> Random effects:
#> Groups Name Std.Dev.
#> Subject (Intercept) 37.12
#> Residual 30.99
#> Number of obs: 180, groups: Subject, 18
#> Fixed Effects:
#> (Intercept) Days
#> 251.41 10.47
Difficulty: Beginner
Each subject has their own baseline reaction time, so the model should estimate one shared trend over nights while letting every subject's starting level float.
Call lmer() with the formula Reaction ~ Days + (1 | Subject) and data = sleepstudy.
Click to reveal solution
Explanation: The notation (1 | Subject) adds a separate intercept deviation for every level of Subject, while the Days coefficient stays a single population-level slope. The Subject standard deviation of 37.1 ms is far larger than zero, signalling that ignoring subject-to-subject baseline differences (i.e., a plain lm()) would underestimate uncertainty for the Days effect. REML is the default estimator because it gives less biased variance components than ML when group counts are modest.
Exercise 1.2: Compute the intraclass correlation coefficient from variance components
Task: A sleep researcher reviewing your random-intercept fit wants the share of total residual variability that lives between subjects rather than within. From ex_1_1, extract the subject-level variance and residual variance using VarCorr(), then compute the intraclass correlation as $\rho = \sigma_{\text{subj}}^2 / (\sigma_{\text{subj}}^2 + \sigma_{\text{resid}}^2)$. Save the scalar to ex_1_2.
Expected result:
#> (Intercept)
#> 0.5893481
Difficulty: Beginner
The intraclass correlation is the fraction of leftover variability that lives between subjects rather than within them.
Pull the variance components from VarCorr(ex_1_1), square the subject "stddev" attribute and the residual "sc" attribute, then form the ratio.
Click to reveal solution
Explanation: VarCorr() returns a list with one entry per grouping factor plus a residual scale stored as the "sc" attribute. Squaring the standard deviations gives the variances on the original response scale. An ICC near 0.59 means almost 60% of the unexplained variance after accounting for Days is between-subject, so any inference that ignores grouping (pseudo-replication) will badly understate standard errors. The performance::icc() helper computes the same quantity in one call.
Exercise 1.3: Extract subject-level BLUPs and order them by baseline reaction time
Task: You want to see which subjects have the slowest baseline reaction time after partialling out the average Days effect. Extract the conditional modes (BLUPs) from ex_1_1 with ranef(), coerce them to a data frame with Subject as a column, and sort ascending by the intercept deviation. Save the ordered data frame to ex_1_3.
Expected result:
#> Subject intercept_dev
#> 1 335 -25.27598
#> 2 309 -71.41859
#> 3 310 -57.74656
#> ...
#> # 15 more rows (sorted ascending)
#> Subject 308 ends with intercept_dev ~ 40.78 (slowest baseline)
Difficulty: Intermediate
You want each subject's estimated deviation from the average intercept, then the subjects ordered from the most negative deviation upward.
Extract the conditional modes with ranef(ex_1_1)$Subject, coerce with as.data.frame(), keep the row names as a Subject column, and arrange() ascending.
Click to reveal solution
Explanation: ranef() returns shrinkage estimates: subject deviations are pulled toward zero in proportion to how little data each subject contributes. These are not the same as fitting 18 separate intercepts with lm(): BLUPs borrow strength across subjects and produce smaller mean squared error for new predictions. Coercing to a tibble keeps the subject IDs as a column instead of leaving them buried in rownames, which is the form most downstream plotting and joining steps want.
Section 2. Random Slopes and Correlated Effects (3 problems)
Exercise 2.1: Add a random slope for Days varying by Subject
Task: Inspecting the per-subject trajectories you noticed that some subjects degrade much faster than others as Days increases. Extend the previous model to let each subject have their own slope: fit Reaction ~ Days + (Days | Subject) on sleepstudy with lmer(). Save the fit to ex_2_1.
Expected result:
#> Linear mixed model fit by REML ['lmerMod']
#> Formula: Reaction ~ Days + (Days | Subject)
#> Data: sleepstudy
#> REML criterion at convergence: 1743.63
#> Random effects:
#> Groups Name Std.Dev. Corr
#> Subject (Intercept) 24.74
#> Days 5.92 0.07
#> Residual 25.59
#> Number of obs: 180, groups: Subject, 18
#> Fixed Effects:
#> (Intercept) Days
#> 251.41 10.47
Difficulty: Intermediate
Let the rate of decline differ from subject to subject, not just the baseline level.
Fit lmer() with the formula Reaction ~ Days + (Days | Subject) on sleepstudy.
Click to reveal solution
Explanation: (Days | Subject) is shorthand for (1 + Days | Subject): an intercept and a slope per subject, with a covariance between them estimated from the data. The residual SD drops from 31 to 26 ms because per-subject variation in the rate of decline is no longer absorbed into the noise term. The fixed-effect estimates (251.4, 10.47) are unchanged because the design is balanced; in unbalanced data they would shift.
Exercise 2.2: Inspect the correlation between random intercepts and slopes
Task: The reviewer asks whether subjects who start slow also degrade faster (positive intercept-slope correlation) or whether the two are independent. From ex_2_1, extract the correlation between the random intercept and the random slope from the VarCorr() output using attr(VarCorr(ex_2_1)$Subject, "correlation")[1, 2]. Save the scalar to ex_2_2.
Expected result:
#> [1] 0.06632237
Difficulty: Advanced
The random-effects structure already stores how baseline and slope move together as a correlation; you just need to read it out.
Take attr(VarCorr(ex_2_1)$Subject, "correlation") and grab the off-diagonal element [1, 2].
Click to reveal solution
Explanation: The random-effects correlation matrix is attached as an attribute on each grouping factor's variance-covariance matrix; the off-diagonal element [1, 2] is the corr between intercept and Days. A value near 0.07 is effectively zero given the small subject count, meaning baseline reaction time tells you little about how steeply someone deteriorates. If this correlation were estimated as exactly ±1 it would signal a singular fit; consider (Days || Subject) (the double-bar form, which forces zero correlation) or refitting with a simpler random structure.
Exercise 2.3: Compare random-intercept vs random-slope fits with anova
Task: To decide whether the slope term is worth the two extra parameters, compare the simpler ex_1_1 against the richer ex_2_1. Pass both to anova(), which automatically refits with ML to make the likelihood ratio test valid. Save the returned anova table to ex_2_3.
Expected result:
#> refitting model(s) with ML (instead of REML)
#> Data: sleepstudy
#> Models:
#> ex_1_1: Reaction ~ Days + (1 | Subject)
#> ex_2_1: Reaction ~ Days + (Days | Subject)
#> npar AIC BIC logLik deviance Chisq Df Pr(>Chisq)
#> ex_1_1 4 1802.1 1814.8 -897.04 1794.1
#> ex_2_1 6 1763.9 1783.1 -875.97 1751.9 42.139 2 1.226e-09 ***
Difficulty: Intermediate
Compare the simpler and richer fits to test whether the two extra slope parameters earn their keep.
Pass ex_1_1 and ex_2_1 to anova(), which refits both with ML automatically before the likelihood ratio test.
Click to reveal solution
Explanation: A likelihood ratio test for nested models requires ML, not REML, because REML log-likelihoods compare differently parameterised mean structures incorrectly. anova.merMod() quietly refits both models with REML = FALSE before computing the chi-square. Note that the LRT is known to be conservative on the boundary of the parameter space (testing whether a variance is zero), so the true p-value is likely even smaller; for borderline cases use parametric bootstrap with pbkrtest::PBmodcomp().
Section 3. Nested and Crossed Random Effects (3 problems)
Exercise 3.1: Build a nested random-effects model for ChickWeight
Task: An agronomist running a chick-feed trial gives you ChickWeight: weight measured at 12 time points for 50 chicks across 4 diets. Fit weight ~ Time + Diet + (Time | Chick) with lmer() so each chick has its own growth trajectory while diet enters as a fixed effect. Save the fit to ex_3_1.
Expected result:
#> Linear mixed model fit by REML ['lmerMod']
#> Formula: weight ~ Time + Diet + (Time | Chick)
#> Data: ChickWeight
#> REML criterion at convergence: 4824.1
#> Random effects:
#> Groups Name Std.Dev. Corr
#> Chick (Intercept) 11.27
#> Time 3.91 -0.99
#> Residual 14.04
#> Number of obs: 578, groups: Chick, 50
#> Fixed Effects:
#> (Intercept) Time Diet2 Diet3 Diet4
#> 28.50 8.75 16.18 36.50 30.23
Difficulty: Intermediate
Each chick gets its own growth trajectory while diet enters as a single population-level effect.
Fit lmer() with the formula weight ~ Time + Diet + (Time | Chick) and data = ChickWeight.
Click to reveal solution
Explanation: Each Chick only ever receives one Diet, so Chick is implicitly nested inside Diet; you do not need Diet/Chick in the random part because the diet effect is captured as a fixed factor. The near-perfect negative correlation (-0.99) between random intercept and slope is a classic warning sign: the model is on the boundary, often because the time variable is not centred. Re-running with Time centred at its mean usually returns a sensible correlation and a non-degenerate covariance matrix.
Exercise 3.2: Distinguish nested from crossed structure with implicit nesting
Task: An education researcher hands you a small school dataset where class_id values "1" and "2" both reappear inside three different schools "A", "B", "C". She wants the model to treat 1A and 1B as different classrooms. Build the tibble below and fit the correctly nested model score ~ 1 + (1 | school_id/class_id). Save the nested fit to ex_3_2.
Expected result:
#> Linear mixed model fit by REML ['lmerMod']
#> Formula: score ~ 1 + (1 | school_id/class_id)
#> Random effects:
#> Groups Name Std.Dev.
#> class_id:school_id (Intercept) 6.4
#> school_id (Intercept) 3.9
#> Residual 4.7
#> Number of obs: 60, groups: class_id:school_id, 6; school_id, 3
Difficulty: Advanced
Reused class labels are not the same classroom across schools, so the grouping has to express that classes sit inside schools.
Use the slash notation (1 | school_id/class_id) in the lmer() formula score ~ 1 + ....
Click to reveal solution
Explanation: The shorthand school_id/class_id expands to school_id + school_id:class_id, which creates the interaction grouping factor needed when class labels are reused across schools. Writing (1 | school_id) + (1 | class_id) instead would tell lmer() that class "1" is the same unit in every school, which is wrong. The cleaner long-term fix is to mint globally unique class IDs (paste(school_id, class_id, sep = "_")) and use (1 | school_id) + (1 | unique_class) so the structure is explicit in the data, not implicit in the formula.
Exercise 3.3: Fit crossed random effects for subjects rated on shared items
Task: A psycholinguist runs an experiment where each of 30 subjects rates each of 20 items under one of two conditions (fully crossed). Build the tibble below and fit rating ~ condition + (1 | subj) + (1 | item) to absorb subject-level and item-level variability simultaneously. Save the fit to ex_3_3.
Expected result:
#> Linear mixed model fit by REML ['lmerMod']
#> Formula: rating ~ condition + (1 | subj) + (1 | item)
#> Random effects:
#> Groups Name Std.Dev.
#> subj (Intercept) 0.92
#> item (Intercept) 1.05
#> Residual 1.50
#> Number of obs: 600, groups: subj, 30; item, 20
#> Fixed Effects:
#> (Intercept) conditionB
#> 4.10 0.45
Difficulty: Advanced
Subjects and items each contribute their own variability and neither one is nested inside the other.
Add two separate grouping terms (1 | subj) + (1 | item) to the lmer() formula rating ~ condition + ....
Click to reveal solution
Explanation: Crossed effects appear when every level of one grouping factor (subjects) is observed with every level of another (items). Treating items as a fixed factor would burn 19 degrees of freedom and prevent generalization to new items; treating them as random instead lets you generalize the condition effect to the broader population of stimuli. Barr et al.'s "keep it maximal" advice would also add random slopes for condition by both subject and item, but that often fails to converge with small designs.
Section 4. Model Comparison and Inference (3 problems)
Exercise 4.1: Refit with REML=FALSE for a likelihood ratio test of the fixed slope
Task: A reviewer wants a formal test of whether the Days fixed effect is needed at all. To do a likelihood ratio test for a fixed effect you must refit with REML = FALSE. Fit a null model Reaction ~ 1 + (Days | Subject) and a full model Reaction ~ Days + (Days | Subject), both with REML = FALSE, then run anova(null, full). Save the anova table to ex_4_1.
Expected result:
#> Models:
#> mod_null: Reaction ~ 1 + (Days | Subject)
#> mod_full: Reaction ~ Days + (Days | Subject)
#> npar AIC BIC logLik deviance Chisq Df Pr(>Chisq)
#> mod_null 5 1785.5 1801.5 -887.74 1775.5
#> mod_full 6 1763.9 1783.1 -875.97 1751.9 23.54 1 1.23e-06 ***
Difficulty: Advanced
Testing a fixed effect needs log-likelihoods that are comparable across models with different mean structures.
Fit both the null and full models with lmer(..., REML = FALSE), then compare them with anova(mod_null, mod_full).
Click to reveal solution
Explanation: REML estimates variance components after profiling out the fixed effects, so the REML log-likelihood depends on the fixed-effect design matrix and is not comparable between models with different fixed parts. Switching to ML with REML = FALSE fixes this. For random-effect tests (testing whether a variance is zero) the opposite advice applies: use REML, because ML biases variance estimates downward, especially with few groups.
Exercise 4.2: Get Satterthwaite p-values for fixed effects with lmerTest
Task: Base lme4 deliberately omits p-values for fixed effects because the reference distribution is unknown in small samples. Load lmerTest, refit ex_2_1, then call summary() on the new fit and pull out the coefficients matrix which now carries Satterthwaite degrees of freedom and t-tests. Save the coefficients matrix to ex_4_2.
Expected result:
#> Estimate Std. Error df t value Pr(>|t|)
#> (Intercept) 251.405 6.825 17.00 36.84 < 2e-16 ***
#> Days 10.467 1.546 17.00 6.77 3.27e-06 ***
Difficulty: Intermediate
Base lme4 withholds p-values for fixed effects; a companion package supplies the approximate degrees of freedom needed for them.
Load lmerTest, refit the random-slope model, then pull summary(fit_lt)$coefficients.
Click to reveal solution
Explanation: lmerTest masks lme4::lmer() and attaches Satterthwaite-approximate degrees of freedom and t-tests to the summary. Df of 17 reflects the 18-subject design (n_groups - 1). Use Kenward-Roger (lmerTest::lmer(..., ddf = "Kenward-Roger")) for smaller designs where Satterthwaite under-covers; it is slower but typically more accurate. Loading lmerTest after lme4 is the canonical order, since you want the p-value-aware lmer() to win the name conflict.
Exercise 4.3: Bootstrap a 95% confidence interval for the fixed slope
Task: The reviewer is sceptical of Satterthwaite for skewed responses and asks for a parametric bootstrap CI on the Days coefficient. Run confint(ex_2_1, parm = "Days", method = "boot", nsim = 200, seed = 1) and save the resulting 2-column matrix to ex_4_3. Use 200 simulations for speed; in published work use at least 2000.
Expected result:
#> 2.5 % 97.5 %
#> Days 7.95 12.95
Difficulty: Advanced
A resampling-based interval is more honest than a Wald interval when the sampling distribution is skewed.
Call confint() on ex_2_1 with parm = "Days", method = "boot", nsim = 200, and seed = 1.
Click to reveal solution
Explanation: Parametric bootstrap simulates new responses from the fitted model and refits each replicate; the empirical quantiles of the replicate coefficient form the CI. This is more honest than Wald intervals when the sampling distribution is asymmetric, which often happens for variance components and GLMM coefficients. The seed argument makes the run reproducible; without it, two readers running the same code see slightly different CIs.
Section 5. Generalized Linear Mixed Models (3 problems)
Exercise 5.1: Fit a binomial GLMM with random subject intercepts
Task: A clinical trial team tracks weekly medication adherence (1 = took it, 0 = missed) over 8 visits for 50 patients across two treatment arms. Build the inline dataset below and fit glmer(adherence ~ arm + (1 | patient), family = binomial). Save the fit to ex_5_1.
Expected result:
#> Generalized linear mixed model fit by maximum likelihood (Laplace Approximation)
#> Family: binomial ( logit )
#> Formula: adherence ~ arm + (1 | patient)
#> Data: trial
#> Random effects:
#> Groups Name Std.Dev.
#> patient (Intercept) 1.18
#> Number of obs: 400, groups: patient, 50
#> Fixed Effects:
#> (Intercept) armB
#> 0.45 0.71
Difficulty: Advanced
A patient who is compliant tends to stay compliant across visits, so the binary outcome needs a per-patient random term.
Fit glmer() with the formula adherence ~ arm + (1 | patient) and family = binomial.
Click to reveal solution
Explanation: A patient who is "compliant" tends to stay compliant across all 8 visits; a random intercept absorbs that within-person correlation so the arm coefficient gets honest standard errors. glmer defaults to the Laplace approximation, fast and adequate for binary outcomes with reasonable group sizes. For more accuracy use nAGQ = 10 or higher (adaptive Gauss-Hermite quadrature), but it only works for single-grouping-factor models.
Exercise 5.2: Fit a Poisson GLMM with an offset for sequencing depth
Task: A geneticist counts read hits to a target gene across 30 RNA-seq samples in 4 tissue types. Because total sequencing depth varies, the model needs an offset(log(read_total)) so the coefficients describe rates per read. Build the inline dataset below and fit glmer(hits ~ tissue + (1 | sample) + offset(log(read_total)), family = poisson). Save the fit to ex_5_2.
Expected result:
#> Generalized linear mixed model fit by maximum likelihood (Laplace Approximation)
#> Family: poisson ( log )
#> Formula: hits ~ tissue + (1 | sample) + offset(log(read_total))
#> Random effects:
#> Groups Name Std.Dev.
#> sample (Intercept) 0.21
#> Number of obs: 30, groups: sample, 30
#> Fixed Effects:
#> (Intercept) tissueB tissueC tissueD
#> -7.55 0.32 0.81 -0.46
Difficulty: Advanced
Counts have to be turned into rates so that deeply sequenced samples do not get systematically higher coefficients.
Fit glmer() with hits ~ tissue + (1 | sample) + offset(log(read_total)) and family = poisson.
Click to reveal solution
Explanation: Including log(read_total) as an offset (coefficient fixed at 1) reparameterises counts as rates: the linear predictor returns log(hits / read_total). Without the offset, deeply sequenced samples would have systematically higher counts and the tissue coefficients would absorb that noise. The random intercept on sample handles residual library-prep variation beyond what depth captures; with only one observation per sample it is identified by the Poisson mean-variance link.
Exercise 5.3: Check overdispersion in a Poisson GLMM with the Pearson chi-square ratio
Task: Poisson assumes variance equals mean, which is rarely true for real count data. From ex_5_2, compute the dispersion statistic as sum(residuals(ex_5_2, type = "pearson")^2) / df.residual(ex_5_2). A value much above 1 (say, > 1.5) flags overdispersion and motivates a negative binomial or observation-level random effect. Save the scalar to ex_5_3.
Expected result:
#> [1] 1.03
Difficulty: Advanced
Poisson assumes the variance equals the mean, so measure how far the spread of the residuals departs from that.
Sum the squared Pearson residuals from residuals(ex_5_2, type = "pearson") and divide by df.residual(ex_5_2).
Click to reveal solution
Explanation: A dispersion of about 1.0 says the Poisson mean-variance assumption fits the data, partly because the (1 | sample) random intercept acts as an observation-level random effect that already soaks up extra variance. If this number came back at 3 you would refit with glmer.nb() (negative binomial) or add an explicit observation-level random effect like (1 | obs_id) where obs_id is unique per row. performance::check_overdispersion() automates the whole check.
Section 6. Diagnostics and Prediction (3 problems)
Exercise 6.1: Plot residuals versus fitted to inspect homoscedasticity
Task: Before trusting any standard error from ex_2_1, you want to see whether residuals fan out with fitted reaction time (heteroscedasticity) or curve (model mis-specification). Build a tibble of fitted = fitted(ex_2_1) and resid = resid(ex_2_1), then plot with ggplot() adding a horizontal zero line and a geom_smooth(se = FALSE) trend. Save the ggplot to ex_6_1.
Expected result:
#> ggplot object: points roughly centred on y = 0 across fitted range
#> - x axis: Fitted (range ~ 200 to 460 ms)
#> - y axis: Residual (range ~ -100 to 100 ms)
#> - red horizontal line at y = 0
#> - blue loess smoother stays close to zero with no obvious funnel
Difficulty: Beginner
A trustworthy fit shows residuals scattered evenly around zero with no funnel shape or curve across the fitted range.
Build a tibble of fitted(ex_2_1) and resid(ex_2_1), then ggplot() with geom_point(), geom_hline(yintercept = 0), and geom_smooth(se = FALSE).
Click to reveal solution
Explanation: Mixed-effects fits return residuals conditional on the random effects, so a flat scatter centred on zero is the diagnostic of choice. A funnel shape (variance growing with the mean) would suggest a log-transform on the response; a curved trend would suggest a non-linear time effect, e.g., a spline on Days. DHARMa::simulateResiduals() produces simulation-based residuals that are uniform on [0, 1] under a correct model and is more reliable for GLMM diagnostics.
Exercise 6.2: Predict the population-average curve for new days
Task: A reaction-time experimenter writes up the paper and wants the population-average curve (not subject-specific) for Days = 0 through 9. Use predict(ex_2_1, newdata = data.frame(Days = 0:9), re.form = NA) to suppress random effects. Save the length-10 numeric vector to ex_6_2.
Expected result:
#> 1 2 3 4 5 6 7 8 9 10
#> 251.41 261.88 272.34 282.81 293.28 303.74 314.21 324.68 335.14 345.61
Difficulty: Beginner
The population-average curve ignores subject-specific deviations and reflects what the typical subject would do.
Call predict() on ex_2_1 with newdata = data.frame(Days = 0:9) and re.form = NA.
Click to reveal solution
Explanation: Setting re.form = NA (or equivalently ~0) zeros out random effects, giving the marginal expectation: what the average subject would do. Using re.form = NULL (the default) would still need a Subject value in newdata to add the subject-specific deviations. For uncertainty around these population predictions, use merTools::predictInterval() or a parametric bootstrap, since predict.merMod() does not return standard errors.
Exercise 6.3: Compute marginal and conditional R-squared with MuMIn
Task: A reviewer asks how much of the variability in Reaction is captured by the fixed effect Days alone versus the full model including random effects. Use MuMIn::r.squaredGLMM(ex_2_1) which returns a single-row matrix with columns R2m (fixed only) and R2c (fixed + random). Save the matrix to ex_6_3.
Expected result:
#> R2m R2c
#> [1,] 0.2786443 0.7992246
Difficulty: Advanced
Separate the variability explained by the fixed predictors alone from the variability explained once random effects are added in.
Call MuMIn::r.squaredGLMM(ex_2_1) and read the R2m and R2c columns of the returned matrix.
Click to reveal solution
Explanation: Nakagawa-Schielzeth marginal R² (R2m) is the share of variance the fixed predictors alone explain; conditional R² (R2c) adds the variance soaked up by random effects. The gap (about 52 percentage points here) is the share captured by subject-to-subject heterogeneity, not by Days. For GLMMs you get two flavours of each (theoretical and delta-method), so the function returns a 2x2 matrix; the theoretical row is usually the one to report.
What to do next
- Read Multilevel Models in R for the conceptual map: when nesting matters, how shrinkage works, and how mixed-effects sits between fixed-effect and Bayesian hierarchical models.
- Practice the underlying Linear Regression Exercises in R to sharpen your intuition for the fixed-effect side before stacking random effects on top.
- Work through ANOVA Exercises in R to see how repeated-measures ANOVA, the classical predecessor of mixed-effects models, handles the same correlated-residual problem with a different formalism.
- Try the Generalized Linear Models in R Exercises to build comfort with
glm()and link functions before tackling the GLMM material in Section 5.
r-statistics.co · Verifiable credential · Public URL
This document certifies mastery of
Mixed-Effects Models Mastery
Every certificate has a public verification URL that proves the holder passed the assessment. Anyone with the link can confirm the recipient and date.
181 learners have earned this certificate