GAMMs in practice

A guided analysis of pupil curves

Dr Massimiliano Canzi

GAMMs in practice

Did you know: goats have horizontally elongated pupils. In a survey of 214 land species, this shape went with plant-eating prey animals, with few exceptions, while vertical slits were more common in ambush predators.

Source: Banks et al. (2015), Science Advances

How today works

Today you run a complete GAMM analysis yourselves, in pairs, on a new data set.

  • Each task slide says what to do, with code to complete where ___ appears
  • A checkpoint slide after some tasks shows what you should get
  • The solutions are in the .Rmd file of these slides, on GitHub. Try first; look only when you are stuck

The study

A simulated pupillometry experiment on listening effort. 24 participants heard 12 sentences in quiet and 12 in background noise. Pupil size was recorded from sentence onset, baseline-corrected and averaged into 100 ms bins from 0 to 3,000 ms.

The research question: does noise change the pupil response, and from what point in time?

The data

set.seed(2031)
participants <- tibble(participant = sprintf("P%02d", 1:24),
                       p_amp = rnorm(24, 0, 40), p_lat = rnorm(24, 0, 150), p_noise = rnorm(24, 0, 30))
trials <- crossing(participants, condition = c("quiet", "noise"), trial = 1:12) |>
  mutate(trial_id = paste(participant, condition, trial, sep = "."),
         t_amp = rnorm(n(), 0, 40), t0 = rnorm(n(), 0, 15))
pdata <- crossing(trials, time = seq(0, 3000, by = 100)) |>
  arrange(trial_id, time) |>
  mutate(peak = 180 + p_amp + if_else(condition == "noise", 90 + p_noise, 0) + t_amp - 4 * (trial - 6.5),
         latency = 1200 + p_lat + if_else(condition == "noise", 250, 0),
         pupil = t0 + peak * (time / latency)^3 * exp(3 * (1 - time / latency))) |>
  group_by(trial_id) |>
  mutate(pupil = round(pupil + as.numeric(arima.sim(list(ar = 0.8), n = n(), sd = 12)), 1)) |>
  ungroup() |>
  mutate(across(c(participant, condition, trial_id), factor),
         condition = fct_relevel(condition, "quiet")) |>
  select(participant, condition, trial, trial_id, time, pupil)

Single trials

Four trials per condition from participant P01. Single trials are noisy and differ in height: the random smooths and the AR1 model will have to deal with both.

Task 1: look at the data

  1. Run the code on the previous slide and look at pdata. How many rows are there, and how many trajectories?
  2. Compute the mean pupil size per participant, condition and time bin
  3. Plot one line per participant, with a panel for each condition

Before fitting anything, write down what you expect the model to find.

Checkpoint 1

17856 rows and 576 trajectories. In noise, the response tends to be larger and later, with a lot of variation between participants.

Building the model

Did you know: when a goat lowers its head to graze, its eyes rotate so that the pupils stay parallel to the ground. Grazing animals can rotate each eye by 50 degrees or more, about ten times the range of human eyes.

Source: Banks et al. (2015), Science Advances

Task 2: a first GAM

pdata$condition_o <- as.ordered(pdata$condition)
contrasts(pdata$condition_o) <- "contr.treatment"
m_gam <- bam(pupil ~ ___ + s(time, k = 20) + s(time, by = ___, k = 20),
             data = pdata, method = "ML")
summary(m_gam)
  • Complete the formula with the ordered factor, as in lecture 3. Which level is the reference?
  • What does each smooth in the summary describe?
  • Is k = 20 enough? Check with k.check()

Task 3: is anything left in the residuals?

resid_acf <- function(res, traj, max_lag = 8) {
  d <- tibble(res, traj)
  map(0:max_lag, \(l) d |>
    mutate(res_lag = lag(res, l), .by = traj) |>
    summarise(acf = cor(res, res_lag, use = "complete.obs")) |>
    mutate(lag = l)) |>
    list_rbind()
}

The same function as in lecture 4. Compute the autocorrelation of the residuals of m_gam within trajectories, and plot it against the lag.

Task 4: random smooths

Every participant heard both conditions, so the comparison is within participants. Participants can differ in their curves and also in how noise changes their curves.

pdata$part_cond <- interaction(pdata$participant, pdata$condition)
m_rs <- bam(pupil ~ condition_o + s(time, k = 20) + s(time, by = condition_o, k = 20) +
              s(time, ___, bs = "fs", m = 1, k = 5),
            data = pdata, method = "ML")
  • Why part_cond and not participant? Look back at “The random slope equivalent” in lecture 4
  • How many levels does part_cond have, and how many coefficients do the random smooths add?

Task 5: an AR1 error model

rho <- itsadug::start_value_rho(m_rs)
m_ar <- bam(pupil ~ condition_o + s(time, k = 20) + s(time, by = condition_o, k = 20) +
              s(time, part_cond, bs = "fs", m = 1, k = 5),
            data = pdata, method = "ML", AR.start = ___, rho = rho)
  1. Mark the first bin of every trajectory in AR.start. Is the data frame sorted the way the AR1 model needs?
  2. Compare the autocorrelation of residuals(m_gam), residuals(m_rs) and m_ar$std.rsd

Checkpoint 2

Your rho should be close to 0.92. With the AR1 model, little autocorrelation should remain.

Results

Did you know: goats can tell human facial expressions apart. Shown photos of the same unfamiliar person looking happy and angry, 20 goats preferred to approach and explore the happy faces, mainly when these were on the right.

Source: Nawroth et al. (2018), Royal Society Open Science

Task 6: check the model

  • Run k.check(m_ar). Does any smooth need a higher k?
  • Plot the standardised residuals (m_ar$std.rsd) against the fitted values, and make a QQ plot. Lecture 4 has the code
  • Would you trust the confidence intervals of this model more than those of m_gam? Why?

Task 7: test the effect of noise

Fit a null model without any term for condition, and compare it with m_ar using itsadug::compareML().

Before you run it, decide what the null model must keep from m_ar, and what must go. If the two models differ in more than the condition terms, the comparison does not test noise alone.

Task 8: plot the results

With {tidygam}, plot:

  1. the predicted curves for the two conditions, with 95% confidence intervals, for an average participant
  2. the difference smooth, noise minus quiet, with a line at zero

Use exclude_terms for the random smooths, as in lecture 4. Then make the figure ready for a paper, as in lecture 2: axis titles with units, a caption that says what the ribbons are, colours that survive greyscale.

Checkpoint 3

Task 9: interpret and report

  • From roughly what time does the confidence interval of the difference exclude zero? Is that the same as “noise has an effect from that time on”?
  • Write a reporting paragraph for a methods and results section, like the one at the end of lecture 4: the model, the random effects and error model with their settings, the test and its result, and a figure caption

Swap paragraphs with another pair. Could they refit your model from your description alone?

If you finish early

The simulation has one more effect built in: the pupil response gets smaller over the 12 trials, as participants tire or get used to the task.

  • Add s(trial, k = 5) to m_ar. Does the model improve?
  • Then add ti(time, trial). Does fatigue only shrink the curve, or does it change its shape too?

Summary

  1. Plot the raw curves first, and write down what you expect
  2. Fit the fixed structure with an ordered factor: a reference smooth plus a difference smooth
  3. Check the residuals for autocorrelation
  4. Add random smooths that match the design: within-participant conditions need participant × condition smooths
  5. Add an AR1 error model and check the standardised residuals
  6. Test with ML-fitted models that differ only in the terms of interest, then plot and report

References

  • Coretta, S. (2024). tidygam: Tidy prediction and plotting of generalised additive models (R package version 1.0.0). https://github.com/stefanocoretta/tidygam
  • Sóskuthy, M. (2017). Generalised additive mixed models for dynamic analysis in linguistics: A practical introduction. arXiv, 1703.05339. https://arxiv.org/abs/1703.05339
  • van Rij, J., Hendriks, P., van Rijn, H., Baayen, R. H., & Wood, S. N. (2019). Analyzing the time course of pupillometric data. Trends in Hearing, 23, 1–22. https://doi.org/10.1177/2331216519832483
  • Wood, S. N. (2017). Generalized additive models: An introduction with R (2nd ed.). CRC Press.

Sources of the animal facts

  • Banks, M. S., Sprague, W. W., Schmoll, J., Parnell, J. A. Q., & Love, G. D. (2015). Why do animal eyes have pupils of different shapes? Science Advances, 1(7), e1500391. https://doi.org/10.1126/sciadv.1500391
  • Nawroth, C., Albuquerque, N., Savalli, C., Single, M.-S., & McElligott, A. G. (2018). Goats prefer positive human emotional facial expressions. Royal Society Open Science, 5(8), 180491. https://doi.org/10.1098/rsos.180491

Slides and code

QR code linking to these slides

These slides

Code and all lectures