Generalised additive models

Modelling curves with smooth terms

Dr Massimiliano Canzi

Generalised additive models

Did you know: humpback whales produce a varied series of sounds for 7 to 30 minutes and then repeat the same series with considerable precision. Payne and McVay called each repeated series a song.

Source: Payne & McVay (1971), Science

Where we are

So far, every model in this course has been linear in its predictors. Many linguistic data are curves: formant trajectories, pitch contours, tongue contours, pupil size over a trial, looks to a target over time.

This lecture and the next one follow Márton Sóskuthy’s GAMM workshop (Analysing Curves, University of Manchester, 2018) and his written tutorial (Sóskuthy, 2017), with simulated data and ggplot2 figures.

Today

  1. Curves
  2. Smooth terms
  3. Fitting GAMs with {mgcv}
  4. Comparing curves

The data

F2 of the PRICE vowel at 11 points (0% to 100% of the vowel), 10 tokens from each of 20 speakers: 10 with English and 10 with Italian as their L1.

set.seed(2029)
speakers <- tibble(speaker = sprintf("S%02d", 1:20),
                   l1 = rep(c("English", "Italian"), each = 10),
                   s0 = rnorm(20, 0, 60),
                   s_mid = rnorm(20, 0, 5))
tokens <- crossing(speakers, token = 1:10) |>
  mutate(traj = paste(speaker, token, sep = "."),
         duration = round(runif(n(), 120, 300)),
         t0 = rnorm(n(), 0, 40))
price <- crossing(tokens, time = seq(0, 100, by = 10)) |>
  arrange(traj, time) |>
  mutate(onset = if_else(l1 == "English", 1250, 1320),
         offset = if_else(l1 == "English", 2050, 1900) - 2 * (250 - duration),
         mid = if_else(l1 == "English", 62, 50) + s_mid,
         width = if_else(l1 == "English", 8, 14),
         f2 = onset + (offset - onset) * plogis((time - mid) / width) + s0 + t0) |>
  group_by(traj) |>
  mutate(f2 = round(f2 + as.numeric(arima.sim(list(ar = 0.6), n = n(), sd = 30)))) |>
  ungroup() |>
  mutate(across(c(speaker, l1, traj), factor)) |>
  select(speaker, l1, traj, duration, time, f2)

The data

price
# A tibble: 2,200 × 6
  speaker l1      traj  duration  time    f2
  <fct>   <fct>   <fct>    <dbl> <dbl> <dbl>
1 S01     English S01.1      252     0  1199
2 S01     English S01.1      252    10  1232
3 S01     English S01.1      252    20  1174
4 S01     English S01.1      252    30  1194
# ℹ 2,196 more rows

What we built in: the glide from [a] to [ɪ] is smaller and earlier for the Italian speakers, short vowels undershoot the glide target, and neighbouring measurements in a token are correlated.

The trajectories

ggplot(price, aes(x = time, y = f2, group = traj)) +
  geom_line(colour = kingfisher, alpha = 0.25) +
  facet_wrap(~ l1) +
  labs(x = "Time (% of vowel duration)", y = "F2 (Hz)")

Each line is one token. The research question is whether the two groups produce different F2 trajectories.

Curves

Did you know: humpback whale song is built in layers. Single sounds (units) form phrases, a repeated phrase forms a theme, and a fixed sequence of themes forms the song.

Source: Payne & McVay (1971), Science

A straight line

The line misses the shape, and the residuals follow a pattern: neighbouring residuals are similar. This is autocorrelation, and it makes standard errors unreliable.

Polynomials

  • A quadratic or cubic term underfits: it cannot follow the flat start and the late rise
  • A high-degree polynomial overfits: it bends to follow the noise

Nothing in the data says which degree to choose.

Smooth terms

Did you know: humpback whales off eastern Australia replaced their song within two years. A few singers from the west coast had brought a new song, and by 1998 only the new song was heard.

Source: Noad et al. (2000), Nature

Smooth terms

A generalised additive model can contain smooth terms next to the usual parametric terms:

y = α + β₁ · x₁ + f(x₂) + e

f(x₂) is a smooth function of x₂ whose shape is estimated from the data. It is built as a sum of simpler functions, the basis functions, much as a polynomial is a sum of powers of x. A smoothing penalty keeps it from following the noise.

Basis functions

Each basis function is multiplied by a coefficient; the sum (salmon), plus the intercept, is the fitted curve. bs = "cr" asks for cubic regression splines, and k = 10 gives 10 basis functions, one of which is absorbed by the intercept.

How wiggly?

  • k sets the maximum wiggliness: how many basis functions the smooth may use
  • The smoothing parameter sets the actual wiggliness: the larger it is, the more wiggliness is penalised
  • {mgcv} estimates the smoothing parameter from the data, so the strategy is to set k high enough and let the penalty do the rest
  • The effective degrees of freedom (edf) describe the result: an edf of 1 is a straight line, and higher values mean more wiggly curves

The smoothing parameter

The same 10 basis functions with four smoothing parameters. A large penalty pushes the smooth toward a straight line.

Fitting GAMs with {mgcv}

Did you know: over 11 years, 11 song types spread across the humpback whale populations of the South Pacific, usually starting off eastern Australia and reaching French Polynesia within about two years.

Source: Garland et al. (2011), Current Biology

gam() and bam()

  • {mgcv} (Wood, 2017) fits GAMs with gam(), and with bam(), which is much faster on big data sets such as curves
  • s(time) is a smooth over time; s(time, by = l1) fits a separate smooth for each group
  • Smooths in a by model are centred on zero, so the group difference in height needs its own parametric term, l1
  • method = "ML" is needed to compare models that differ in their fixed effects; the default of bam() is "fREML"

A first GAM

m1 <- bam(f2 ~ l1 + s(time, by = l1, k = 10), data = price, method = "ML")
round(summary(m1)$p.table, 2)
            Estimate Std. Error t value Pr(>|t|)
(Intercept)  1555.40       3.38  460.62     0.00
l1Italian      -4.78       4.78   -1.00     0.32
round(summary(m1)$s.table, 2)
                   edf Ref.df      F p-value
s(time):l1English 7.54   8.48 888.25       0
s(time):l1Italian 4.92   6.01 424.13       0

Reading the summary

  • The parametric table works as in lm(): the intercept is the mean F2 of the English trajectories, and l1Italian is how much higher or lower the Italian trajectories are on average
  • The smooth table has one row per smooth: edf close to 9 means a very wiggly curve, close to 1 a straight line

The F test asks whether a smooth is flat, so two identical curves would both come out significant. Testing whether two smooths differ needs a different model, later in this lecture.

Plotting with {tidygam}

m1_pred <- predict_gam(m1, series = "time", length_out = 50)
plot(m1_pred, series = "time", comparison = "l1")

predict_gam() returns a tibble of predictions with standard errors and 95% confidence intervals; its plot() method draws them with ggplot2.

Your own plot

ggplot(m1_pred, aes(x = time, y = f2, colour = l1, fill = l1)) +
  geom_ribbon(aes(ymin = lower_ci, ymax = upper_ci), alpha = 0.3, colour = NA) +
  geom_line(linewidth = 1.2) +
  labs(x = "Time (% of vowel duration)", y = "Predicted F2 (Hz)", colour = NULL, fill = NULL) +
  theme(legend.position = "right")

m1_pred is an ordinary tibble with the predictor values, the predicted f2, its standard error se, and the limits lower_ci and upper_ci.

Is k high enough?

k.check(m1)
                  k'      edf   k-index p-value
s(time):l1English  9 7.540370 0.5732537       0
s(time):l1Italian  9 4.924248 0.5732537       0
  • k' is the number of basis functions available; edf is how many the smooth used
  • A k-index below 1 with a small p-value suggests there is pattern left in the residuals that more basis functions could capture

To be sure, raise k (for example to 15) and refit. If the curves and the edf barely change, k was high enough.

Choosing k

  • Start from what you expect: a formant trajectory over 11 points rarely needs more than 10 basis functions; a pitch contour over a whole sentence may need 20 or more
  • Look at the fitted curves against the data, compare edf with k', and use k.check()

In Sóskuthy’s (2018) workshop example, gam.check() flagged smooths whose wiggliness simulations showed to be fine. Use these checks as guides, together with the plots.

Comparing curves

Did you know: in each humpback whale population, only males sing, and all of them sing the current version of the song, which keeps changing over the years.

Source: Garland et al. (2011), Current Biology

Reference and difference smooths

To test whether two curves differ, the model needs a term for the difference. With an ordered factor in by, s(time) becomes the reference smooth for the first level, and s(time, by = l1_o) the difference between the other level and the reference.

price <- mutate(price, l1_o = as.ordered(l1))
contrasts(price$l1_o) <- "contr.treatment"
m2 <- bam(f2 ~ l1_o + s(time, k = 10) + s(time, by = l1_o, k = 10),
          data = price, method = "ML")

The difference smooth

round(summary(m2)$p.table, 2)
            Estimate Std. Error t value Pr(>|t|)
(Intercept)  1555.40       3.38  460.36     0.00
l1_oItalian    -4.78       4.78   -1.00     0.32
round(summary(m2)$s.table, 2)
                     edf Ref.df      F p-value
s(time)             7.75   8.51 883.90       0
s(time):l1_oItalian 6.59   7.65 108.44       0
  • s(time) is the English trajectory, and s(time):l1_oItalian is the difference smooth: the Italian trajectory minus the English one, apart from the overall difference in height captured by l1_oItalian
  • m1 and m2 fit the same curves in two parametrisations

Plotting the difference

m2_diff <- get_difference(m2, "time", list(l1_o = c("Italian", "English")), length_out = 50)
ggplot(m2_diff, aes(x = time, y = diff)) +
  geom_hline(yintercept = 0, colour = muted, linetype = "dashed") +
  geom_ribbon(aes(ymin = lower_ci, ymax = upper_ci), fill = kingfisher, alpha = 0.3) +
  geom_line(colour = kingfisher, linewidth = 1.2) +
  labs(x = "Time (% of vowel duration)", y = "Italian − English F2 (Hz)")

Where the 95% confidence interval excludes zero, the curves differ. plot(m2_diff) shades those stretches automatically.

Model comparison

m2_null <- bam(f2 ~ s(time, k = 10), data = price, method = "ML")
itsadug::compareML(m2_null, m2)
m2_null: f2 ~ s(time, k = 10)

m2: f2 ~ l1_o + s(time, k = 10) + s(time, by = l1_o, k = 10)

Chi-square test of ML scores
-----
    Model    Score Edf Difference    Df  p.value Sig.
1 m2_null 13869.68   3                               
2      m2 13529.22   6    340.453 3.000  < 2e-16  ***

AIC difference: 699.89, model m2 has lower AIC.

The null model drops both the height difference and the difference smooth. Both models are fitted with method = "ML".

The binary smooth

price <- mutate(price, is_italian = as.numeric(l1 == "Italian"))
m2_bin <- bam(f2 ~ s(time, k = 10) + s(time, by = is_italian, k = 10),
              data = price, method = "ML")
  • In a binary smooth, the by variable is numeric (0 or 1), so the smooth is not centred, so a single term covers both the difference in height and the difference in shape
  • Its p-value tests both differences at once, which is often the question of interest

Which test to report?

Sóskuthy’s (2018) recommendations:

  • Use the model summary only as a rough guide
  • Report the significance from a model comparison
  • Use prediction plots and the difference smooth for the qualitative discussion: where and how the curves differ

He calls the binary smooth probably ideal when, as here, a single difference between two curves is the question.

The catch

These models treat all 2,200 measurements as independent, although 11 come from each token and 110 from each speaker.

tibble(res = residuals(m2), lagged = lag(res), time = price$time) |>
  filter(time > 0) |>
  ggplot(aes(x = lagged, y = res)) +
  geom_point(colour = kingfisher, alpha = 0.2) +
  labs(x = "Residual at the previous time point", y = "Residual")

Neighbouring residuals are correlated, so the confidence intervals above are too narrow. Lecture 4 deals with this.

Summary

  • Polynomials underfit or overfit curves, and leave the choice of degree to the analyst
  • A smooth term is a weighted sum of basis functions; k sets the maximum wiggliness and the smoothing penalty the actual wiggliness
  • The smooth table tests whether each smooth is flat; an ordered factor in by gives a reference smooth and a difference smooth; plot the difference with its confidence interval
  • Report the significance from a model comparison; describe the curves with prediction plots and the difference smooth
  • Models without random effects or an error model are overconfident with curve data

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
  • Sóskuthy, M. (2018). Introducing generalised additive models and Becoming a GAM(M) warrior [Workshop slides]. Analysing Curves, University of Manchester, 29 May 2018.
  • Sóskuthy, M. (2021). Evaluating generalised additive mixed modelling strategies for dynamic speech analysis. Journal of Phonetics, 84, 101017. https://doi.org/10.1016/j.wocn.2020.101017
  • van Rij, J., Wieling, M., Baayen, R. H., & van Rijn, H. itsadug: Interpreting time series and autocorrelated data using GAMMs (R package). https://CRAN.R-project.org/package=itsadug
  • Wood, S. N. (2017). Generalized additive models: An introduction with R (2nd ed.). CRC Press.

Sources of the animal facts

  • Garland, E. C., Goldizen, A. W., Rekdahl, M. L., Constantine, R., Garrigue, C., Hauser, N. D., Poole, M. M., Robbins, J., & Noad, M. J. (2011). Dynamic horizontal cultural transmission of humpback whale song at the ocean basin scale. Current Biology, 21(8), 687–691. https://doi.org/10.1016/j.cub.2011.03.019
  • Noad, M. J., Cato, D. H., Bryden, M. M., Jenner, M.-N., & Jenner, K. C. S. (2000). Cultural revolution in whale songs. Nature, 408, 537. https://doi.org/10.1038/35046199
  • Payne, R. S., & McVay, S. (1971). Songs of humpback whales. Science, 173(3997), 585–597. https://doi.org/10.1126/science.173.3997.585

Slides and code

QR code linking to these slides

These slides

Code and all lectures