Model Prediction/Visualisation
5. Linear Mixed Models (LMMs)

code
concept
modelling
visualisation
Visualising fixed vs random effects.
Published

June 27, 2025

This week we are continuing our series on predicting and visualising models in R by focussing on linear mixed models (LMM’s). Like GLM’s, the fundamentals of the process remain the same bar a few tweaks specific to LMM’s that you need to be aware of. Here we are back to dealing with a continuous covariate and outcome - the new development to note is the distinction between fixed and random effects and how to handle each appropriately in the predict() function.

1 Predicting

To illustrate these ideas we’ll revisit the sleepstudy dataset which is available in the lme4 package (I have used this for a similar purpose in one of my previous posts). The sleepstudy data looks at reaction times over time in sleep-deprived individuals. For the sake of the exercise we will fit a mixed model with reaction time (ms) as the outcome, time (days) as a fixed-effect and time (days) and individual as random-effects. So this is a random slopes model allowing the ‘effect’ of sleep-deprivation on reaction time to vary over time for each individual. We fit the model and view a few lines of the dataframe which now contains the fixed (mod_pred_fix) and random (mod_pred_ran) predictions. Obtaining the predictions is very easy - we create the two new variables by simply calling:

predict(mod) for the random-effects, and:

predict(mod, re.form = NA) for the fixed-effects.

Note the re.form = NA sets all random-effects to 0.

Code
library(lme4)
library(ggplot2)
library(gtsummary)
library(kableExtra)
# Load data
data("sleepstudy")
# Model
mod <- lmer(Reaction ~ Days + (Days | Subject), data = sleepstudy)
# Predict
sleepstudy$mod_pred_fix <- predict(mod, re.form = NA) # predict fixed effects
sleepstudy$mod_pred_ran <- predict(mod) # predict random effects
# View data
head(sleepstudy, 10) |> 
  kable(align = "c", digits = 2)
Reaction Days Subject mod_pred_fix mod_pred_ran
249.56 0 308 251.41 253.66
258.70 1 308 261.87 273.33
250.80 2 308 272.34 293.00
321.44 3 308 282.81 312.66
356.85 4 308 293.27 332.33
414.69 5 308 303.74 351.99
382.20 6 308 314.21 371.66
290.15 7 308 324.68 391.33
430.59 8 308 335.14 410.99
466.35 9 308 345.61 430.66


So, mod_pred_fix represents the group-averaged prediction of reaction time (outcome) as a function of time (covariate) and similarly mod_pred_ran represents the individual predictions of the same.

2 One-Plot Visualisation

Plotting the results is then quite straightforward with ggplot2:

Code
# Plot all data
sleepstudy |>
    ggplot(aes(x = Days, y = Reaction, color = factor(Subject))) +
    geom_line(aes(x = Days, y = mod_pred_ran)) +
    geom_line(aes(x = Days, y = mod_pred_fix), linewidth = 2, color = "blue") +
    scale_y_continuous(limits = c(200, 500), breaks = seq(200, 500, by = 50)) +
    scale_x_continuous(limits = c(0, 9), breaks = seq(0, 9, by = 1)) +
    geom_point(alpha = 0.5) +
    labs(title = "Reaction Time ~ Days of Sleep Deprivation") + xlab("Time (days)") + ylab("Average Reaction Time (ms)") +
    theme_bw(base_size = 15)

3 Multi-Plot Visualisation

The above plot is good for showing all the data at once, but can get a little bit confusing if you are interested in looking more closely at any specific individual trajectories. If that is in fact the case, an alternative plot representation is to facet on individuals, like so:

Code
# Plot - facet on individual
sleepstudy |>
  ggplot(aes(x = Days, y = Reaction)) +
  geom_line(aes(x = Days, y = mod_pred_ran), linewidth = 1, color = "green") +
  geom_line(aes(x = Days, y = mod_pred_fix), linewidth = 1, color = "blue") +
  scale_y_continuous(limits = c(200, 500), breaks = seq(200, 500, by = 100)) +
  scale_x_continuous(limits = c(0, 9), breaks = seq(0, 9, by = 2)) +
  geom_point(alpha = 0.5) +
  facet_wrap(~Subject) +
  labs(x = "Time (Days)",
       y = "Average Reaction Time (ms)",
       title = "Reaction Time ~ Days of Sleep Deprivation",
       subtitle = "Blue = Fixed Effects Prediction | Green = Random Effects Prediction") +
  theme_bw(base_size = 15)

It’s now much easier to see the individual trajectories and how they compare to that of the group average.

4 Some Background Stuff

While not essential to producing a visualisation of the model predictions, taking a little detour to understand in more detail how we actually arrived at the fixed- and random-effect predictions is a worthwhile exercise and one which can aid in building your mixed-modelling intuition.

Let’s first inspect an unprettified summary of the model output:

Code
summary(mod)
Linear mixed model fit by REML ['lmerMod']
Formula: Reaction ~ Days + (Days | Subject)
   Data: sleepstudy

REML criterion at convergence: 1743.6

Scaled residuals: 
    Min      1Q  Median      3Q     Max 
-3.9536 -0.4634  0.0231  0.4634  5.1793 

Random effects:
 Groups   Name        Variance Std.Dev. Corr
 Subject  (Intercept) 612.10   24.741       
          Days         35.07    5.922   0.07
 Residual             654.94   25.592       
Number of obs: 180, groups:  Subject, 18

Fixed effects:
            Estimate Std. Error t value
(Intercept)  251.405      6.825  36.838
Days          10.467      1.546   6.771

Correlation of Fixed Effects:
     (Intr)
Days -0.138

We obtain the fixed-effects as an Intercept and a coefficient for time (Days) and we can interpret that as an average reaction time of 251.4 ms at baseline and an average increase of 10.5 ms in reaction time for the passing of each day. Note that the random-effects are not as easily interpretable in their reported form which is an estimate of the variance of the distribution of individual reaction times, not average differences as for fixed-effects.

A shortcut to obtain the fixed-effects of an lmer class model in R is:

fixef(mod)

Code
fixef(mod)
(Intercept)        Days 
  251.40510    10.46729 

and similarly for the random-effects:

ranef(mod)

Code
ranef(mod)
$Subject
    (Intercept)        Days
308   2.2585509   9.1989758
309 -40.3987381  -8.6196806
310 -38.9604090  -5.4488565
330  23.6906196  -4.8143503
331  22.2603126  -3.0699116
332   9.0395679  -0.2721770
333  16.8405086  -0.2236361
334  -7.2326151   1.0745816
335  -0.3336684 -10.7521652
337  34.8904868   8.6282652
349 -25.2102286   1.1734322
350 -13.0700342   6.6142178
351   4.5778642  -3.0152621
352  20.8636782   3.5360011
369   3.2754656   0.8722149
370 -25.6129993   4.8224850
371   0.8070461  -0.9881562
372  12.3145921   1.2840221

with conditional variances for "Subject" 

So what do the random-effects actually tell us? In a nutshell, these represent the individual differences from the fixed-effects values.

Let’s consider Subject 308 as an example to illustrate this. If we are interested in the predicted baseline reaction time for this individual, all we need to do is add the fixed- and random-effects intercepts values - 251.405 + 2.259 = 253.664 ms (we can ignore the time ‘effect’ which is 0 by definition at baseline). By comparison, we can also easily obtain that predicted value with the following code:

predict(mod, newdata = data.frame(days = 0, Subject = 308), re.form = ~(Days|Subject))

Code
predict(mod, newdata = data.frame(Days = 0, Subject = 308), re.form = ~(Days|Subject))
       1 
253.6637 

It’s a little bit more complex to manually calculate the predicted value at any other time point because the ‘effect’ of time is no longer ignorable and we just need to remember the basics of our regression equation. Say, we want to calculate the reaction time for Subject 308 at 5 days. A simple way to do this is to first work out the fixed-effect prediction at 5 days and then add the random-effect prediction to it.

The fixed-effect prediction is just the intercept value plus the coefficient for time multiplied by the number of days (either from the model summary or from fixef()). In numbers: 251.405 + (10.467 * 5) = 303.74. This gives the predicted group-average reaction time at 5 days.

The random-effect prediction is the same but from ranef() and specific to the individual. In numbers: 2.259 + (9.198 * 5) = 48.249. This gives the additional (to the group-average) reaction time specific to the individual.

We can then add those two values to obtain the predicted reaction time for Subject 308 at 5 days - i.e. 303.74 + 48.249 = 351.989. If you don’t want to manually calculate this, then you can use:

predict(mod, newdata = data.frame(days = 5, Subject = 308), re.form = ~(Days|Subject))

Code
predict(mod, newdata = data.frame(Days = 5, Subject = 308), re.form = ~(Days|Subject))
      1 
351.995 

Which is essentially the same, bar rounding error.

And that brings us to the end of this post. Hopefully that gives you some idea as to how random effects and their predictions are calculated. In the next post, we will finish our series on model prediction and visualisation be taking a look at the Cox model. Until then…