Troubleshooting model fit, checking assumptions and diagnostics


Data Analysis for Psychology in R 3

Elizabeth Pankratz (elizabeth.pankratz@ed.ac.uk)


Department of Psychology
University of Edinburgh
2026–2027

Course Overview


Linear mixed models
(with Dr. Elizabeth Pankratz)
Regression refresher, intro to group-structured data
Modelling group-structured data using random effects
Interpreting LMMs and building maximal models
Troubleshooting model fit, checking assumptions + diagnostics
LMMs: Practice analysis
factor analysis
working with multi-item measures
(with Dr. Josiah King)
measurement and dimensionality
exploring underlying constructs (EFA)
testing theoretical models (CFA)
reliability and validity
recap & exam prep

Retrieval practice: What is a maximal model?


Challenge: try to respond without looking at your notes!


This week’s learning objectives


What are four common issues that you might encounter when trying to fit LMMs, and how can you solve each one?

What five assumptions does an LMM make?

How do you check each assumption in an LMM?

How can you detect influential observations or influential groups in an LMM?

Common issues when fitting LMMs

Common issues when fitting LMMs


Model failed to converge

How you can tell:

warning(s): Model failed to converge with max|grad| = 0.0071877 (tol = 0.002, component 1)
Warning: Model failed to converge with 1 negative eigenvalue: -1.3e+02


What’s the problem?

  • LMMs estimate parameters using a process called “maximum likelihood estimation” (MLE).
  • The model’s goal is to come up with the parameters that are maximally likely to have generated our data.
  • To find the most likely parameters, the model starts with random values that probably don’t match our data very well. Then the model adjusts those values until they do match the data.
  • When the model successfully estimates those numbers, we say the model has “converged”.
  • But when this process runs into problems or gets stuck, we say the model has “not converged” or “failed to converge”.


Never trust any numbers from a model that has failed to converge.

Singularity / singular fit

How you can tell:

boundary (singular) fit: see ?isSingular

or the correlations between random effects are –1 or 1, or the SD/variance of random effects are 0.

(We always have to check on those things manually, we don’t get any warnings about them ☹️)


What’s the problem?

  • The random effect structure that we’ve tried to fit is too complex (i.e., too many group-level parameters).
  • When there are too many parameters, the model encounters technical/mathematical problems that make estimation impossible.


Never trust any numbers from a model with a singular fit.

Not enough observations for random effect structure

How you can tell:

Error: number of observations (=150) <= number of random effects (=150) for term (1 + x | g); the random-effects parameters and the residual variance (or scale parameter) are probably unidentifiable


What’s the problem?

  • Again, the random effect structure is too complex for the data to support.
  • This warning is common when each level of a grouping variable has exactly one observation per level of a predictor.
    • Strictly speaking, random slopes should be possible, because the predictor does vary within each level of the grouping variable.
    • But practically speaking, the model needs more data to estimate the random slopes.


Never trust any numbers from a model with unidentifiable parameters.

Variables need rescaling

How you can tell:

Warning messages:
1: Some predictor variables are on very different scales:
consider rescaling
Warning messages:
1: In checkConv(attr(opt, “derivs”), opt$par, ctrl = control$checkConv, :
Model is nearly unidentifiable: large eigenvalue ratio
- Rescale variables?


What’s the problem?

  • Continuous predictors can measure the same thing on different scales: e.g., my height is 1.78 metres and also 1,780,000 micrometres.
  • Numbers on some scales are hard for the model to deal with, because they result in variance values too close to zero, which the model does not like.


Never trust any numbers from a model with unidentifiable parameters.

What to do about each issue

What to do about each issue


What to do about each issue


Change optimiser

LMMs arrive at their parameter values by initialising them to random values, and then adjusting them until they are the parameters that are maximally likely to have produced the data we observe.

There are several different ways a model might adjust (or optimise) these parameters.

If the default way is not working, then it might help to use a different optimiser.


One optimiser that generally works well is bobyqa (pronounced like “bo-BEE-ka”):

lmer(
  y ~ 1 + x * z + (1 + x * z | g), 
  data = df, 
  control = lmerControl(optimizer = "bobyqa")  # add in this line
)

What to do about each issue


What to do about each issue


Simplify random effect structure

Imagine you’re starting with the following maximal model:

y ~ x * z + (1 + x * z | g)

We simplify the model bit by bit, and try to fit it again after every simplification step. We stop once the model can successfully be fit without any further singularity.

There is no single correct way to simplify a model’s random effect structure.

This order of steps is what usually works well for me.


Simplification to make Model formula
[maximal model] y ~ x * z + (1 + x * z | g)
Remove the by-group adjustment to the interaction term. y ~ x * z + (1 + x + z | g)
If predictors x and z are continuous, remove the correlations between by-group adjustments (if predictors are categorical, other weird stuff happens and this step doesn’t really simplify anything). y ~ x * z + (1 + x + z || g)
Remove one of the by-group slope adjustments (if one is just a covariate, remove that one first). y ~ x * z + (1 + x | g)
If x is continuous, remove the correlations between by-group adjustments. y ~ x * z + (1 + x || g)
Remove the slope adjustments. y ~ x * z + (1 | g)

What to do about each issue


What to do about each issue


What to do about each issue


What to do about each issue


Rescale (z-score) predictors

There are lots of ways to rescale a predictor on a case-by-case basis (e.g., you could divide micrometres by one million to get metres). But the transformation that will pretty much always work is the z-score.


Recall that when you z-score a variable:

  • its mean becomes 0 (no matter what the variable’s mean originally was)
  • its SD becomes 1 (no matter what the variable’s SD originally was)
  • its units are SDs (no matter what the variable’s units originally were)


df <- df |>
  mutate(
    predictor_z = (predictor - mean(predictor)) / sd(predictor)
  )


Then use the predictor_z version in your model instead of the original predictor.

This week’s data (again):
Test-enhanced learning

This week’s data (again): Test-enhanced learning

RQ: Does self-testing improve retention, such that the StudyStudy group may perform better on the immediate test, but the StudyTest group will perform better on the test one week later?

Code
tel_data |>
  ggplot(aes(x = Delay, y = TestScore, colour = Delay, fill = Delay)) +
  geom_violin(alpha = 0.5) +
  geom_jitter(alpha = 0.15, size = 3) +
  facet_wrap(~ Group) +
  stat_summary(geom = 'point', fun = mean, colour = 'black', size = 5) +
  theme(
    legend.position = 'none',
    strip.background = element_blank(),
    strip.text.x = element_text(size = 24)
  ) +
  scale_colour_manual(values = pal) +
  scale_fill_manual(values = pal) +
  NULL

The data and the maximal model

tel_data |> head()
# A tibble: 6 x 5
  Subject_ID  Group     Delay Test_word TestScore
  <chr>       <chr>     <chr> <chr>         <dbl>
1 StudyTest_A StudyTest week  carrot           52
2 StudyTest_A StudyTest week  cane             60
3 StudyTest_A StudyTest min   scale            54
4 StudyTest_A StudyTest min   letter           79
5 StudyTest_A StudyTest week  cannon           62
6 StudyTest_A StudyTest week  pencil           30



The maximal model we worked out last week:

TestScore ~ Delay * Group + (1 + Delay | Subject_ID) + (1 + Delay * Group | Test_word)

Try to fit the maximal model

Try to fit the maximal model

library(lme4)      # for fitting LMMs
library(lmerTest)  # for getting p-values

m1 <- lmer(
  TestScore ~ Delay * Group + 
    (1 + Delay | Subject_ID) + 
    (1 + Delay * Group | Test_word),
  data = tel_data
)
boundary (singular) fit: see help('isSingular')
Warning: Model failed to converge with 1 negative eigenvalue: -1.3e+02

Will changing the optimiser help?

m2 <- lmer(
  TestScore ~ Delay * Group + 
    (1 + Delay | Subject_ID) + 
    (1 + Delay * Group | Test_word),
  data = tel_data,
  control = lmerControl(optimizer = "bobyqa")  # add in this line
)
boundary (singular) fit: see help('isSingular')

Nope, we still have a singular fit warning. The random effect structure is too complex for the data.


Simplifying random effects (1)

Simplification to make Model formula
[maximal model] y ~ x * z + (1 + x * z | g)
Remove the by-group adjustment to the interaction term. y ~ x * z + (1 + x + z | g)
If predictors x and z are continuous, remove the correlations between by-group adjustments (if predictors are categorical, other weird stuff happens and this step doesn’t really simplify the model). y ~ x * z + (1 + x + z || g)
Remove one of the by-group slope adjustments (if one is just a covariate, remove that one first). y ~ x * z + (1 + x | g)
If x is continuous, remove the correlations between by-group adjustments. y ~ x * z + (1 + x || g)
Remove the slope adjustments. y ~ x * z + (1 | g)


m3 <- lmer(
  TestScore ~ Delay * Group + 
    (1 + Delay | Subject_ID) + 
    (1 + Delay + Group | Test_word),  # before: (1 + Delay * Group | Test_word)
  data = tel_data
)
boundary (singular) fit: see help('isSingular')

Simplifying random effects (2)

Simplification to make Model formula
[maximal model] y ~ x * z + (1 + x * z | g)
Remove the by-group adjustment to the interaction term. y ~ x * z + (1 + x + z | g)
If predictors x and z are continuous, remove the correlations between by-group adjustments (if predictors are categorical, other weird stuff happens and this step doesn’t really simplify the model). y ~ x * z + (1 + x + z || g)
Remove one of the by-group slope adjustments (if one is just a covariate, remove that one first). y ~ x * z + (1 + x | g)
If x is continuous, remove the correlations between by-group adjustments. y ~ x * z + (1 + x || g)
Remove the slope adjustments. y ~ x * z + (1 | g)


m4 <- lmer(
  TestScore ~ Delay * Group + 
    (1 + Delay | Subject_ID) + 
    (1 + Delay | Test_word),  # before: (1 + Delay + Group | Test_word)
  data = tel_data
)
boundary (singular) fit: see help('isSingular')

Simplifying random effects (3)

Simplification to make Model formula
[maximal model] y ~ x * z + (1 + x * z | g)
Remove the by-group adjustment to the interaction term. y ~ x * z + (1 + x + z | g)
If predictors x and z are continuous, remove the correlations between by-group adjustments (if predictors are categorical, other weird stuff happens and this step doesn’t really simplify the model). y ~ x * z + (1 + x + z || g)
Remove one of the by-group slope adjustments (if one is just a covariate, remove that one first). y ~ x * z + (1 + x | g)
If x is continuous, remove the correlations between by-group adjustments. y ~ x * z + (1 + x || g)
Remove the slope adjustments. y ~ x * z + (1 | g)


m5 <- lmer(
  TestScore ~ Delay * Group + 
    (1 | Subject_ID) +
    (1 + Delay | Test_word),
  data = tel_data
)
boundary (singular) fit: see help('isSingular')

Simplifying random effects (4)

Simplification to make Model formula
[maximal model] y ~ x * z + (1 + x * z | g)
Remove the by-group adjustment to the interaction term. y ~ x * z + (1 + x + z | g)
If predictors x and z are continuous, remove the correlations between by-group adjustments (if predictors are categorical, other weird stuff happens and this step doesn’t really simplify the model). y ~ x * z + (1 + x + z || g)
Remove one of the by-group slope adjustments (if one is just a covariate, remove that one first). y ~ x * z + (1 + x | g)
If x is continuous, remove the correlations between by-group adjustments. y ~ x * z + (1 + x || g)
Remove the slope adjustments. y ~ x * z + (1 | g)


m6 <- lmer(
  TestScore ~ Delay * Group + 
    (1 + Delay | Subject_ID) +
    (1 | Test_word),
  data = tel_data
)

No warnings! But we still have to check for the sneakier singularities:

  • correlations of –1 or 1, and
  • group-level variance or SD of 0.

Check variance components for sneakier singularities

We can extract only the variance components in the model summary using VarCorr():

VarCorr(m6)
 Groups     Name        Std.Dev. Corr
 Test_word  (Intercept)  3.330       
 Subject_ID (Intercept)  5.755       
            Delayweek    0.798   0.19
 Residual               12.920       


Good! The correlation is not extreme, and the SDs are all different from 0!

Now we can trust the model: the model’s estimates will actually mean what we think they mean, and we can interpret it as usual.

m6 is the model that we would use to make inferences and respond to the RQ.

Reporting the process of model simplification

Tell the reader: what random effect structure would the maximal model have? (No need to explain how you figured this out.)

[after describing the outcome and predictors] The maximal random effect structure for this model would include by-subject random intercepts and random slopes over test delay, as well as by-word random intercepts and random slopes over test delay, over study group, and over their interaction.

Did the maximal model work? If not, what did you do about it?

The maximal model resulted in non-convergence and a singular fit, which was not remedied by changing optimisers, so we simplified the random effect structure until model fit was achieved.

When simplifying this model, what order did you remove parameters in?

First we simplified the by-word random effects. We removed the random slope over the interaction between test delay and experimental group, then the random slope over experimental group, but both times the model still had a singular fit. Then we tried to also remove the by-subject random slope over test delay. The model’s fit was still singular. So we replaced the by-subject random slope over delay, and instead removed the by-item random slope over delay. After that change, the model no longer had a singular fit.

What’s the random effect structure of the final simplified model?

The final simplified model contains by-subject random intercepts and random slopes over test delay, and by-word random intercepts only.

Check LMM assumptions

Why do models make assumptions?



An example of a model’s constraint:

  • Linear models can only model associations between predictors and outcomes as straight lines.
  • We cannot change that the model uses straight lines only.
  • So if we use linear models, we have to assume that a straight line is a reasonable way of representing the actual data generating process in the world.


A model’s unchangeable constraints = the assumptions that we must be satisfied with in order to use and interpret the model.

LMM assumptions: LINE + N = LINEN


The assumptions made by standard (non-logistic) linear models:

  • L = Linearity of association
  • I = Independence of errors
  • N = Normality of errors
  • E = Equal variance of errors


An additional assumption when we include random effects:

  • N = Normality of random effects

L = Linearity of association

  • A linear model can only model associations between predictor and outcome in terms of a straight line.
  • To be satisfied with a linear model of our data, we have to assume that the best possible straight line is a good enough representation of how variables are associated.
  • Note: If we only have categorical predictors, then this assumption is automatically met.

For LMMs, check with a fitted-vs-residuals plot:

plot(m6, type=c(
       "p",      # includes points representing Pearson residuals
       "smooth"  # includes smoothed line representing mean of residuals
))

We want the blue wobbly line to closely match the black horizontal line. This one does, so I would be satisfied that the association appears linear enough.

I = Independence of errors

  • A linear model can only model errors as residuals that are independent from one another.
  • To be satisfied with a linear model of our data, we have to assume that independent residuals are a good representation of the data.


  • In between-subjects data, we assume errors to be independent because we only have one observation per subject.

  • In repeated-measures data and/or when we have grouping variables which contribute random variability to the data, errors are not independent unless we model the grouping structure using random effects.


If we start with a maximal model (and simplify it as necessary to achieve convergence/fit), then we can be satisfied that errors are sufficiently independent.

N = Normality of errors

  • A linear model can only model errors as residuals that are normally distributed.
  • To be satisfied with a linear model of our data, we have to assume that normally-distributed residuals are a good representation of the data.

Check with a quantile–quantile (or “Q-Q”) plot:

qqnorm(resid(m6)); qqline(resid(m6))

We want the points to closely match the black diagonal line. These points match pretty well, so I’d be satisfied that the errors are sufficiently normally distributed.

E = Equal variance of errors
(aka “homoscedasticity”)

  • A linear model can only model errors as residuals that have equal variance across the whole range of fitted values.
  • To be satisfied with a linear model of our data, we have to assume that residuals with equal variance are a good representation of the data.

Check the residuals-vs-fitted plot again:

Code
plot(m6, type=c(
       "p",      # includes points representing Pearson residuals
       "smooth"  # includes smoothed line representing mean of residuals
))

We want the vertical spread of residuals to be approximately the same across the whole x axis. The vertical spread here looks pretty good (certainly no problematic fan/funnel type shapes), so I’m satisfied that variance of residuals is equal enough.

The new one! N = Normality of random effects

  • A linear mixed model can only model random effects (that is, the group-level adjustments to the fixed effects) as being normally distributed with a mean of zero.
  • To be satisfied with a linear model of our data, we have to assume that normally-distributed random effects are a good representation of the data.


As a reminder, here’s our model formula:

TestScore ~ Delay * Group + (1 + Delay | Subject_ID) + (1 | Test_word)


We can use Q-Q plots to check the normality of each set of group-level adjustments:

  1. By-subject random intercepts
  2. By-subject random slopes over Delay
  3. By-word random intercepts

N = Normality of random effects 1:
By-subject random intercepts

ranef(m6)$Subject_ID |> head(16)
             (Intercept) Delayweek
StudyStudy_A      -1.158   -0.2347
StudyStudy_B       1.566    0.3406
StudyStudy_C       0.997   -0.8721
StudyStudy_D       0.400    0.1464
StudyStudy_E      -1.926   -0.2929
StudyStudy_F       1.076    0.3756
StudyStudy_G       2.000    0.7630
StudyStudy_H      -1.435   -0.2231
StudyStudy_I       2.107    0.4229
StudyStudy_J      -3.627   -0.4259
StudyTest_A        4.582   -0.3913
StudyTest_B       -6.005   -0.4597
StudyTest_C        5.925    0.5407
StudyTest_D        5.670    0.0915
StudyTest_E      -11.549   -0.1114
StudyTest_F        7.636    0.0539
# one way:
qqnorm(ranef(m6)$Subject_ID$`(Intercept)`); qqline(ranef(m6)$Subject_ID$`(Intercept)`)

# another equivalent way:
qqnorm(ranef(m6)$Subject_ID[,1]); qqline(ranef(m6)$Subject_ID[,1])

Some divergence at the upper and lower edges. You might report that the by-subject random intercepts diverge a bit from normality at the extremes, so their estimated SD (which assumes a normal distribution) could be a little biased.

N = Normality of random effects 2:
By-subject random slopes over Delay

ranef(m6)$Subject_ID |> head(16)
             (Intercept) Delayweek
StudyStudy_A      -1.158   -0.2347
StudyStudy_B       1.566    0.3406
StudyStudy_C       0.997   -0.8721
StudyStudy_D       0.400    0.1464
StudyStudy_E      -1.926   -0.2929
StudyStudy_F       1.076    0.3756
StudyStudy_G       2.000    0.7630
StudyStudy_H      -1.435   -0.2231
StudyStudy_I       2.107    0.4229
StudyStudy_J      -3.627   -0.4259
StudyTest_A        4.582   -0.3913
StudyTest_B       -6.005   -0.4597
StudyTest_C        5.925    0.5407
StudyTest_D        5.670    0.0915
StudyTest_E      -11.549   -0.1114
StudyTest_F        7.636    0.0539
# one way:
qqnorm(ranef(m6)$Subject_ID$Delayweek); qqline(ranef(m6)$Subject_ID$Delayweek)

# another equivalent way:
qqnorm(ranef(m6)$Subject_ID[,2]); qqline(ranef(m6)$Subject_ID[,2])

Not too bad here, still a little off. So I might also report that the extreme by-subject slope adjustments also diverge slightly from normality.

N = Normality of random effects 3:
By-word random intercepts

ranef(m6)$Test_word |> head(16)
           (Intercept)
ambulance       -5.664
anchor          -1.831
apple            2.147
baby             4.308
ball             0.839
balloon          3.164
banana           2.129
basket          -0.905
bat              4.563
beard           -3.484
bed             -0.832
bell             0.621
belt            -0.360
bench           -2.013
binoculars      -9.497
bone             0.476
# one way:
qqnorm(ranef(m6)$Test_word$`(Intercept)`); qqline(ranef(m6)$Test_word$`(Intercept)`)

# another equivalent way:
qqnorm(ranef(m6)$Test_word[,1]); qqline(ranef(m6)$Test_word[,1])

The extremes here also diverge from the ideal diagonal. Again, not a big deal—just mention it when reporting/interpreting the by-word intercept adjustments.

Diagnostics for high-influence observations and groups

High influence

A single observation might influence our model’s estimates more than it should if

  • it has an unusual value for the outcome variable (i.e., if it’s an outlier),
  • it has an unusual value for the predictor variable (i.e., if it has high leverage),
  • or both.

Outlier:

High leverage:

Outlier with high leverage:


With LMMs, we will additionally check whether any specific levels of our grouping variable(s) are influencing the model’s estimates.


The R package we’ll use for this is called HLMdiag:

library(HLMdiag)    # Hierarchical Linear Model DIAGnostics

Check influence of individual observations

inf_obs <- hlm_influence(
  model = m6,       # name the LMM we want to check
  level = 1,        # look at the influence of each observation = level 1
  approx = TRUE     # just approximate the measures, don't actually re-fit models
)

inf_obs
# A tibble: 6,959 x 10
      id TestScore  Delay Group    Subject_ID Test_word  cooksd mdffits covtrace
   <int> <dw_trnsf> <fct> <fct>    <fct>      <fct>       <dbl>   <dbl>    <dbl>
 1     1 52         week  StudyTe~ StudyTest~ carrot    6.52e-6 6.52e-6 0.000221
 2     2 60         week  StudyTe~ StudyTest~ cane      6.40e-6 6.40e-6 0.000221
 3     3 54         min   StudyTe~ StudyTest~ scale     4.84e-6 4.84e-6 0.000236
 4     4 79         min   StudyTe~ StudyTest~ letter    1.53e-4 1.53e-4 0.000236
 5     5 62         week  StudyTe~ StudyTest~ cannon    2.48e-5 2.48e-5 0.000221
 6     6 30         week  StudyTe~ StudyTest~ pencil    1.81e-4 1.81e-4 0.000221
 7     7 63         week  StudyTe~ StudyTest~ piano     2.54e-5 2.54e-5 0.000221
 8     8 46         week  StudyTe~ StudyTest~ chimney   4.33e-6 4.33e-6 0.000221
 9     9 64         week  StudyTe~ StudyTest~ pen       2.13e-5 2.13e-5 0.000221
10    10 35         week  StudyTe~ StudyTest~ fireplace 8.92e-5 8.92e-5 0.000221
# i 6,949 more rows
# i 1 more variable: covratio <dbl>


We’ll just pay attention to the cooksd column, which tells us the Cook’s Distance of each observation.

Cook’s Distance is a measure of how much model estimates change when we remove this specific observation and fit the model again. Bigger numbers mean higher influence.

Plot influence of individual observations

dotplot_diag(             # this plotting function comes with HLMdiag
  inf_obs$cooksd,         # we want to plot the Cook's Distance column
  cutoff = "internal"     # show arbitrary threshold at the third quartile + 3*IQR
)


No massively outlying points here, so I wouldn’t worry about checking any of these individual observations.

Check influence of groups: Subject_ID

inf_subj <- hlm_influence(
  model = m6,
  level = "Subject_ID",   # now look at the influence of each subject
  approx = TRUE
)

inf_subj
# A tibble: 20 x 5
   Subject_ID    cooksd mdffits covtrace covratio
   <fct>          <dbl>   <dbl>    <dbl>    <dbl>
 1 StudyTest_A  0.0825  0.0743     0.220     1.23
 2 StudyTest_E  0.140   0.126      0.220     1.23
 3 StudyStudy_E 0.0145  0.0130     0.220     1.23
 4 StudyStudy_G 0.106   0.0957     0.220     1.23
 5 StudyTest_D  0.0320  0.0288     0.220     1.23
 6 StudyStudy_I 0.0305  0.0274     0.220     1.23
 7 StudyTest_J  0.0791  0.0713     0.220     1.23
 8 StudyTest_C  0.0575  0.0518     0.220     1.23
 9 StudyStudy_D 0.00387 0.00349    0.220     1.23
10 StudyStudy_B 0.0198  0.0179     0.220     1.23
# i 10 more rows

Plot influence of individual subjecs

dotplot_diag(
  inf_subj$cooksd,
  cutoff = "internal"     # show arbitrary threshold at the third quartile + 3*IQR
)


No points at all beyond the threshold! All good, nothing to check.

Check influence of groups: Test_word

inf_word <- hlm_influence(
  model = m6,
  level = "Test_word",   # now look at the influence of each word
  approx = TRUE
)

inf_word
# A tibble: 174 x 5
   Test_word    cooksd   mdffits covtrace covratio
   <fct>         <dbl>     <dbl>    <dbl>    <dbl>
 1 carrot    0.000395  0.000393   0.00919     1.01
 2 cane      0.00153   0.00152    0.00919     1.01
 3 scale     0.00192   0.00191    0.00919     1.01
 4 letter    0.000178  0.000177   0.00919     1.01
 5 cannon    0.000648  0.000646   0.00919     1.01
 6 pencil    0.0000649 0.0000646  0.00919     1.01
 7 piano     0.000917  0.000913   0.00919     1.01
 8 chimney   0.000566  0.000564   0.00919     1.01
 9 pen       0.000789  0.000786   0.00919     1.01
10 fireplace 0.00451   0.00449    0.00919     1.01
# i 164 more rows

Plot influence of individual words

dotplot_diag(
  inf_word$cooksd,
  index = inf_word$Test_word,   # label dots with Test_word value, not numbers
  cutoff = "internal"     # show arbitrary threshold at the third quartile + 3*IQR
)


Because “saddle” and “nose” are beyond the threshold and have higher Cook’s Distances compared to the other data points, let’s double-check whether those items might be influencing our model’s results.

To do that, we run a sensitivity analysis.

Sensitivity analysis

Sensitivity analysis

The key idea:

  • Remove each of the potentially-influential observations/levels, fit the same LMM again to that reduced dataset, and see if our conclusions change.
  • Even if an observation has a big Cook’s Distance, it might not be very impactful unless it actually changes the conclusions we’d draw from the model.

Code template for re-fitting the model with a specific observation removed (say, number 100):

lmer(
  # same model formula as m6 (simplified model)
  TestScore ~ Delay * Group + (1 + Delay | Subject_ID) + (1 | Test_word),
  
  data = tel_data[-100,]    # remove row 100 from tel_data
)


Code template for re-fitting the model with a specific level of a grouping variable removed (say, “saddle”):

lmer(
  # same model formula as m6 (simplified model)
  TestScore ~ Delay * Group + (1 + Delay | Subject_ID) + (1 | Test_word),
  
  data = filter(            # using tidyverse's filter() function
    tel_data,               # name the original df
    Test_word != "saddle"   # keep all rows where Test_word isn't "saddle"
  )
)

Sensitivity analysis for “saddle”

m6_saddleless <- lmer(
  TestScore ~ Delay * Group + (1 + Delay | Subject_ID) + (1 | Test_word),
  data = filter(tel_data, Test_word != "saddle")
)


The original model’s fixed effects:

                          Estimate Std. Error      df t value Pr(>|t|)    
 (Intercept)                58.105      1.863  18.677   31.18  < 2e-16 ***
 Delayweek                 -17.698      0.506  18.007  -35.01  < 2e-16 ***
 GroupStudyTest             -4.247      2.611  17.999   -1.63     0.12    
 Delayweek:GroupStudyTest   13.550      0.715  18.003   18.95  2.4e-13 ***


The “saddle”-less model’s fixed effects:

                          Estimate Std. Error      df t value Pr(>|t|)    
 (Intercept)                58.114      1.864  18.687   31.18  < 2e-16 ***
 Delayweek                 -17.664      0.506  18.006  -34.92  < 2e-16 ***
 GroupStudyTest             -4.300      2.611  18.002   -1.65     0.12    
 Delayweek:GroupStudyTest   13.613      0.715  18.002   19.03  2.3e-13 ***


No change to the significance of the fixed effects, so our conclusions from this model are not driven by the word “saddle”.

Sensitivity analysis for “nose”

m6_noseless <- lmer(
  TestScore ~ Delay * Group + (1 + Delay | Subject_ID) + (1 | Test_word),
  data = filter(tel_data, Test_word != "nose")
)


The original model’s fixed effects:

                         Estimate Std. Error      df t value Pr(>|t|)    
 (Intercept)                58.105      1.863  18.677   31.18  < 2e-16 ***
 Delayweek                 -17.698      0.506  18.007  -35.01  < 2e-16 ***
 GroupStudyTest             -4.247      2.611  17.999   -1.63     0.12    
 Delayweek:GroupStudyTest   13.550      0.715  18.003   18.95  2.4e-13 ***


The “nose”-less model’s fixed effects:

                         Estimate Std. Error      df t value Pr(>|t|)    
 (Intercept)                58.114      1.869  18.679   31.10  < 2e-16 ***
 Delayweek                 -17.794      0.507  18.007  -35.09  < 2e-16 ***
 GroupStudyTest             -4.272      2.619  17.999   -1.63     0.12    
 Delayweek:GroupStudyTest   13.682      0.717  18.003   19.08  2.2e-13 ***


No change to the significance of the fixed effects, so our conclusions from this model are also not driven by the word “nose”.

Reporting sensitivity analyses

Tell the reader: Which observation(s) or level(s) of the grouping variable did you leave out?

Two test words had higher Cook’s Distance values than the others: “saddle” and “nose”. We ran two sensitivity analyses, one to check for the influence of each of these words on the model’s estimates. Specifically, we re-fit the model once to all data except the word “saddle”, and again to all data except the word “nose”.

Did the overall pattern of results change?

The pattern of results did not change after leaving out each of these words.

What should the reader take away from this analysis?

Therefore the higher-influence words “saddle” and “nose” do not affect the conclusions we would draw from the model.

It’s often nice to include the sensitivity analysis model summary/summaries in the appendix.

See Appendix X for all sensitivity analysis model summaries.

What questions do you have right now?


Back matter

Learning objectives revisited (1)

What are four common issues that you might encounter when trying to fit LMMs, and how can you solve each one?

  • Model failed to converge \(\rightarrow\) change optimiser, simplify random effects
  • Singularity / singular fit \(\rightarrow\) simplify random effects
  • Not enough observations for random effect structure \(\rightarrow\) simplify random effects
  • Variables need rescaling \(\rightarrow\) rescale (z-score) predictors

What five assumptions does an LMM make?

  • L = Linearity of association
  • I = Independence of errors
  • N = Normality of errors
  • E = Equal variance of errors
  • N = Normality of random effects

Learning objectives revisited (2)

How do you check each assumption in an LMM?

  • Linearity of association \(\rightarrow\) residuals-vs-fitted plot, look for flat line
  • Independence of errors \(\rightarrow\) start with maximal model and simplify as necessary
  • Normality of errors \(\rightarrow\) Q-Q plot of model residuals
  • Equal variance of errors \(\rightarrow\) residuals-vs-fitted plot, look for uniform cloud
  • Normality of random effects \(\rightarrow\) Q-Q plot of each set of by-group adjustments

How can you detect influential observations or influential groups in an LMM?

  • Compute the Cook’s Distance of each observation and each group
  • Run a sensitivity analysis to see any observations or groups with big Cook’s Distances actually impact the conclusions you’d draw

To do this week


Tasks:


Work on exercises in labs


Complete the weekly quiz

Get support:


Consult the flash cards


Ask questions anonymously on Piazza


We really like seeing you in office hours!

Why are there two blobs/clusters in the residuals-vs-fitted plot?

Code
plot(m6, type=c(
       "p",      # includes points representing Pearson residuals
       "smooth"  # includes smoothed line representing mean of residuals
))

Code
tel_data |>
  ggplot(aes(x = Delay, y = TestScore, colour = Delay, fill = Delay)) +
  geom_violin(alpha = 0.5) +
  geom_jitter(alpha = 0.1, size = 3) +
  facet_wrap(~ Group) +
  stat_summary(geom = 'point', fun = mean, colour = 'black', size = 5) +
  theme(
    legend.position = 'none',
    strip.background = element_blank(),
    strip.text.x = element_text(size = 24)
  )

Because the data itself is kind of clustered around particular TestScore values.

  • Lots of observations around 40 (where there’s one cloud of fitted(.) values)
  • Lots of observations between 50 and 60 (where there’s another cloud of fitted(.) values)