Did you know: Egyptian fruit bats live in colonies of
hundreds to thousands of individuals, can live for at least 25 years in
captivity, and are extremely vocal in their
social interactions.
Source: Prat, Taub & Yovel (2015), Science Advances
Last time: smooth terms, difference smooths and significance testing with GAMs, in models that treated every measurement as independent. Today: the “mixed” part, following the second half of Sóskuthy’s (2018) workshop.
The PRICE trajectories from lecture 3, now with an ordered factor for 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), l1_o = as.ordered(l1)) |>
select(speaker, l1, l1_o, traj, duration, time, f2)
contrasts(price$l1_o) <- "contr.treatment"Ten tokens from each of four speakers. Each speaker’s tokens sit at their own height, and within a token, neighbouring measurements move up and down together.
Did you know: Prat and colleagues recorded 22 Egyptian
fruit bats for 75 days. Their calls carried information about the
caller, the context and even the addressee: the
bat the call was directed at.
Source: Prat, Taub & Yovel (2016), Scientific Reports
| Fixed effects only | With random effects | |
|---|---|---|
| Parametric terms | LM, lm() |
LMER, lmer() |
| Smooth terms | GAM, bam() |
GAMM, bam() |
In {mgcv}, random effects are written as smooth terms,
so the same function fits GAMs and GAMMs.
Lecture 3’s model ignored both, so its confidence intervals were too narrow.
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()
}
m_gam <- bam(f2 ~ l1_o + s(time, k = 10) + s(time, by = l1_o, k = 10),
data = price, method = "ML")
acf_gam <- resid_acf(residuals(m_gam), price$traj)The autocorrelation at lag 1 is the
correlation between each residual and the residual one measurement
earlier in the same trajectory; lag 2 skips one measurement, and so on.
itsadug::acf_resid() does the same with base R
graphics.
ggplot(acf_gam, aes(x = lag, y = acf)) +
geom_hline(yintercept = 0, colour = muted) +
geom_segment(aes(xend = lag, yend = 0), colour = kingfisher, linewidth = 1.2) +
geom_point(colour = kingfisher, size = 3) +
labs(x = "Lag (measurements)", y = "Autocorrelation")Neighbouring residuals correlate at about 0.94, and the correlation is still clearly positive several measurements apart.
Did you know: fruit bat pups raised without hearing
adult bats kept producing underdeveloped, pup-like calls far into adolescence.
Source: Prat, Taub & Yovel (2015), Science Advances
{mgcv} term |
Random effect |
|---|---|
s(speaker, bs = "re") |
random intercepts by speaker |
s(speaker, time, bs = "re") |
random slopes over time by speaker |
s(time, speaker, bs = "fs", m = 1) |
random smooths over time by speaker |
Watch the order: random slopes put the grouping factor first, random smooths put the continuous predictor first.
m_int <- bam(f2 ~ l1_o + s(time, k = 10) + s(time, by = l1_o, k = 10) +
s(speaker, bs = "re"),
data = price, method = "ML")
m_slope <- bam(f2 ~ l1_o + s(time, k = 10) + s(time, by = l1_o, k = 10) +
s(speaker, bs = "re") + s(speaker, time, bs = "re"),
data = price, method = "ML")
m_smooth <- bam(f2 ~ l1_o + s(time, k = 10) + s(time, by = l1_o, k = 10) +
s(time, speaker, bs = "fs", m = 1, k = 5),
data = price, method = "ML")One line per speaker. Intercepts shift each speaker’s curve up or down, slopes tilt it, and random smooths let each speaker’s curve take its own shape.
bs = "fs" fits one smooth per speaker,
with a shared smoothing parameter, much like
random slopes share one variancem = 1 changes the penalty so that each
speaker’s smooth is pulled toward a flat line, as a random effect should
be (Sóskuthy, 2017)k applies to each speaker: 20 speakers
with k = 5 add 100 coefficients. Keep k low;
too many coefficients for the data stops the model with an errorlmer()One way is a random smooth for every speaker and vowel combination,
s(time, speaker_vowel, bs = "fs", m = 1). Sóskuthy (2021)
compares this and other options in simulations.
Did you know: isolated fruit bat pups that heard
recordings of adult bats developed a repertoire that copied the playbacks.
Source: Prat, Taub & Yovel (2015), Science Advances
Random smooths by speaker handle dependencies within speakers. For the dependencies within each trajectory, there are two options:
s(time, traj, bs = "fs", m = 1), give
each of the 200 tokens its own smoothrhom_noar <- bam(f2 ~ l1_o + s(time, k = 10) + s(time, by = l1_o, k = 10) +
s(time, speaker, bs = "fs", m = 1, k = 5),
data = price, method = "ML")
rho <- itsadug::start_value_rho(m_noar)
m_ar <- bam(f2 ~ l1_o + s(time, k = 10) + s(time, by = l1_o, k = 10) +
s(time, speaker, bs = "fs", m = 1, k = 5),
data = price, method = "ML", AR.start = price$time == 0, rho = rho)start_value_rho() estimates
rho from the lag-1 autocorrelation of a model without the
AR1 componentAR.start marks the first measurement
of each trajectory, so the error model does not link one token to the
next. The data must be sorted by trajectory and timeFor the AR1 model, the autocorrelation is computed on the standardised residuals, m_ar$std.rsd,
which take the error model into account.
rho is shared by all trajectories, and
in {mgcv} the AR1 option is available in bam()
onlyDid you know: fruit bat pups raised in three groups,
each hearing a different playback, developed distinct dialects from about six months of age, each closer to
its own playback.
Source: Prat, Azoulay, Dor & Yovel (2017), PLoS Biology
ggplot(price, aes(x = time, y = f2, group = traj, colour = duration)) +
geom_line(alpha = 0.6) +
scale_colour_viridis_c(option = "magma", begin = 0.2) +
facet_wrap(~ l1) +
labs(x = "Time (%)", y = "F2 (Hz)", colour = "Duration (ms)") +
theme(legend.position = "right")Short vowels (dark) stop lower: the glide undershoots its target. Duration changes the shape of the curve as well as its height.
s(), te() and ti()s(duration) adds a smooth effect of
duration on overall F2: it can shift whole curves, but not change their
shapete(time, duration) is a tensor product smooth: one surface over time and
duration, including both main effectsti(time, duration) is the tensor product interaction alone. Combined with
s(time) and s(duration), it separates the main
effects from the interaction, so each gets its own row in the
summarym_dur <- bam(f2 ~ l1_o + s(time, k = 10) + s(time, by = l1_o, k = 10) +
s(duration) + ti(time, duration) +
s(time, speaker, bs = "fs", m = 1, k = 5),
data = price, method = "ML", AR.start = price$time == 0, rho = rho)
round(summary(m_dur)$s.table, 2) edf Ref.df F p-value
s(time) 8.88 8.97 190.31 0
s(time):l1_oItalian 8.67 8.91 36.40 0
s(duration) 1.00 1.00 304.45 0
ti(time,duration) 4.51 5.17 83.45 0
s(time,speaker) 78.30 98.00 10.41 0
predict_gam(m_dur, series = "time", length_out = 50,
values = list(duration = c(150, 220, 290)),
exclude_terms = "s(time,speaker)") |>
ggplot(aes(x = time, y = f2, colour = factor(duration), fill = factor(duration))) +
geom_ribbon(aes(ymin = lower_ci, ymax = upper_ci), alpha = 0.2, colour = NA) +
geom_line(linewidth = 1.2) +
facet_wrap(~ l1_o) +
labs(x = "Time (%)", y = "Predicted F2 (Hz)", colour = "Duration (ms)", fill = "Duration (ms)") +
theme(legend.position = "right")exclude_terms removes the random smooths, so the curves
are for an average speaker.
predict_gam(m_dur, series = c("time", "duration"), length_out = 40,
values = list(l1_o = "English"), exclude_terms = "s(time,speaker)") |>
ggplot(aes(x = time, y = duration, fill = f2, z = f2)) +
geom_raster() +
geom_contour(colour = floor_col, alpha = 0.6, bins = 8) +
scale_fill_viridis_c(option = "mako", begin = 0.15) +
labs(x = "Time (%)", y = "Duration (ms)", fill = "Predicted\nF2 (Hz)") +
theme(legend.position = "right")A heat map shows the whole interaction for English speakers, but prediction curves at a few durations are easier to read.
Did you know: the pups in the dialect experiment heard
their mothers too, but their calls moved toward the playback of the
whole colony. Prat and colleagues called this crowd
vocal learning.
Source: Prat, Azoulay, Dor & Yovel (2017), PLoS Biology
k' edf k-index p-value
s(time) 9 8.880414 1.0565009 0.9975
s(time):l1_oItalian 9 8.669315 1.0565009 0.9950
s(duration) 9 1.001933 0.2914938 0.0000
ti(time,duration) 16 4.514466 1.0488305 0.9975
s(time,speaker) 100 78.296626 1.0565009 0.9925
p_resid <- ggplot(tibble(fitted = fitted(m_dur), res = m_dur$std.rsd), aes(fitted, res)) +
geom_point(colour = kingfisher, alpha = 0.2) + labs(x = "Fitted F2 (Hz)", y = "Std. residual")
p_qq <- ggplot(tibble(res = m_dur$std.rsd), aes(sample = res)) +
geom_qq(colour = kingfisher, alpha = 0.4) + geom_qq_line(colour = mist) + labs(x = "Theoretical", y = "Sample")
p_resid + p_qqm_dur_null <- bam(f2 ~ s(time, k = 10) + s(duration) + ti(time, duration) +
s(time, speaker, bs = "fs", m = 1, k = 5),
data = price, method = "ML", AR.start = price$time == 0, rho = rho)
itsadug::compareML(m_dur_null, m_dur)m_dur_null: f2 ~ s(time, k = 10) + s(duration) + ti(time, duration) + s(time,
speaker, bs = "fs", m = 1, k = 5)
m_dur: f2 ~ l1_o + s(time, k = 10) + s(time, by = l1_o, k = 10) + s(duration) +
ti(time, duration) + s(time, speaker, bs = "fs", m = 1, k = 5)
Chi-square test of ML scores
-----
Model Score Edf Difference Df p.value Sig.
1 m_dur_null 11087.91 10
2 m_dur 11035.71 13 52.200 3.000 < 2e-16 ***
AIC difference: 18.90, model m_dur has lower AIC.
Both models have the same random effects, error model and
rho, and are fitted with method = "ML".
compare <- list(l1_o = c("Italian", "English"))
bind_rows(`GAM (lecture 3)` = as_tibble(get_difference(m_gam, "time", compare, length_out = 50)),
`GAMM with AR1` = as_tibble(get_difference(m_dur, "time", compare, length_out = 50,
exclude_terms = "s(time,speaker)")),
.id = "model") |>
ggplot(aes(x = time, y = diff, colour = model, fill = model)) +
geom_hline(yintercept = 0, colour = muted, linetype = "dashed") +
geom_ribbon(aes(ymin = lower_ci, ymax = upper_ci), alpha = 0.25, colour = NA) +
geom_line(linewidth = 1.1) +
labs(x = "Time (%)", y = "Italian − English F2 (Hz)", colour = NULL, fill = NULL) +
theme(legend.position = "right")The GAM treats the 2,200 measurements as independent; the GAMM knows they come from 200 tokens and 20 speakers.
We fitted a GAMM to F2 (
bam(),{mgcv}; Wood, 2017) with an ordered factor for L1 (reference: English), a reference smooth over time, a difference smooth for Italian speakers, a smooth for duration and a tensor product interaction between time and duration. Random smooths over time by speaker (m = 1,k = 5) and an AR1 error model (ρ = 0.8) accounted for dependencies within speakers and trajectories.
Then report the model comparison, and show the prediction curves and the difference smooth with their confidence intervals.
{mgcv}, random intercepts and
slopes use bs = "re", and random
smooths use bs = "fs" with m = 1ti() adds a non-linear interaction between two continuous
predictors; show it with prediction curves at a few valuesThese slides
mcanzi.de/ss27_advanced_statistics/
Code and all lectures
github.com/mcanzi/ss27_advanced_statistics