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
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.
{mgcv}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)# 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.
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.
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
The line misses the shape, and the residuals follow a pattern: neighbouring residuals are similar. This is autocorrelation, and it makes standard errors unreliable.
Nothing in the data says which degree to choose.
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
A generalised additive model can contain smooth terms next to the usual parametric terms:
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.
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.
k sets the maximum wiggliness: how
many basis functions the smooth may use{mgcv} estimates the smoothing
parameter from the data, so the strategy is to set k high
enough and let the penalty do the restThe same 10 basis functions with four smoothing parameters. A large penalty pushes the smooth toward a straight line.
{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 curvess(time) is a smooth over time;
s(time, by = l1) fits a separate smooth for each groupby model are centred on
zero, so the group difference in height needs its own parametric term,
l1method = "ML" is needed to compare
models that differ in their fixed effects; the default of
bam() is "fREML"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
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
lm(): the intercept is the mean F2 of the English
trajectories, and l1Italian is how much higher or lower the
Italian trajectories are on averageedf close to 9 means a very wiggly curve,
close to 1 a straight lineThe 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.
{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.
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.
k high enough? 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 usedTo be sure, raise k (for example to 15) and refit. If
the curves and the edf barely change, k was high
enough.
kedf 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.
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
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.
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
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_oItalianm1 and m2 fit the same
curves in two parametrisationsm2_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.
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".
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")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 shapeSóskuthy’s (2018) recommendations:
He calls the binary smooth probably ideal when, as here, a single difference between two curves is the question.
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.
k sets the maximum wiggliness and
the smoothing penalty the actual wigglinessby gives a reference smooth and a
difference smooth; plot the difference with its
confidence intervalThese slides
mcanzi.de/ss27_advanced_statistics/
Code and all lectures
github.com/mcanzi/ss27_advanced_statistics