Chapter 11
Unsupervised models
Lectures 21–23 · latent factors behind questionnaires, mixtures behind measurements, the posteriors that will not agree with themselves, and copulas that separate dependence from the margins
Every chapter so far has had an outcome: something the model was asked to predict or explain. This chapter drops it. We are left with a table of measurements and no label, and the question is whether the table itself contains structure: a few underlying dimensions along which people or objects vary, or a few groups into which they fall. Both kinds of structure are latent, never observed directly, and both are inferred by the same Bayesian machinery as before: write down a model in which the latent structure generates the data, and compute the posterior.
- Lecture 21 is about latent factors. A personality questionnaire asks 25 questions; psychologists claim the answers reflect five underlying traits. We build principal components, probabilistic PCA and the factor model, fit a Bayesian factor model in Stan, meet the rotation problem, and ask how many factors the data actually support.
- Lecture 22 is about mixtures. We hide the species labels of the Palmer penguins from Chapter 1 and ask whether four body measurements can find the species again. Along the way we meet label switching, a posterior that will not agree with itself, soft assignments, and the Dirichlet process, and then we look for breast-cancer subtypes in expression data.
- Lecture 23 is about copulas. Two genes can be strongly correlated in very different ways. We separate a joint distribution into its margins and its dependence with Sklar's theorem, meet the main copula families and their tails, fit them in Stan to co-amplified genes, use the Gaussian copula for questionnaires that mix ordinal, binary and continuous answers, and check whether home and away goals in football are independent.
Think About It: Before You Start
Suppose a clustering algorithm splits a set of patients into three groups. What would convince you that the groups are real, rather than a partition the algorithm would have produced from any data? And if a patient sits halfway between two groups, should the algorithm say so, or assign her to one of them? Keep your answers in mind; Lecture 22 is built around them.
Lecture 21 — Latent factors
11.1 Twenty-five questions
The Synthetic Aperture Personality Assessment project (SAPA) collected answers to personality items from thousands of volunteers online. William Revelle distributes 2,800 of them as the bfi data set (Revelle, 2024): 25 statements, five for each of the "Big Five" personality traits (Agreeableness, Conscientiousness, Extraversion, Neuroticism and Openness), each rated on a six-point scale from "very inaccurate" to "very accurate" as a description of oneself. "I am indifferent to the feelings of others" (A1), "I am exacting in my work" (C1), "I make friends easily" (E4), "I get angry easily" (N1), "I am full of ideas" (O1). Some statements are worded in reverse, so that agreeing indicates less of the trait; we reverse their scores so that every item points the same way.
The questions: do 25 answers really record 25 different things, or a handful of underlying dimensions? If a handful, how many, and what are they? Psychology's answer, the five-factor model, was itself found largely by factor analysis of questionnaires like this one. Here we ask whether a Bayesian factor model applied to these data agrees.
library(tidyverse)
library(rstan)
library(loo)
options(mc.cores = parallel::detectCores())
harbour <- c(ink = "#10191d", muted = "#4a5f68", grid = "#d5e3e6",
sky = "#1d5187", amber = "#805907", rose = "#ad3b28",
moss = "#1d6b54", lilac = "#5b3a94")
theme_harbour <- function(base_size = 11) {
theme_minimal(base_size = base_size) +
theme(text = element_text(colour = harbour[["ink"]]),
axis.text = element_text(colour = harbour[["muted"]]),
panel.grid.major = element_line(colour = harbour[["grid"]]),
panel.grid.minor = element_blank(),
strip.text = element_text(colour = harbour[["ink"]], hjust = 0),
legend.position = "top")
}
theme_set(theme_harbour())
tmp <- tempfile()
download.file("https://raw.githubusercontent.com/cran/psychTools/master/data/bfi.rda", tmp, quiet = TRUE)
load(tmp) # creates `bfi`
reversed <- c("A1", "C4", "C5", "E1", "E2", "O2", "O5") # negatively worded items
items <- bfi |> select(A1:O5) |> drop_na() |> mutate(across(all_of(reversed), ~ 7 - .x))
Y <- scale(as.matrix(items)) # standardised: correlations, not raw scores
c(people = nrow(Y), items = ncol(Y))
round(cor(items)[c("N1", "N2", "E1", "E4", "C2"), c("N1", "N2", "E1", "E4", "C2")], 2)
people items
2436 25
N1 N2 E1 E4 C2
N1 1.00 0.72 -0.01 -0.14 -0.02
N2 0.72 1.00 -0.02 -0.15 -0.01
E1 -0.01 -0.02 1.00 0.42 -0.02
E4 -0.14 -0.15 0.42 1.00 0.12
C2 -0.02 -0.01 -0.02 0.12 1.00
After dropping people with any missing answer, 2,436 of the 2,800 remain. The correlations show the pattern factor analysis looks for. Two Neuroticism items correlate 0.72 with each other and two Extraversion items 0.42, while items written for different traits correlate between −0.15 and 0.12. Items written for the same trait hang together; items written for different traits mostly do not.
11.2 Latent variables as causes
The factor model makes a causal claim, and it is worth stating it before fitting anything. A person has a level of neuroticism, which we cannot observe. That level causes her answers to "I get angry easily", "I get irritated easily" and "I panic easily". The five Neuroticism answers are correlated because they share a cause, and once the cause is known they are independent: this is local independence, the fork of Chapter 1 with an unobserved common cause.
This direction is an assumption, not a finding. Psychometricians call it a reflective model: the items reflect the trait. The alternative is a formative model, in which the items cause the construct: socioeconomic status is made up of income, education and occupation, and a rise in income raises status without any common cause behind it. A factor model fitted to formative indicators will happily find a "factor", because correlated variables always have a first principal axis, but that factor is a summary, not a cause, and interventions on it make no sense. A second warning applies to both: the latent variable is defined by the model. If we had asked different questions, we would have found different factors. The Big Five are a description of what people say about themselves in English-language questionnaires, and whether they are also causal entities in the brain is a question no factor analysis can settle (Borsboom, Mellenbergh and van Heerden, 2003).
Check Yourself
Correlation alone does not tell you which way the arrows run.
Factor models can be useful even when they are not causal, but the interpretation changes.
Correlated indicators are compatible with a common cause, with indicators that cause each other (chronic disease causes slow walking), and with an index that is merely their sum. The factor model assumes the first. Both a frailty "phenotype" (Fried et al., 2001) and a "deficit index" (Mitnitski, Mogilner and Rockwood, 2001) are in use. Reading the first as reflective and the second as formative is our gloss rather than a settled classification, but it shows what each one assumes.
11.3 From principal components to factors
Principal component analysis (PCA) finds the directions in the 25-dimensional space of answers along which people vary most. Mathematically, these are the eigenvectors of the correlation matrix, and each eigenvalue is the variance along its direction. No probability model is involved. If the five-trait story is right, we expect five eigenvalues to stand out and the other twenty to be small and similar.
ev <- eigen(cor(Y))$values
tibble(component = 1:10, eigenvalue = round(ev[1:10], 2),
cumulative_share = round(cumsum(ev)[1:10] / sum(ev), 3))
# A tibble: 10 x 3
component eigenvalue cumulative_share
<int> <dbl> <dbl>
1 1 5.13 0.205
2 2 2.75 0.315
3 3 2.14 0.401
4 4 1.85 0.475
5 5 1.55 0.537
6 6 1.07 0.58
7 7 0.84 0.614
8 8 0.8 0.646
9 9 0.72 0.674
10 10 0.69 0.702
Five eigenvalues stand clearly above 1 (5.13, 2.75, 2.14, 1.85 and 1.55), the sixth is 1.07, and from the seventh on they decline slowly from 0.84. Together the first five components account for 54% of the variance in the 25 items. That supports a handful of dimensions, and five is a reasonable reading, though the cut-off is not sharp: the classical rule of keeping eigenvalues above 1 would say six. Notice too that the first eigenvalue is much larger than the rest; the rotation widget below shows why.
Probabilistic PCA (Tipping and Bishop, 1999) turns this into a generative model. Each person has a vector of latent scores , and her standardised answers are a noisy linear function of them:
The matrix holds the loadings: says how strongly factor drives item . In probabilistic PCA the noise has the same variance for every item, ; in the factor analysis model each item has its own unique variance , the part of the answer that the factors do not explain. Integrating out the unobserved gives the distribution of the data:
All the information about the factors is in the covariance matrix: the model says that 300 correlations between 25 items are generated by loadings and 25 unique variances. It also says something uncomfortable. If is any rotation matrix (), then : rotated loadings fit the data exactly as well. The likelihood cannot tell a factor model from any rotation of itself. The widget shows this with two factors.
At 0° the plot shows the solution as fitted, with the loadings constrained to lower-triangular form and E3 and N1 as anchors (Section 11.4 explains the constraint). Because the anchors are strong items of two different traits, this solution is already close to simple structure, and the varimax button moves it by only about a degree: the Neuroticism items lie along factor 2, and the Extraversion and Agreeableness items, with the weaker Conscientiousness and Openness items, along factor 1. That so many items load positively on one direction is why the first eigenvalue is so large. Now rotate by 45°: every item loads on both factors and the picture is hard to read, yet the readout shows that nothing about the fit has changed. Rotation is a choice made for interpretation, and the varimax criterion (Kaiser, 1958), which maximises the variance of the squared loadings within each factor, is one convention for making it.
11.4 A Bayesian factor model
For a Bayesian fit, the rotation problem is more than cosmetic. If the likelihood is unchanged along a continuum of rotations, so is the posterior (with rotation-invariant priors), and the sampler wanders around a ridge of equally good solutions; the chains never agree and the loadings have no meaningful posterior mean. The standard fix (Geweke and Zhou, 1996) is to choose one representative from each set of rotations: constrain to be lower triangular with a positive diagonal. Item 1 loads only on factor 1, item 2 only on factors 1 and 2, and so on up to item ; from item on, all loadings are free. Any can be rotated into this form, so the constraint restricts the parameterisation, not the fit. The first items are called anchors.
Because the model depends on the data only through the sample covariance matrix , the likelihood can be computed from in one step rather than person by person: for people, plus a constant, with . That makes the model fast even for thousands of respondents. We still compute each person's log-likelihood in generated quantities, for leave-one-out comparison.
fa_code <- "
data { int<lower=1> N; int<lower=1> P; int<lower=1> K; matrix[N, P] Y; }
transformed data { matrix[P, P] S = crossprod(Y) / N; } // sample covariance (Y is centred)
parameters {
vector<lower=0>[K] ldiag; // positive diagonal of the loadings
vector[K * (K - 1) / 2 + (P - K) * K] lfree; // loadings below the diagonal
vector<lower=0>[P] psi; // unique standard deviations
}
transformed parameters {
matrix[P, K] L = rep_matrix(0, P, K); // lower triangular: identifies the rotation
{ int idx = 1;
for (j in 1:K) { L[j, j] = ldiag[j];
for (i in (j + 1):P) { L[i, j] = lfree[idx]; idx += 1; } } }
}
model {
matrix[P, P] Sigma = add_diag(tcrossprod(L), square(psi));
ldiag ~ normal(0, 1); lfree ~ normal(0, 1); psi ~ exponential(1);
target += -0.5 * N * (log_determinant(Sigma) + trace(mdivide_left_spd(Sigma, S)));
}
generated quantities {
vector[N] log_lik;
{ matrix[P, P] Lc = cholesky_decompose(add_diag(tcrossprod(L), square(psi)));
for (n in 1:N) log_lik[n] = multi_normal_cholesky_lpdf(Y[n]' | rep_vector(0, P), Lc); }
}"
fa_mod <- stan_model(model_code = fa_code)
Which items should be the anchors? This matters more than the theory suggests. The constraint forces anchor to load positively on factor , so an anchor that barely relates to the factor we hope it defines leaves the posterior with two nearly equal modes, one for each sign, and chains can settle in different ones. In a first attempt (before reverse-scoring the negatively worded items, and with anchors chosen by hand), one chain found a solution in which the "Agreeableness" anchor had a loading of 0.01 while the other found 0.41, and reached 37. The cure is to choose, for each factor, the item that loads most strongly on it in a quick classical solution (maximum-likelihood factor analysis with factanal()), and to start every chain at that classical solution rotated into lower-triangular form.
fit_fa <- function(K) {
path <- sprintf("fits/ch11_fa%d.rds", K); if (file.exists(path)) return(readRDS(path))
ml <- factanal(Y, K, rotation = if (K > 1) "varimax" else "none") # classical solution
Lml <- unclass(ml$loadings); anchors <- integer(0)
for (j in 1:K) { a <- abs(Lml[, j]); a[anchors] <- -1; anchors <- c(anchors, which.max(a)) }
ord <- c(anchors, setdiff(seq_len(ncol(Y)), anchors)) # anchors first
Lm <- Lml[ord, , drop = FALSE]
Q <- qr.Q(qr(t(Lm[1:K, , drop = FALSE]))) # rotate to lower-triangular form
Li <- Lm %*% Q; Li <- sweep(Li, 2, sign(diag(Li[1:K, , drop = FALSE])), "*")
init <- function() list(ldiag = as.array(diag(Li[1:K, , drop = FALSE])),
lfree = as.array(unlist(lapply(1:K, function(j) Li[(j + 1):ncol(Y), j]))),
psi = sqrt(ml$uniquenesses[ord]))
fit <- sampling(fa_mod, data = list(N = nrow(Y), P = ncol(Y), K = K, Y = Y[, ord]),
chains = 4, iter = 1000, seed = 11, refresh = 0, init = init)
out <- list(fit = fit, items = colnames(Y)[ord]); saveRDS(out, path); out
}
fa5 <- fit_fa(5)
fa5$items[1:5] # the anchors
s5 <- summary(fa5$fit, pars = c("ldiag", "lfree", "psi"))$summary
c(max_rhat = round(max(s5[, "Rhat"]), 3), min_ess = round(min(s5[, "n_eff"])),
divergences = get_num_divergent(fa5$fit))
[1] "N1" "E2" "C4" "A3" "O3"
max_rhat min_ess divergences
1.01 953.00 0.00
The anchors chosen from the classical solution are N1 ("Get angry easily"), E2 ("Find it difficult to approach others", reverse-scored), C4 ("Do things in a half-way manner", reverse-scored), A3 ("Know how to comfort others") and O3 ("Carry the conversation to a higher level"). Started from the classical solution, the four chains agree: the largest is 1.01, the smallest effective sample size is 953, and there are no divergent transitions. The fit takes under a minute.
The lower-triangular loadings are identified but not interpretable: they depend on which items we chose as anchors. To read the factors, we rotate. We take the varimax rotation of the posterior mean loadings as a reference, and then rotate every posterior draw to match the reference as closely as possible (an orthogonal Procrustes rotation, one singular value decomposition per draw). Because rotation does not change the fit, every rotated draw is an equally valid draw from the posterior; we have only chosen how to look at it.
rotate_draws <- function(res) {
L <- rstan::extract(res$fit, "L")$L # draws x items x factors
ref <- unclass(varimax(apply(L, c(2, 3), mean))$loadings)
key <- substr(res$items[apply(abs(ref), 2, which.max)], 1, 1) # trait letter of each factor's top item
ref <- sweep(ref, 2, sign(ref[cbind(apply(abs(ref), 2, which.max), seq_len(ncol(ref)))]), "*")
rot <- apply(L, 1, function(M) { sv <- svd(crossprod(M, ref)); M %*% sv$u %*% t(sv$v) })
rot <- array(t(rot), dim(L)); dimnames(rot) <- list(NULL, res$items, key); rot
}
lam <- rotate_draws(fa5)
lam_tab <- as.data.frame.table(apply(lam, c(2, 3), mean), responseName = "mean") |>
bind_cols(lo = as.vector(apply(lam, c(2, 3), quantile, 0.05)),
hi = as.vector(apply(lam, c(2, 3), quantile, 0.95))) |>
rename(item = Var1, factor = Var2) |> mutate(trait = substr(item, 1, 1))
lam_tab |> group_by(factor) |> slice_max(abs(mean), n = 3) |> mutate(across(c(mean, lo, hi), ~ round(.x, 2)))
# A tibble: 15 x 6
# Groups: factor [5]
item factor mean lo hi trait
<fct> <fct> <dbl> <dbl> <dbl> <chr>
1 N1 N 0.82 0.79 0.85 N
2 N2 N 0.79 0.76 0.82 N
3 N3 N 0.72 0.69 0.75 N
4 E2 E 0.68 0.64 0.71 E
5 E4 E 0.62 0.59 0.65 E
6 E1 E 0.59 0.55 0.63 E
7 C4 C 0.66 0.62 0.69 C
8 C2 C 0.63 0.59 0.66 C
9 C5 C 0.58 0.54 0.61 C
10 A3 A 0.66 0.63 0.7 A
11 A2 A 0.6 0.57 0.64 A
12 A5 A 0.58 0.55 0.61 A
13 O3 O 0.62 0.58 0.65 O
14 O1 O 0.53 0.49 0.56 O
15 O5 O 0.51 0.47 0.55 O
After rotation, each factor is led by the items of one trait, so we can name the factors N, E, C, A and O. The strongest loadings are those of N1 and N2 on Neuroticism (0.82 and 0.79), E2 on Extraversion (0.68), C4 on Conscientiousness (0.66), A3 on Agreeableness (0.66) and O3 on Openness (0.62). The 90% intervals are narrow, about ±0.03: with 2,436 people, the loadings are well determined once the rotation is fixed. The names are ours, not the model's. The model found five directions, and we named them after the items that load on them.
lam_tab |> mutate(item = fct_rev(factor(item)), big = abs(mean) > 0.3) |>
ggplot(aes(y = item)) +
geom_vline(xintercept = 0, colour = harbour[["grid"]]) +
geom_errorbarh(aes(xmin = lo, xmax = hi, colour = big), height = 0) +
geom_point(aes(x = mean, colour = big), size = 1.3) +
facet_grid(trait ~ factor, scales = "free_y", space = "free_y",
labeller = labeller(factor = function(x) paste("factor", x))) +
scale_colour_manual(values = c(`FALSE` = harbour[["muted"]], `TRUE` = harbour[["sky"]]), guide = "none") +
scale_x_continuous(breaks = c(0, 0.4, 0.8)) + labs(x = "loading", y = NULL)
Every item loads most strongly on the factor of its own trait, so the five-factor structure is recovered. But it is not perfectly simple. The largest cross-loadings link Extraversion and Agreeableness: E4 ("Make friends easily") loads 0.36 on Agreeableness, and A5 ("Make people feel at ease") 0.36 on Extraversion. N4 ("Often feel blue") loads −0.37 on Extraversion, so it measures low mood as much as emotional reactivity. O4 ("Spend time reflecting on things") is the weakest Openness item (0.37), with small loadings on Neuroticism (0.21) and, negatively, Extraversion (−0.22). Departures like these are what a sixth or seventh factor can absorb.
Check Yourself
An item has standardised loadings 0.6 on one factor and 0 on all others, and the factors are uncorrelated with variance 1. What share of the item's variance do the factors explain (its communality)?
The item's variance is for a standardised item.
The communality is , so 36% of the variance in answers to this item is shared with the other items through the factor, and the unique variance is the rest: specific content of the question plus measurement error. Loadings of 0.5 to 0.7 are typical of personality items, which is why single questions are poor measures of a trait and questionnaires use several.
11.5 How many factors?
Five factors was the theory's choice. The data can be asked, too: fit models with and compare their expected log predictive density by PSIS-LOO, which here measures how well each model predicts a new person's 25 answers.
fits_k <- map(1:8, fit_fa)
loos_k <- map(fits_k, ~ loo(extract_log_lik(.x$fit)))
cmp <- map_dfr(1:8, function(k) {
d <- loos_k[[k]]$pointwise[, "elpd_loo"] - loos_k[[5]]$pointwise[, "elpd_loo"]
tibble(K = k, diff = sum(d), se = sqrt(length(d)) * sd(d),
max_rhat = max(summary(fits_k[[k]]$fit, pars = c("ldiag", "psi"))$summary[, "Rhat"]))
})
ggplot(cmp, aes(K, diff)) + geom_hline(yintercept = 0, colour = harbour[["muted"]], linetype = 2) +
geom_pointrange(aes(ymin = diff - 2 * se, ymax = diff + 2 * se), colour = harbour[["sky"]], size = 0.3) +
geom_line(colour = harbour[["sky"]]) + scale_x_continuous(breaks = 1:8) +
labs(x = "number of factors K", y = "elpd difference from K = 5")
cmp |> mutate(across(c(diff, se), ~ round(.x, 1)), max_rhat = round(max_rhat, 3))
L2 <- apply(rstan::extract(fits_k[[2]]$fit, "L")$L, c(2, 3), mean); items2 <- fits_k[[2]]$items
rm(fits_k, loos_k); invisible(gc()) # free memory before compiling the next model
# A tibble: 8 x 4
K diff se max_rhat
<int> <dbl> <dbl> <dbl>
1 1 -4486 138. 1
2 2 -2482. 102 1
3 3 -1461. 75.2 1.00
4 4 -728. 57 1.00
5 5 0 0 1.00
6 6 277. 32.4 1.00
7 7 395. 39 1.00
8 8 463. 42.4 1.01
LOO does not stop at five. Each extra factor up to five improves the expected log predictive density by hundreds or thousands of units, and beyond five the gains shrink but stay much larger than their standard errors: +277 for six factors, +395 for seven and +463 for eight, with standard errors from 32 to 42. All eight fits mixed well (largest 1.01). With 2,436 people, small departures from the five-factor model become detectable: pairs of items with similar wording, items that belong partly to two traits, and six-point answers that are not normally distributed. A sixth or seventh factor may describe these better without being a new personality trait. LOO answers the question "which model predicts a new person's answers best?", and its answer is "more than five factors". Whether the extra factors are psychology or artefact is a substantive question, and Exercise 1 looks at what five factors miss.
History: From g to the Big Five
Factor analysis began with Charles Spearman (1904), who noticed that schoolchildren's marks in Classics, French, English and mathematics were all positively correlated, and proposed a single "general intelligence", g, as their common cause. Louis Thurstone (1935, 1947) generalised the method to several factors, invented the idea of rotation to "simple structure", and argued against Spearman that intelligence was several primary abilities; the dispute, a disagreement about rotation as much as about minds, ran for decades. Gordon Allport and Henry Odbert (1936) extracted about 18,000 personality-describing words from an English dictionary, on the "lexical hypothesis" that important differences between people become words. Factor analyses of ratings using such words kept producing five broad factors (Tupes and Christal in 1961, Norman in 1963, Goldberg in the 1980s), and by the 1990s the "Big Five" had become psychology's standard description of personality. Probabilistic PCA (Tipping and Bishop, 1999) and Bayesian PCA (Bishop, 1999) brought the same models into machine learning, where they are the simplest members of a family that runs through variational autoencoders.
Key Takeaways: Lecture 21
- A factor model says that correlated observations share unobserved causes: , with local independence given . The causal direction is an assumption; for formative indices it is wrong.
- Loadings are identified only up to rotation. A Bayesian fit needs a constraint, such as lower-triangular loadings with positive diagonal, and the choice of anchor items matters: weak anchors create competing modes.
- Start the chains from a classical solution, check , then rotate every draw (varimax plus Procrustes alignment) for interpretation.
- Predictive comparisons with large samples keep rewarding extra factors; the number to report is a substantive decision informed, not dictated, by LOO.
Lecture 22 — Mixtures
11.6 Penguins without labels
In Chapter 1 we used the Palmer penguins (Gorman, Williams and Fraser, 2014; Horst, Hill and Gorman, 2020) to predict body mass. Now we hide the one variable a biologist would never be without, the species, and ask whether four measurements (bill length, bill depth, flipper length and body mass) can find it again. The question is a fair test of clustering, because we know the answer: 151 Adélie, 68 Chinstrap and 123 Gentoo penguins, measured on three islands near Palmer Station, Antarctica, between 2007 and 2009.
library(palmerpenguins)
birds <- penguins |> drop_na(bill_length_mm, bill_depth_mm, flipper_length_mm, body_mass_g)
Yp <- scale(as.matrix(select(birds, bill_length_mm, bill_depth_mm, flipper_length_mm, body_mass_g)))
colnames(Yp) <- c("bill_length", "bill_depth", "flipper", "mass")
c(penguins = nrow(Yp), measurements = ncol(Yp))
penguins measurements
342 4
A mixture model says that each penguin belongs to one of unobserved groups, , with probabilities , and that its measurements are drawn from a distribution specific to its group:
The group labels are discrete, and Hamiltonian Monte Carlo cannot sample discrete parameters. As with the missing data of Chapter 7, the remedy is to sum them out: the likelihood of one penguin is
which depends only on continuous parameters. Each penguin's group is then recovered after fitting by Bayes' rule, draw by draw:
Averaging over the posterior gives each penguin a soft assignment: a probability for each group, not a single label. Before HMC, mixtures were fitted by two older algorithms that are still worth knowing. The EM algorithm (Dempster, Laird and Rubin, 1977) alternates between computing these membership probabilities (the E step) and re-estimating each group's weight, mean and variance from the penguins weighted by their memberships (the M step); it climbs to a local maximum of the likelihood. Gibbs sampling (Diebolt and Robert, 1994) instead draws each from its membership probabilities and then draws the parameters given the labels, and produces a sample from the posterior. The widget runs both on flipper length alone.
With two components, both algorithms quickly find a small-flipper group (Adélie and Chinstrap) and a large-flipper group (Gentoo). Try a few new starts with three components: EM sometimes stops at a poor solution, for instance splitting the Gentoo into two and leaving Adélie and Chinstrap merged, because it only ever climbs. That is a local mode, and the Bayesian posterior has it too, as the next section shows.
11.7 Chains that disagree
We write the mixture in Stan with full covariance matrices for each group (an LKJ prior on each correlation matrix, as in Chapter 6), a Dirichlet(2) prior on the weights, and the per-penguin membership probabilities computed in generated quantities. Each chain starts from a k-means solution.
mix_code <- "
data { int<lower=1> N; int<lower=1> D; int<lower=1> K; array[N] vector[D] y; }
parameters {
simplex[K] w; // group weights
array[K] vector[D] mu; // group means
array[K] cholesky_factor_corr[D] Omega; // group correlation matrices
array[K] vector<lower=0>[D] sigma; // group standard deviations
}
model {
array[K] matrix[D, D] Lk;
w ~ dirichlet(rep_vector(2, K));
for (k in 1:K) {
mu[k] ~ normal(0, 1.5); sigma[k] ~ exponential(2); Omega[k] ~ lkj_corr_cholesky(2);
Lk[k] = diag_pre_multiply(sigma[k], Omega[k]);
}
for (n in 1:N) { // the group label is summed out
vector[K] lp;
for (k in 1:K) lp[k] = log(w[k]) + multi_normal_cholesky_lpdf(y[n] | mu[k], Lk[k]);
target += log_sum_exp(lp);
}
}
generated quantities {
vector[N] log_lik; array[N] vector[K] prob; // membership probabilities by Bayes' rule
{ array[K] matrix[D, D] Lk;
for (k in 1:K) Lk[k] = diag_pre_multiply(sigma[k], Omega[k]);
for (n in 1:N) { vector[K] lp;
for (k in 1:K) lp[k] = log(w[k]) + multi_normal_cholesky_lpdf(y[n] | mu[k], Lk[k]);
log_lik[n] = log_sum_exp(lp); prob[n] = softmax(lp); } }
}"
mix_path <- "fits/ch11_mix_model.rds" # cache the compiled model between sessions
if (file.exists(mix_path)) mix_mod <- readRDS(mix_path) else { mix_mod <- stan_model(model_code = mix_code); saveRDS(mix_mod, mix_path) }
fit_mix <- function(Y, K, tag) {
path <- sprintf("fits/ch11_%s%d.rds", tag, K); if (file.exists(path)) return(readRDS(path))
set.seed(11); km <- kmeans(Y, K, nstart = 50) # starting values from k-means
init <- function() list(w = as.array(as.numeric(table(km$cluster)) / nrow(Y)),
mu = lapply(1:K, function(k) as.array(km$centers[k, ])),
sigma = lapply(1:K, function(k) as.array(pmax(apply(Y[km$cluster == k, , drop = FALSE], 2, sd), 0.1))),
Omega = lapply(1:K, function(k) diag(ncol(Y))))
fit <- sampling(mix_mod, data = list(N = nrow(Y), D = ncol(Y), K = K, y = lapply(seq_len(nrow(Y)), function(i) Y[i, ])),
chains = 4, iter = 1000, seed = 11, refresh = 0, init = init)
saveRDS(fit, path); fit
}
m3 <- fit_mix(Yp, 3, "penguin")
c(max_rhat = round(max(summary(m3, pars = c("w", "mu"))$summary[, "Rhat"]), 2), divergences = get_num_divergent(m3))
max_rhat divergences
23.48 0.00
The largest is 23.5, although there are no divergent transitions. By every rule used in this course, these chains have not converged.
The diagnostics say the four chains disagree. Before trying to fix anything, look at what each chain found: its average log posterior density, its group weights, and how the penguins it puts in each group line up with the species we hid.
per_chain <- function(fit, K) { # posterior-mean memberships, one N x K matrix per chain
arr <- as.array(fit); pn <- grep("^prob\\[", dimnames(arr)[[3]], value = TRUE)
map(seq_len(dim(arr)[2]), ~ matrix(colMeans(arr[, .x, pn]), ncol = K))
}
lp3 <- colMeans(as.array(m3, pars = "lp__")[, , 1]); P3 <- per_chain(m3, 3)
round(rbind(lp = lp3, sapply(1:4, function(ch) colMeans(as.array(m3, pars = "w")[, ch, ]))), 2)
for (chain in 1:4) { cat("chain", chain, "\n"); print(table(birds$species, group = max.col(P3[[chain]]))) }
chain:1 chain:2 chain:3 chain:4
lp -1210.43 -1210.78 -1210.59 -1210.94
w[1] 0.45 0.44 0.36 0.44
w[2] 0.20 0.36 0.45 0.20
w[3] 0.36 0.20 0.20 0.36
chain 1
group
1 2 3
Adelie 149 2 0
Chinstrap 4 64 0
Gentoo 0 0 123
chain 2
group
1 2 3
Adelie 149 0 2
Chinstrap 4 0 64
Gentoo 0 123 0
chain 3
group
1 2 3
Adelie 0 149 2
Chinstrap 0 4 64
Gentoo 123 0 0
chain 4
group
1 2 3
Adelie 149 2 0
Chinstrap 4 64 0
Gentoo 0 0 123
All four chains reached the same log posterior density (−1210.4 to −1210.9) and put the same penguins together: one group holds 149 Adélie and 4 Chinstrap, another 64 Chinstrap and 2 Adélie, and the third all 123 Gentoo. Only the numbering differs. Gentoo is group 3 in chains 1 and 4, group 2 in chain 2 and group 1 in chain 3. The chains agree completely about the penguins and disagree only about the names.
Two different problems are hiding in that output. The first is label switching. The likelihood is unchanged if we swap the names of two groups: nothing in the data says which group is "1". So a posterior with well-separated groups has identical copies of every mode, one for each labelling, and different chains can land in different copies. Averaging their draws then blends Gentoo with Chinstrap into nonsense, and rightly complains. This is not a problem with the fit, only with its bookkeeping. A common remedy is to constrain one coordinate of the means to be ordered (), but it works only if the groups are well separated in that coordinate; Chinstrap and Gentoo penguins have almost the same average bill length, so ordering by bill length would not have helped. The widget shows label switching within a single chain, which happens when groups overlap.
Relabelling fixes the first problem. We take the chain with the highest log posterior as the reference and, for each other chain, choose the permutation of its labels that best matches the reference's membership probabilities (Stephens, 2000, does this draw by draw; chain by chain suffices when labels do not switch within a chain, which the clean per-chain tables above indicate).
The second problem did not occur in this run, but it does in others, and it is not bookkeeping. In an earlier run with a different random seed, one of the four chains settled in a genuinely different solution, a local mode like the one EM gets stuck in: Adélie and Chinstrap merged into one group and a few birds in a small group of their own, with a log posterior density about 50 units below the others. No relabelling turns that into the same answer. Such a mode carries essentially no posterior probability, since is negligible, but a chain that falls into it cannot climb out. Our align_chains() function therefore keeps only the chains whose average log density is within 10 units of the best, and reports how many it kept. With a minor mode closer in log density, the principled alternative is to weight the chains by their predictive performance (stacking; Yao, Vehtari and Gelman, 2022). We will need the rule for four groups.
perms <- function(K) if (K == 1) list(1) else do.call("c", lapply(1:K, function(i) lapply(perms(K - 1), function(p) c(i, ifelse(p >= i, p + 1, p)))))
align_chains <- function(fit, K, gap = 10) {
lp <- colMeans(as.array(fit, pars = "lp__")[, , 1]); P <- per_chain(fit, K)
ref <- which.max(lp); keep <- which(lp > max(lp) - gap) # chains in the dominant mode
pm <- map(seq_along(P), function(ch) { pp <- perms(K); pp[[which.max(map_dbl(pp, ~ sum(P[[ref]] * P[[ch]][, .x])))]] })
prob <- Reduce(`+`, map(keep, ~ P[[.x]][, pm[[.x]]])) / length(keep)
ll <- as.array(fit, pars = "log_lik")[, keep, , drop = FALSE]
list(prob = prob, keep = keep, lp = lp, loo = suppressWarnings(loo(ll, r_eff = relative_eff(exp(ll)))))
}
a3 <- align_chains(m3, 3)
c(chains_kept = length(a3$keep))
table(birds$species, group = max.col(a3$prob))
c(certain_0.9 = sum(apply(a3$prob, 1, max) > 0.9), uncertain = sum(apply(a3$prob, 1, max) <= 0.9))
chains_kept
4
group
1 2 3
Adelie 149 2 0
Chinstrap 4 64 0
Gentoo 0 0 123
certain_0.9 uncertain
329 13
After relabelling, all four chains are kept and the pooled assignments recover the species almost exactly: 149 of 151 Adélie, 64 of 68 Chinstrap and all 123 Gentoo fall in groups of their own, so only 6 of the 342 penguins are grouped with another species, all of them between Adélie and Chinstrap. 329 penguins are assigned with probability above 0.9; 13 are borderline.
Research Practice: Reporting Latent-Variable Models
- State the identification strategy: anchors and constraints for a factor model, ordering or relabelling for a mixture. A reader cannot interpret loadings or groups without it.
- Report every chain, not just the ones that converged. If chains found different modes, say which, how far apart they were in log density, and what you did (relabelled, discarded with a stated rule, or stacked).
- Report how the number of factors or groups was chosen, with the comparison (LOO or held-out likelihood) and its standard errors, and say where substantive judgement overrode it.
- For mixtures, report soft assignments: how many cases are confidently assigned and how many are borderline. Downstream analyses that use group labels should carry this uncertainty, for example by repeating the analysis over posterior draws of the labels.
- When true labels exist for some cases, report agreement with them (a cross-table, or the adjusted Rand index), but do not let a good match be the only justification for the groups.
Check Yourself
R-hat is computed on labelled parameters, which label switching scrambles.
More iterations do not help a chain cross a barrier of 50 units of log density.
Label switching changes names, not assignments, so it shows up as chains that agree after a permutation. A genuine local mode puts the penguins in different groups and has a different log density. A gap of about 50 means the mode is about times less probable, but HMC cannot cross the low-probability valley between modes, so more iterations do not help.
11.8 Soft assignments and choosing K
The figure shows every penguin in the two measurements that separate the species best, coloured by its most probable group, with paler points for less certain assignments, and with the hidden species as the marker shape.
birds |> mutate(group = factor(max.col(a3$prob)), certainty = apply(a3$prob, 1, max)) |>
ggplot(aes(bill_length_mm, flipper_length_mm, colour = group, shape = species, alpha = certainty)) +
geom_point(size = 1.9) +
scale_colour_manual(values = unname(harbour[c("sky", "amber", "rose")])) +
scale_alpha_continuous(range = c(0.25, 1), limits = c(0.5, 1)) +
guides(alpha = "none") + theme(legend.position = "right") +
labs(x = "bill length (mm)", y = "flipper length (mm)", colour = "group", shape = "species")
The Gentoo group is unmistakable, with its long flippers (and, in the measurements not shown, shallower bills and greater weight). Adélie and Chinstrap differ mainly in bill length, and the pale points, the uncertain assignments, sit where their clouds meet.
unc <- order(apply(a3$prob, 1, max))[1:15]
as_tibble(a3$prob[unc, ], .name_repair = ~ paste("group", 1:3)) |>
mutate(bird = factor(paste0("#", unc, " (", birds$species[unc], ", ", birds$sex[unc], ")"), levels = rev(paste0("#", unc, " (", birds$species[unc], ", ", birds$sex[unc], ")")))) |>
pivot_longer(-bird, names_to = "group", values_to = "p") |>
ggplot(aes(p, bird, fill = group)) + geom_col(width = 0.75) +
scale_fill_manual(values = unname(harbour[c("sky", "amber", "rose")])) +
labs(x = "posterior probability", y = NULL, fill = NULL)
The borderline penguins are all on the Adélie–Chinstrap boundary: 11 of the 15 are female Chinstrap and 4 are male Adélie. Small Chinstrap females and large Adélie males overlap in every measurement, so much of the uncertainty is explained by sex, a variable the model never sees. A biologist would recognise these birds as hard cases, and the model says so instead of hiding them in a confident label.
How many groups would the data have chosen without our hint? We fit and compare by PSIS-LOO (on one core, the four-group model alone takes about 20 minutes, which is itself a warning sign), using the chains of the dominant mode for each.
mk <- map(1:4, ~ fit_mix(Yp, .x, "penguin"))
ak <- map2(mk, 1:4, align_chains)
tibble(K = 1:4, chains_kept = map_int(ak, ~ length(.x$keep)),
lp_range = map_dbl(ak, ~ round(max(.x$lp) - min(.x$lp), 1)),
elpd = map_dbl(ak, ~ .x$loo$estimates["elpd_loo", "Estimate"]),
se = map_dbl(ak, ~ .x$loo$estimates["elpd_loo", "SE"]),
k_above_0.7 = map_int(ak, ~ sum(.x$loo$diagnostics$pareto_k > 0.7))) |>
mutate(elpd_vs_3 = elpd - elpd[3], across(c(elpd, se, elpd_vs_3), ~ round(.x, 1)))
table(paste(birds$species, birds$sex), group = max.col(ak[[4]]$prob))
# A tibble: 4 x 7
K chains_kept lp_range elpd se k_above_0.7 elpd_vs_3
<int> <int> <dbl> <dbl> <dbl> <int> <dbl>
1 1 4 0.6 -1530 23.2 0 -336
2 2 4 0.1 -1236. 27.2 0 -42.6
3 3 4 0.5 -1194. 28.9 1 0
4 4 1 70.7 -1073. 40.1 1 121.
group
1 2 3 4
Adelie NA 4 0 1 0
Adelie female 68 0 5 0
Adelie male 70 2 1 0
Chinstrap female 6 25 3 0
Chinstrap male 0 33 1 0
Gentoo NA 0 0 0 4
Gentoo female 0 0 0 58
Gentoo male 0 0 0 61
The comparison does not stop at three. Two groups are much worse than three (−42.6), but four groups beat three by 121 units of expected log predictive density, about three times the standard errors. That is not the end of the story. With four groups the chains disagreed: their average log densities spanned 70.7 units, only one chain was in the best mode, and so the four-group estimate rests on 500 draws from a single chain, with no guarantee that an even better mode was not missed. Nor is the fourth group a new species, or one sex of a species. Gentoo stays one group; most Chinstrap (with two Adélie males) form another; most Adélie (with six female Chinstrap) a third; and the fourth holds 11 birds, mostly females of the two smaller species. A component like this improves the description of the density by catching birds that sit away from their species' main cloud, but it is not a natural kind. The lesson is the factor model's lesson again: LOO says how many components describe the data best, which is not the same as how many kinds of thing there are.
Choosing by comparing finite mixtures has a Bayesian alternative: put a prior on partitions with no fixed number of groups. The Dirichlet process (Ferguson, 1973) does this, and its behaviour is easiest to see through the Chinese restaurant process: penguins arrive one at a time; each joins an existing group with probability proportional to its size, or starts a new group with probability proportional to a concentration parameter . The expected number of groups after arrivals is , which grows like . In a Dirichlet process mixture the data then decide how many of these groups are occupied.
Two features of the prior matter in practice. Group sizes are very unequal: a few large groups and a tail of small ones, a "rich get richer" pattern that suits some data (species abundances, word frequencies) and not others. And the number of groups keeps growing with , so a Dirichlet process mixture fitted to a large data set from three true groups tends to report several extra small ones (Miller and Harrison, 2013). That is not a bug in the method, but it means the posterior number of groups is not an estimate of "the" number of clusters; it is the number of components needed to describe the density.
Check Yourself
Under a Chinese restaurant process with , what is the expected number of groups among observations?
.
. For it is about 6.4, and for about 9.8: logarithmic growth, so the prior always leaves room for one more group.
11.9 Subtypes from expression
The cancer gene-expression project asks the penguin question of tumours: do expression profiles fall into groups, and are they the known molecular subtypes? We return to the TCGA breast-cancer data of Chapter 10 (Cancer Genome Atlas Network, 2012; Goldman et al., 2020), now taking every primary tumour with a PAM50 subtype call, 844 in all. PAM50 is a 50-gene classifier that assigns each tumour to one of five intrinsic subtypes, Luminal A, Luminal B, HER2-enriched, Basal-like and Normal-like (Parker et al., 2009); the calls play the part of the hidden penguin species. No labels are used in fitting, so there is no training and test split. With 1,000 genes and 844 tumours a full-covariance mixture is out of reach, so we combine this chapter's two tools: reduce the 1,000 most variable genes to their first three principal components, then fit a three-group mixture in that space with the code of Section 11.7.
xena <- "https://tcga-xena-hub.s3.us-east-1.amazonaws.com/download/TCGA.BRCA.sampleMap%2F"
dir.create("data/tcga", showWarnings = FALSE, recursive = TRUE)
for (f in c("HiSeqV2.gz", "BRCA_clinicalMatrix"))
if (!file.exists(file.path("data/tcga", f))) download.file(paste0(xena, f), file.path("data/tcga", f))
expr <- read_tsv("data/tcga/HiSeqV2.gz", show_col_types = FALSE)
clin <- read_tsv("data/tcga/BRCA_clinicalMatrix", show_col_types = FALSE, guess_max = 5000)
X <- t(as.matrix(expr[, -1])); colnames(X) <- expr$sample
pam <- clin |> filter(substr(sampleID, 14, 15) == "01", !is.na(PAM50Call_RNAseq)) # primary tumours
X <- X[intersect(rownames(X), pam$sampleID), ]
subtype <- pam$PAM50Call_RNAseq[match(rownames(X), pam$sampleID)]
her2 <- pam$HER2_Final_Status_nature2012[match(rownames(X), pam$sampleID)]
table(subtype)
G <- X[, order(apply(X, 2, var), decreasing = TRUE)[1:1000]] # no labels used
pc <- prcomp(G, scale. = TRUE)
round(summary(pc)$importance[2:3, 1:5], 3)
Z <- scale(pc$x[, 1:3])
mt <- fit_mix(Z, 3, "xena"); at <- align_chains(mt, 3)
c(chains_kept = length(at$keep), lp_range = round(max(at$lp) - min(at$lp), 1))
table(subtype, group = max.col(at$prob))
c(uncertain = sum(apply(at$prob, 1, max) <= 0.9))
subtype
Basal Her2 LumA LumB Normal
141 67 421 192 23
PC1 PC2 PC3 PC4 PC5
Proportion of Variance 0.161 0.074 0.043 0.032 0.026
Cumulative Proportion 0.161 0.235 0.278 0.310 0.336
chains_kept lp_range
4.0 0.6
group
subtype 1 2 3
Basal 135 0 6
Her2 0 0 67
LumA 0 314 107
LumB 0 39 153
Normal 5 12 6
uncertain
279
The first three principal components explain 16%, 7% and 4% of the variance in the 1,000 genes, 28% together. The four chains agree after relabelling (log densities within 0.6 of each other). The three groups follow the best-known divisions in breast cancer. Group 1 is Basal-like: it holds 135 of the 141 Basal tumours and nothing else apart from five Normal-like ones. Basal tumours differ from the rest across thousands of genes, so they dominate the first component and separate cleanly, just as the Gentoo penguins did. Group 2 is mostly Luminal A (314 tumours, with 39 Luminal B). Group 3 collects all 67 HER2-enriched tumours and 153 of the 192 Luminal B, but also 107 Luminal A. What separates groups 2 and 3 is roughly the difference between slowly and rapidly dividing tumours, and the PAM50 classes on that axis overlap rather than forming islands. And 279 of the 844 tumours have no group with probability above 0.9: a third of the tumours sit between groups, against 13 of 342 penguins.
mt4 <- fit_mix(Z, 4, "xena"); at4 <- align_chains(mt4, 4)
c(chains_kept = length(at4$keep), lp_range = round(max(at4$lp) - min(at4$lp), 1))
loo_compare(list(K3 = at$loo, K4 = at4$loo))
table(subtype, group = max.col(at4$prob))
c(uncertain = sum(apply(at4$prob, 1, max) <= 0.9))
table(clinical_her2 = her2, group_K3 = max.col(at$prob), useNA = "ifany")
chains_kept lp_range
4.0 0.3
elpd_diff se_diff
K4 0.0 0.0
K3 -25.1 8.2
group
subtype 1 2 3 4
Basal 1 0 136 4
Her2 26 0 0 41
LumA 139 280 0 2
LumB 164 27 0 1
Normal 2 12 4 5
uncertain
394
group_K3
clinical_her2 1 2 3
Equivocal 1 0 8
Negative 125 299 210
Positive 3 21 89
<NA> 11 45 32
PSIS-LOO prefers four groups to three by 25.1 units of expected log predictive density, about three standard errors. The fourth group is mostly HER2-enriched: 41 of its 53 tumours, and 41 of the 67 HER2-enriched tumours overall, with the other 26 joining a group of Luminal B and Luminal A. The Basal group is unchanged, but the extra group buys a better density at the price of more ambiguity: 394 tumours are now uncertain. The last table compares the three groups with clinical HER2 status, the outcome of Chapter 10: group 3 holds 89 of the 113 clinically positive tumours, but they are a minority of it (210 are negative). That is an honest, unglamorous result. HER2 status is decided by the amplification of one small region of chromosome 17, a strong signal in a handful of genes, and a variance-based summary of a thousand genes does not emphasise it; the supervised horseshoe of Chapter 10 found it in one gene. Unsupervised groups are groups in whatever the reduction emphasises.
Check Yourself
Soft assignments are a strength, but they are not the main issue here.
Many good studies use principal components first.
A clustering is a choice among many (features, preprocessing, , algorithm), and survival differences between groups are easy to find when the choice is made after seeing them. The honest procedure fixes the clustering pipeline in advance or validates the groups and their survival differences in an independent cohort, and propagates membership uncertainty, for example by repeating the survival analysis over posterior draws of the labels.
Project Connections: Structure Without Labels
For the cancer gene-expression project, Section 10.8 and this section are two halves of one workflow: discover candidate subtypes without labels (factor or principal-component reduction, then a mixture with soft assignments), then ask, with held-out data, whether they predict anything that matters. The TCGA result is the template for an honest report: which known subtypes the groups recover, which they merge, and how many tumours sit between groups. A Bayesian factor model with a horseshoe prior on the loadings (a sparse factor model) is the natural next step for 20,000 genes, because each factor then involves only a few genes and can be read as a pathway. For clustering hostile web traffic, mixtures are the natural model of "kinds of client", and two lessons here transfer directly: run several chains from different starts and report the modes they find, and expect a Dirichlet-process mixture to keep adding small groups as the logs grow, which is useful for spotting new behaviour but makes "the number of attack types" a poorly defined target. For AI-generated text detection, topic models (Chapter 12) are mixtures of words, and the identification and label-switching issues carry over unchanged.
Key Takeaways: Lecture 22
- A mixture model says each case belongs to one of latent groups; summing out the label gives a likelihood HMC can sample, and Bayes' rule gives each case a soft assignment.
- Label switching is bookkeeping: fix it by relabelling chains or draws to a reference. Ordering constraints work only when the groups separate in the ordered coordinate.
- Local modes are real: chains can settle in different, worse solutions. Compare chains' log posterior densities, report what each found, and set aside or stack them by a stated rule.
- Choosing by LOO measures how well the mixture describes the density, which is not the same as counting natural kinds; on the penguins, the data preferred more groups than species.
Lecture 23 — Copulas
11.10 Two genes on one amplicon
Chapter 10 left a puzzle that this lecture takes up. In its Exercise 6 the horseshoe could not decide between ERBB2 and GRB7, two genes a few tens of kilobases apart on chromosome 17 that are amplified together in HER2-positive tumours. Their correlation across tumours is high, but a correlation coefficient is a poor description of how they move together. Look at them across the 1,097 primary tumours of the TCGA data, and at a second pair, ESR1, the gene for the oestrogen receptor, and GATA3, a transcription factor that works with it.
library(copula)
xena <- "https://tcga-xena-hub.s3.us-east-1.amazonaws.com/download/TCGA.BRCA.sampleMap%2F"
dir.create("data/tcga", showWarnings = FALSE, recursive = TRUE)
if (!file.exists("data/tcga/HiSeqV2.gz")) download.file(paste0(xena, "HiSeqV2.gz"), "data/tcga/HiSeqV2.gz")
expr <- read_tsv("data/tcga/HiSeqV2.gz", show_col_types = FALSE) # log2(RSEM + 1)
G <- expr |> filter(sample %in% c("ERBB2", "GRB7", "ESR1", "GATA3")) |>
pivot_longer(-sample, names_to = "tumour") |> pivot_wider(names_from = sample) |>
filter(substr(tumour, 14, 15) == "01") # primary tumours
nrow(G)
dep <- function(x, y) c(pearson = cor(x, y), spearman = cor(x, y, method = "spearman"),
kendall = cor(x, y, method = "kendall"))
rbind(`ERBB2-GRB7, log scale` = dep(G$ERBB2, G$GRB7), `ERBB2-GRB7, raw scale` = dep(2^G$ERBB2, 2^G$GRB7),
`ESR1-GATA3, log scale` = dep(G$ESR1, G$GATA3), `ESR1-GATA3, raw scale` = dep(2^G$ESR1, 2^G$GATA3)) |> round(3)
[1] 1097
pearson spearman kendall
ERBB2-GRB7, log scale 0.843 0.678 0.510
ERBB2-GRB7, raw scale 0.871 0.678 0.510
ESR1-GATA3, log scale 0.835 0.717 0.525
ESR1-GATA3, raw scale 0.560 0.717 0.525
The two pairs have almost the same rank correlations: Kendall's is 0.510 for ERBB2 and GRB7 and 0.525 for ESR1 and GATA3, and Spearman's 0.678 and 0.717. Pearson's correlation is higher, 0.843 and 0.835, on the log scale on which expression is usually analysed; on the original scale of normalised counts it is 0.871 for the first pair but only 0.560 for the second. The rank correlations do not change at all. A dependence measure that changes when the data are re-expressed is describing the margins as much as the dependence.
pobs <- function(x) rank(x) / (length(x) + 1) # pseudo-observations
pairs_long <- bind_rows(
tibble(pair = "ERBB2 and GRB7", x = G$ERBB2, y = G$GRB7),
tibble(pair = "ESR1 and GATA3", x = G$ESR1, y = G$GATA3)) |>
mutate(u = pobs(x), v = pobs(y), .by = pair)
p_log <- ggplot(pairs_long, aes(x, y)) + geom_point(size = 0.5, alpha = 0.45, colour = harbour[["sky"]]) +
facet_wrap(~ pair, ncol = 1, scales = "free") + labs(x = "first gene, log2 expression", y = "second gene")
p_rank <- ggplot(pairs_long, aes(u, v)) + geom_point(size = 0.5, alpha = 0.45, colour = harbour[["sky"]]) +
annotate("rect", xmin = 0.95, xmax = 1, ymin = 0.95, ymax = 1, fill = NA, colour = harbour[["rose"]], linetype = 2) +
annotate("rect", xmin = 0, xmax = 0.05, ymin = 0, ymax = 0.05, fill = NA, colour = harbour[["rose"]], linetype = 2) +
facet_wrap(~ pair, ncol = 1) + coord_equal() + labs(x = "rank of first gene", y = "rank of second gene")
patchwork::wrap_plots(p_log, p_rank, widths = c(1.15, 1))
The two pairs have almost the same rank correlation, yet they are dependent in opposite ways. For ERBB2 and GRB7 the dependence is loose among ordinary tumours and very tight at the top: when the amplicon is amplified, both genes are pushed to extreme values together. For ESR1 and GATA3 it is the other way round: tumours that have lost the oestrogen-receptor programme, mostly the Basal tumours of Section 11.9, have both genes very low, while among the oestrogen-receptor-positive majority the two vary fairly independently. A correlation coefficient, or a bivariate normal distribution, cannot tell these two pictures apart. The tool that can is the copula: the joint distribution of the ranks, stripped of the margins.
11.11 Sklar's theorem
The construction rests on one simple fact, the probability integral transform: if is continuous with distribution function , then is uniform on , because . Run backwards, it is how a computer simulates from any distribution: draw uniform and return . Apply it to each coordinate of a pair with margins and , and the pair has uniform margins. Its joint distribution function
is a copula: a bivariate distribution function on the unit square with uniform margins. Everything about how and depend on each other is in , and nothing about their individual distributions is.
Result: Sklar's Theorem (Sklar, 1959)
Let be a joint distribution function with margins and . Then there is a copula such that
If and are continuous, is unique. Conversely, for any copula and any distribution functions and , the formula defines a joint distribution function with margins and .
We prove the continuous case. Let and . For continuous , , where , so is uniform, and likewise . Let be the joint distribution function of , a copula. Since is non-decreasing, implies , and implies except on an event of probability zero, so
Uniqueness: every is for some when is continuous, so is determined by everywhere. For the converse, let have distribution function and set , . Because exactly when , , and setting shows that has margin . ∎
The converse is also how copula models are simulated: draw from , then transform each coordinate with the inverse distribution function of the margin you want. Differentiating gives the density,
so the log-likelihood of pairs splits into three parts, one for each margin and one for the dependence:
This is what makes copulas useful in applied work. The margins can be anything: a gamma for waiting times, a Poisson for goals, a mixture for a bimodal gene, or no model at all. The dependence is then modelled separately, with a family chosen for the shape of the dependence rather than forced on it by the margins. A bivariate normal distribution is the special case of normal margins with a Gaussian copula; change either part and you have a model the bivariate normal cannot express.
Four consequences follow directly.
- Invariance. If and are strictly increasing, has the same copula as , because the ranks do not change. So any measure of dependence that depends only on the copula is unchanged by taking logarithms, square roots or any other monotone transformation. Pearson's correlation is not such a measure; the output in Section 11.10 shows it falling from 0.835 to 0.560 when the expression of ESR1 and GATA3 is taken off the log scale, while both rank correlations stay exactly the same.
- Rank correlations are copula properties. Kendall's , the probability that two tumours are ranked the same way on both genes minus the probability that they are ranked the opposite way, and Spearman's , the correlation of the ranks, are
- Fréchet–Hoeffding bounds. Every copula lies between two extremes:
The upper bound is perfect positive dependence (), the lower bound perfect negative dependence (), and independence is the product copula . A consequence that surprises people: for given margins, not every Pearson correlation between and is attainable. Two lognormal variables with very different variances cannot have correlation 1 even when one is an increasing function of the other, which is one reason a correlation matrix is a poor specification of a joint distribution (Embrechts, McNeil and Straumann, 2002).
- Tail dependence. The upper and lower tail dependence coefficients,
measure whether extremes happen together. If , then given that one variable is beyond a very high quantile, the other is beyond it too with probability about 0.6, however extreme the quantile. If , joint extremes become rarer than single ones as the threshold rises, even when the variables are strongly correlated.
11.12 A small zoo of copulas
Five families cover most applied work. Each has one parameter, and each is most easily understood through the Kendall's it implies, which lets families be compared at the same overall strength of dependence.
| Family | Kendall's | ||
|---|---|---|---|
| Gaussian, | |||
| Student , , | |||
| Clayton, | |||
| Gumbel, | |||
| Frank, |
Here and in the Gumbel copula, is the standard bivariate normal distribution function with correlation , its Student- analogue, , and is the Debye function. The Gaussian and Frank copulas have no tail dependence; the has the same amount in both tails, shrinking as grows; Clayton has it only in the lower tail and Gumbel only in the upper. Any copula can be rotated: the survival copula, the copula of , swaps the tails, so a survival Gumbel has lower tail dependence and a survival Clayton upper. Clayton and Gumbel belong to the Archimedean class, for a decreasing generator , which is also how they are simulated: Gumbel's generator is the Laplace transform of a positive stable variable, Clayton's that of a gamma variable, so both are a shared random "frailty" that pushes the two variables up or down together.
Set and step through the families. The Gaussian and Frank clouds are symmetric and thin out in both corners; Frank is the more evenly spread. Clayton pinches into a point at the lower left and fans out at the upper right; Gumbel does the opposite. On the unit square the differences look modest, because the corners are small. On the normal scale they are obvious: under Clayton the lower-left tail is a tight streak, under the Gaussian copula an ellipse. The readout counts the draws in the two corner squares. Under independence each would hold 0.25% of the draws; at the Gaussian copula puts about 2.0% in each and Frank 1.1%, while Gumbel puts 3.0% in the upper square and Clayton 3.5% in the lower one. For anything that depends on joint extremes, such as two hospitals both overwhelmed on the same day or two assets both crashing, those factors of two or three are the whole story.
Check Yourself
A pair of variables has a Gaussian copula with . What is Kendall's ? (What is it if the margins are changed to exponential?)
Use , and remember that depends on the copula only.
, whatever the margins: exponential, lognormal or anything else, because changing the margins applies an increasing transformation to each variable and leaves the ranks, and hence the copula, unchanged. Pearson's correlation would change with the margins.
11.13 Fitting copulas in Stan
Bayesian inference for a copula follows from the split likelihood of Section 11.11. The full approach models the margins and the copula together and samples everything jointly; it is the right choice when the margins are of interest, or when the data are discrete (Section 11.15). When the margins are awkward and the question is about dependence, as with these genes, a common shortcut replaces and by the scaled ranks and , the pseudo-observations plotted in Section 11.10, and treats as the likelihood of the copula parameter (Genest, Ghoudi and Rivest, 1995). The ranks are not quite independent data, so this is a pseudo-likelihood and its posterior is slightly too narrow; with a thousand tumours and one parameter the effect is small, and Section 11.14 shows the fully Bayesian alternative.
One Stan program fits all five candidate copulas. We write each density in terms of its log, put the same uniform prior on Kendall's for every family, and convert to the family's own parameter with the formulas in the table, so that the families are compared on equal terms. The survival versions are obtained by flipping the data, .
cop_code <- "
functions {
vector cop_lpdf_vec(vector a, vector b, vector x, vector y, real theta, int family) {
if (family == 1) // Gaussian
return -0.5 * log1m(theta^2) - (theta^2 * (x .* x + y .* y) - 2 * theta * x .* y) / (2 * (1 - theta^2));
if (family == 2 || family == 4) { // Gumbel
vector[rows(a)] s = x .^ theta + y .^ theta;
vector[rows(a)] A = s .^ (1 / theta);
return -A - log(a) - log(b) + (theta - 1) * (log(x) + log(y)) + (1 / theta - 2) * log(s) + log(A + theta - 1);
}
return log1p(theta) - (1 + theta) * (log(a) + log(b)) // Clayton
- (2 + 1 / theta) * log(a .^ (-theta) + b .^ (-theta) - 1);
}
}
data {
int<lower=1> N; vector<lower=0, upper=1>[N] u; vector<lower=0, upper=1>[N] v;
int<lower=1, upper=5> family; // 1 Gaussian, 2 Gumbel, 3 Clayton, 4 survival Gumbel, 5 survival Clayton
}
transformed data {
vector[N] a = family >= 4 ? 1 - u : u; // a survival copula is the copula of (1 - U, 1 - V)
vector[N] b = family >= 4 ? 1 - v : v;
vector[N] x = family == 1 ? inv_Phi(a) : -log(a);
vector[N] y = family == 1 ? inv_Phi(b) : -log(b);
}
parameters { real<lower=0, upper=0.95> tau; } // Kendall's tau, uniform prior for every family
transformed parameters {
real theta = family == 1 ? sin(pi() * tau / 2)
: (family == 2 || family == 4) ? 1 / (1 - tau) : 2 * tau / (1 - tau);
}
model { target += sum(cop_lpdf_vec(a, b, x, y, theta, family)); }
generated quantities {
vector[N] log_lik = cop_lpdf_vec(a, b, x, y, theta, family);
real lambda = family == 1 ? 0 : (family == 2 || family == 4) ? 2 - 2^(1 / theta) : 2^(-1 / theta);
}"
cop_path <- "fits/ch11_cop_model.rds"
if (file.exists(cop_path)) cop_mod <- readRDS(cop_path) else { cop_mod <- stan_model(model_code = cop_code); saveRDS(cop_mod, cop_path) }
families <- c(Gaussian = 1, Gumbel = 2, Clayton = 3, `survival Gumbel` = 4, `survival Clayton` = 5)
fit_cop <- function(x, y, tag) map(families, function(f) {
path <- sprintf("fits/ch11_cop_%s_%d.rds", tag, f); if (file.exists(path)) return(readRDS(path))
fit <- sampling(cop_mod, data = list(N = length(x), u = pobs(x), v = pobs(y), family = f),
chains = 4, iter = 1000, seed = 11, refresh = 0)
saveRDS(fit, path); fit })
compare_cop <- function(fits) {
loos <- map(fits, function(f) { ll <- extract_log_lik(f, merge_chains = FALSE); loo(ll, r_eff = relative_eff(exp(ll))) })
cmp <- loo_compare(loos)
tibble(family = rownames(cmp), elpd_diff = cmp[, "elpd_diff"], se_diff = cmp[, "se_diff"]) |>
left_join(map_dfr(fits, function(f) { s <- summary(f, pars = c("tau", "lambda"))$summary
tibble(tau = s["tau", "mean"], lambda = s["lambda", "mean"], rhat = s["tau", "Rhat"]) }, .id = "family"), by = "family") |>
mutate(across(where(is.double), ~ round(.x, 3)))
}
fits_her2 <- fit_cop(G$ERBB2, G$GRB7, "her2"); fits_er <- fit_cop(G$ESR1, G$GATA3, "er")
compare_cop(fits_her2)
compare_cop(fits_er)
# A tibble: 5 x 6
family elpd_diff se_diff tau lambda rhat
<chr> <dbl> <dbl> <dbl> <dbl> <dbl>
1 Gumbel 0 0 0.546 0.63 1.00
2 survival Clayton -0.009 8.55 0.517 0.724 1.00
3 Gaussian -87.5 10.5 0.519 0 1.00
4 survival Gumbel -150. 13.6 0.475 0.561 1.00
5 Clayton -253. 20.0 0.367 0.55 1.01
# A tibble: 5 x 6
family elpd_diff se_diff tau lambda rhat
<chr> <dbl> <dbl> <dbl> <dbl> <dbl>
1 Clayton 0 0 0.475 0.682 1.00
2 survival Gumbel -0.549 8.93 0.51 0.596 1.00
3 Gaussian -50.6 18.5 0.499 0 1.01
4 Gumbel -159. 22.2 0.432 0.517 1.01
5 survival Clayton -253. 22.8 0.311 0.464 1.00
The two pairs give mirror-image answers. For ERBB2 and GRB7, the two families with upper tail dependence only, Gumbel and survival Clayton, are indistinguishable (difference 0.0, standard error 8.6) and beat the Gaussian copula by 87.5 units of expected log predictive density, more than eight standard errors; the families with lower tail dependence are far worse. For ESR1 and GATA3 the order is reversed: Clayton and survival Gumbel lead and beat the Gaussian by 50.6 (standard error 18.5), and the upper-tail families come last. All chains converged (). The posterior means of are close to the sample values but not equal to them (0.546 for Gumbel against 0.510; 0.475 for Clayton against 0.525), because each family's is the value at which that family best matches the whole shape of the copula; a family of the wrong shape distorts it badly (Clayton gives 0.367 for the first pair). The winners' tail dependence, for Gumbel and for Clayton, says that given one gene is extreme, the other is too about two times in three.
The question that matters in practice is how much the choice changes a prediction about extremes. Take the probability that a tumour is in the top 5% for both ERBB2 and GRB7: under independence it would be , and with perfect dependence 0.05. For ESR1 and GATA3 the corresponding question is about the bottom 5%. Each copula answers it through , which we compute for every posterior draw.
joint_tail <- function(fit, f, q, upper) { # posterior draws of Pr(both beyond the q-quantile)
th <- rstan::extract(fit, "theta")$theta[seq(1, 2000, by = 4)]
rot <- f >= 4; lowr <- if (upper) !rot else rot # which corner of the fitted (possibly rotated) copula
Cqq <- function(t, p) switch(as.character(f), `1` = pCopula(c(p, p), normalCopula(t)),
`2` = , `4` = p^(2^(1 / t)), `3` = , `5` = (2 * p^(-t) - 1)^(-1 / t))
map_dbl(th, function(t) if (lowr) 1 - 2 * q + Cqq(t, q) else Cqq(t, 1 - q))
}
tail_table <- function(fits, x, y, upper) map_dfr(c(0.95, 0.99), function(p) {
emp <- if (upper) mean(pobs(x) > p & pobs(y) > p) else mean(pobs(x) < 1 - p & pobs(y) < 1 - p)
bind_rows(tibble(model = "observed", mean = emp, lo = NA, hi = NA),
imap_dfr(fits, function(f, nm) { d <- joint_tail(f, families[[nm]], p, upper)
tibble(model = nm, mean = mean(d), lo = quantile(d, 0.05), hi = quantile(d, 0.95)) })) |>
mutate(quantile = p, .before = 1) }) |> mutate(across(c(mean, lo, hi), ~ round(.x, 4)))
tail_table(fits_her2[c("Gaussian", "Gumbel")], G$ERBB2, G$GRB7, upper = TRUE)
tail_table(fits_er[c("Gaussian", "Clayton")], G$ESR1, G$GATA3, upper = FALSE)
# conditional exceedance curves for the widget below, at the posterior mean of each copula
Cdiag <- function(f, t, p) switch(as.character(f), `1` = pCopula(c(p, p), normalCopula(t)), `2` = p^(2^(1 / t)),
`3` = (2 * p^(-t) - 1)^(-1 / t), `4` = 2 * p - 1 + (1 - p)^(2^(1 / t)), `5` = 2 * p - 1 + (2 * (1 - p)^(-t) - 1)^(-1 / t))
qs <- seq(0.5, 0.99, by = 0.01)
curves <- function(fits, x, y) { u <- pobs(x); v <- pobs(y)
list(emp_upper = round(map_dbl(qs, ~ mean(u > .x & v > .x) / mean(u > .x)), 4),
emp_lower = round(map_dbl(qs, ~ mean(u < 1 - .x & v < 1 - .x) / mean(u < 1 - .x)), 4),
model = imap(fits, function(f, nm) { t <- mean(rstan::extract(f, "theta")$theta); fam <- families[[nm]]
list(upper = round(map_dbl(qs, ~ (1 - 2 * .x + Cdiag(fam, t, .x)) / (1 - .x)), 4),
lower = round(map_dbl(qs, ~ Cdiag(fam, t, 1 - .x) / (1 - .x)), 4)) })) }
writeLines(paste0("var C11C=", jsonlite::toJSON(list(q = qs, her2 = curves(fits_her2, G$ERBB2, G$GRB7),
er = curves(fits_er, G$ESR1, G$GATA3)), auto_unbox = TRUE), ";"), "data/ch11_cop.js")
# A tibble: 6 x 5
quantile model mean lo hi
<dbl> <chr> <dbl> <dbl> <dbl>
1 0.95 observed 0.0392 NA NA
2 0.95 Gaussian 0.0209 0.02 0.0218
3 0.95 Gumbel 0.0321 0.0312 0.0329
4 0.99 observed 0.0027 NA NA
5 0.99 Gaussian 0.0029 0.0027 0.0031
6 0.99 Gumbel 0.0063 0.0061 0.0065
# A tibble: 6 x 5
quantile model mean lo hi
<dbl> <chr> <dbl> <dbl> <dbl>
1 0.95 observed 0.0201 NA NA
2 0.95 Gaussian 0.0199 0.0189 0.0208
3 0.95 Clayton 0.0342 0.0332 0.035
4 0.99 observed 0.0018 NA NA
5 0.99 Gaussian 0.0027 0.0025 0.0029
6 0.99 Clayton 0.0068 0.0066 0.007
At the 95th percentile, the Gumbel copula is much closer to the data than the Gaussian for ERBB2 and GRB7: it predicts 3.2% of tumours in the top corner (90% interval 3.1% to 3.3%), the Gaussian 2.1%, and 3.9% (43 tumours) are observed. At the 99th percentile the picture flips: 3 tumours (0.27%) are in the corner, which is what the Gaussian predicts (0.29%), while the Gumbel predicts 0.63%. For ESR1 and GATA3 the Gaussian matches the observed share in the bottom 5% (2.0%) and Clayton, the LOO winner, overpredicts it (3.4%). Two lessons follow. First, these intervals are far too narrow to believe: they describe uncertainty about within one family, with ranks treated as data, not uncertainty about the shape of the tail, and the top 1% of each gene contains only 11 tumours. Second, the family that fits best overall need not fit best in every corner.
set.seed(11); n <- nrow(G)
tau_post <- function(f) mean(rstan::extract(f, "tau")$tau)
sims <- bind_rows(
tibble(panel = "observed", u = pobs(G$ERBB2), v = pobs(G$GRB7)),
as_tibble(rCopula(n, normalCopula(sin(pi * tau_post(fits_her2$Gaussian) / 2))), .name_repair = ~ c("u", "v")) |> mutate(panel = "Gaussian copula"),
as_tibble(rCopula(n, gumbelCopula(1 / (1 - tau_post(fits_her2$Gumbel)))), .name_repair = ~ c("u", "v")) |> mutate(panel = "Gumbel copula")) |>
mutate(panel = factor(panel, c("observed", "Gaussian copula", "Gumbel copula")))
ggplot(filter(sims, u > 0.8, v > 0.8), aes(u, v)) + geom_point(size = 0.7, alpha = 0.6, colour = harbour[["sky"]]) +
facet_wrap(~ panel) + coord_equal(xlim = c(0.8, 1), ylim = c(0.8, 1)) +
scale_x_continuous(breaks = c(0.8, 0.9, 1)) + scale_y_continuous(breaks = c(0.8, 0.9, 1)) +
labs(x = "rank of ERBB2", y = "rank of GRB7")
The widget sets the fitted copulas side by side through the quantity that defines tail dependence, , as moves toward 1 (or as moves toward 0).
Look at the points rather than the lines. For ERBB2 and GRB7 in the upper tail, the observed conditional probability rises to 0.94 at and then falls, to 0.80 at 0.95 and 0.57 at 0.98. For ESR1 and GATA3 in the lower tail it peaks at about 0.9 near , the bottom quarter, and falls to 0.29 at 0.98. No one-parameter family has this shape: the Gaussian curve falls steadily toward zero, while Gumbel and Clayton level off at their . The shape is that of a mixture. About one tumour in seven is HER2-positive (Section 10.8), and in those tumours both genes are pushed above everyone else's, so a tumour in the top 10% for ERBB2 is almost always in the top 10% for GRB7; within the amplified tumours the two genes are still strongly related but not in lockstep, so once the threshold lies deep inside the amplified group, the conditional probability falls. In the same way, about 23% of the tumours are oestrogen-receptor negative and have both ESR1 and GATA3 low. The copula of these data is a mixture of copulas, one for each kind of tumour, and the model it calls for is a Lecture 22 mixture with a copula inside each component (Exercise 11). The one-parameter families were still informative: LOO picked the right tail for each pair, and Gumbel beat the Gaussian around the 90th to 95th percentiles where the amplified tumours sit. But a tail dependence coefficient is a limit that finite data never reach, so report joint exceedance probabilities at thresholds the data can support, and check them against the data.
Warning: The Formula That Was Blamed
In 2000 David Li proposed modelling the times to default of many companies with a Gaussian copula, with a single correlation parameter calibrated from market prices (Li, 2000). The model made it easy to price collateralised debt obligations, and it became an industry standard. When US house prices fell in 2007 and 2008, defaults arrived together on a scale the models had treated as almost impossible, and the Gaussian copula was blamed in the press as "the formula that killed Wall Street". The history is more complicated: practitioners knew the model's limits and used it as a pricing convention as much as a description of risk (MacKenzie and Spears, 2014). But the statistical point is exactly the one above. A Gaussian copula has no tail dependence, so it says that joint extremes become negligible as they become more extreme, and a single correlation fitted to ordinary times says nothing about whether that is true. Whenever a conclusion depends on joint extremes, the copula family must be checked against the tails of the data, and when the tails are thin in the data, the uncertainty about them must be reported.
Check Yourself
Kendall's tau summarises the whole copula, not its tails.
Copulas are defined for any continuous margins.
Copula families with the same can differ greatly in the corners. With , , so : given that one station has an extreme day, the other does too about half the time, however extreme the threshold. The Gaussian copula says that this conditional probability falls to zero. For flood risk, which depends on several catchments flooding together, the choice dominates the answer.
11.14 Mixed data and the Gaussian copula
The questionnaire of Lecture 21 is the opposite problem: the items are coded 1 to 6, so their margins are discrete, and the factor model treated the codes as if they were continuous and normal. Discrete margins break the neat story of Section 11.11. The probability integral transform of a discrete variable is not uniform, and Sklar's copula is no longer unique: many copulas give the same joint distribution of the codes, and rank-based estimates are biased by the ties (Genest and Nešlehová, 2007).
The Gaussian copula has a way through, because it is a latent-variable model. Suppose each person has a latent vector , with a correlation matrix, and each observed variable is an increasing function of one coordinate, . For a continuous variable that is the Gaussian copula with margin . For an ordinal item it is the threshold model of Chapter 4, with when falls between two cut points. Peter Hoff (2007) noticed that one does not need to model the margins at all. Whatever the increasing functions are, the observed data tell us the order of the latent values within each variable: if person gave a higher code than person on item , then , and ties say nothing. The probability that the latent normal vectors respect all these orderings depends only on . This extended rank likelihood gives a posterior for that is the same whichever increasing functions produced the data, and a Gibbs sampler that draws each from a truncated normal between its neighbours in the ordering. It handles continuous, ordinal and binary variables together, and missing values are simply left unconstrained, so they are imputed as part of the sampler (Chapter 7). The sbgcop package implements it.
We apply it to the five Neuroticism items, which ask how easily people get angry, irritated, moody, blue and panicky, together with three variables of different kinds: education (an ordinal code from 1 to 5, missing for 223 people), age (continuous and skewed) and a female indicator (binary).
tmp <- tempfile()
download.file("https://raw.githubusercontent.com/cran/psychTools/master/data/bfi.rda", tmp, quiet = TRUE)
load(tmp)
D <- bfi |> transmute(N1, N2, N3, N4, N5, education, age, female = as.integer(gender == 2)) |> as.matrix()
c(people = nrow(D), missing = sum(is.na(D)))
set.seed(11)
gc_path <- "fits/ch11_sbgcop.rds"
if (file.exists(gc_path)) gc_fit <- readRDS(gc_path) else {
gc_fit <- sbgcop::sbgcop.mcmc(D, nsamp = 2000, odens = 4, verb = FALSE); saveRDS(gc_fit, gc_path) }
R_draws <- gc_fit$C.psamp # 8 x 8 x 500 posterior draws of R
R_mean <- apply(R_draws, c(1, 2), mean)
round(R_mean, 2)
people missing
2800 342
N1 N2 N3 N4 N5 education age female
N1 1.00 0.77 0.61 0.44 0.42 -0.06 -0.10 0.07
N2 0.77 1.00 0.60 0.43 0.39 -0.06 -0.10 0.13
N3 0.61 0.60 1.00 0.57 0.47 -0.06 -0.11 0.17
N4 0.44 0.43 0.57 1.00 0.44 0.01 -0.04 0.01
N5 0.42 0.39 0.47 0.44 1.00 -0.06 -0.11 0.29
education -0.06 -0.06 -0.06 0.01 -0.06 1.00 0.35 0.01
age -0.10 -0.10 -0.11 -0.04 -0.11 0.35 1.00 0.05
female 0.07 0.13 0.17 0.01 0.29 0.01 0.05 1.00
pr <- which(upper.tri(R_mean), arr.ind = TRUE)
P <- cor(D, use = "pairwise.complete.obs")
cmp <- tibble(pair = paste(colnames(D)[pr[, 1]], "and", colnames(D)[pr[, 2]]), copula = R_mean[pr],
lo = apply(R_draws, 3, function(m) m[pr]) |> apply(1, quantile, 0.05),
hi = apply(R_draws, 3, function(m) m[pr]) |> apply(1, quantile, 0.95), pearson = P[pr]) |>
filter(abs(copula) > 0.08) |> mutate(pair = fct_reorder(pair, copula))
ggplot(cmp, aes(y = pair)) + geom_vline(xintercept = 0, colour = harbour[["grid"]]) +
geom_point(aes(x = pearson), colour = harbour[["sky"]], size = 1.6) +
geom_errorbarh(aes(xmin = lo, xmax = hi), height = 0, colour = harbour[["rose"]]) +
geom_point(aes(x = copula), colour = harbour[["rose"]], size = 1.6) +
labs(x = "correlation (blue: Pearson on codes; red: Gaussian copula, 90% interval)", y = NULL)
Almost every latent correlation in the figure is further from zero than Pearson's correlation of the codes; the exceptions are the small correlations with age, where the two agree. For pairs of items the difference is modest (N1 and N2: 0.77, 90% interval 0.75 to 0.78, against 0.71), because six-point items keep most of the information. It is largest where a variable is coarse: education and age correlate 0.35 (0.32 to 0.39) on the latent scale against 0.24 for Pearson, and the binary female indicator correlates 0.29 (0.25 to 0.33) with N5, panicking easily, against 0.21. Pearson's correlation of codes is attenuated: coding throws away information that the latent values had. The copula estimates the correlation of the underlying continuous quantities, under the assumption that they are jointly normal after some increasing transformation of each, which the data cannot check for a binary variable and can only partly check for an ordinal one. The 342 missing values, 223 of them for education, were imputed inside the sampler, so nobody was dropped, and age, which is strongly skewed in this sample, needed no transformation, because the rank likelihood uses only its ordering.
The latent correlation matrix is a natural input to the factor model of Section 11.4: instead of treating codes as normal, put the factor structure on , so that on the latent scale. This is the Gaussian copula factor model of Murray et al. (2013), which keeps the interpretation of Lecture 21 while respecting the ordinal nature of the items, and lets binary, count and continuous variables load on the same factors.
Check Yourself
Ranks can be computed for any variable that can be ordered.
The Gaussian copula is used for these data in this very section.
Sklar's theorem guarantees a unique copula only for continuous margins. With discrete margins, takes a few values, not a uniform spread, and many copulas reproduce the same table of codes. Treating ties as data attenuates the dependence, which is what the Pearson correlations in Section 11.14 show; the extended rank likelihood (or a full model of discrete margins, as in Section 11.15) avoids it.
11.15 Copulas in applied projects
The three uses above, describing tail dependence, separating margins from dependence, and modelling mixed data on a latent scale, are the common ones in applied work. A fourth is a check of an assumption that most models make silently: that two outcomes are independent given the model's predictors. The football model of the Afterword, like most models of match results, gives home and away goals independent Poisson distributions given the two teams' strengths. In the Premier League the two scores are negatively correlated, which suggests the assumption is wrong. But the correlation of the raw scores mixes two things: dependence within a match, and the fact that a strong team both scores more and concedes less. A copula on the conditional margins separates them.
Goals are counts, so we use the full likelihood rather than ranks. For a match with home goals and away goals , Poisson margins and given the team strengths, and a copula , the probability of the score is the probability of a rectangle,
which is how any copula is applied to discrete margins. We use the Frank copula, which allows negative as well as positive dependence and has no tail dependence to confuse with the small counts, and fit it jointly with the attack and defence strengths of every team in each of six seasons (2019/20 to 2024/25, 2,280 matches), using the data of the Afterword.
seasons <- c("1920", "2021", "2122", "2223", "2324", "2425")
for (s in seasons) { f <- sprintf("data/football/E0_%s.csv", s)
if (!file.exists(f)) download.file(sprintf("https://www.football-data.co.uk/mmz4281/%s/E0.csv", s), f) }
epl <- map_dfr(seasons, function(s) read_csv(sprintf("data/football/E0_%s.csv", s), show_col_types = FALSE) |>
transmute(season = s, home = HomeTeam, away = AwayTeam, hg = FTHG, ag = FTAG))
teams <- sort(unique(c(paste(epl$season, epl$home), paste(epl$season, epl$away))))
c(matches = nrow(epl), team_seasons = length(teams), raw_correlation = round(cor(epl$hg, epl$ag), 3))
foot_code <- "
functions {
real frank_C(real u, real v, real theta) { // Frank copula C(u, v)
if (u <= 0 || v <= 0) return 0;
return -log1p(expm1(-theta * u) * expm1(-theta * v) / expm1(-theta)) / theta;
}
real rect(real F1, real F0, real G1, real G0, real theta) {
return frank_C(F1, G1, theta) - frank_C(F0, G1, theta) - frank_C(F1, G0, theta) + frank_C(F0, G0, theta);
}
}
data { int<lower=1> N; int<lower=1> T; array[N] int<lower=0> hg; array[N] int<lower=0> ag;
array[N] int<lower=1, upper=T> ht; array[N] int<lower=1, upper=T> at; }
parameters { real mu; real home; vector[T] att_z; vector[T] def_z; real<lower=0> s_att; real<lower=0> s_def; real theta; }
transformed parameters { vector[T] att = s_att * att_z; vector[T] def = s_def * def_z; }
model {
mu ~ normal(0, 1); home ~ normal(0, 0.5); att_z ~ std_normal(); def_z ~ std_normal();
s_att ~ normal(0, 0.5); s_def ~ normal(0, 0.5); theta ~ normal(0, 3);
for (n in 1:N) {
real lh = exp(mu + home + att[ht[n]] - def[at[n]]); real la = exp(mu + att[at[n]] - def[ht[n]]);
target += log(rect(poisson_cdf(hg[n] | lh), hg[n] > 0 ? poisson_cdf(hg[n] - 1 | lh) : 0,
poisson_cdf(ag[n] | la), ag[n] > 0 ? poisson_cdf(ag[n] - 1 | la) : 0, theta));
}
}
generated quantities { // predicted share of draws, 0-0 and 1-1, with and without the copula
real p_draw = 0; real p_draw_ind = 0; real p00 = 0; real p00_ind = 0; real p11 = 0; real p11_ind = 0;
vector[N] lam_h; vector[N] lam_a;
for (n in 1:N) {
real lh = exp(mu + home + att[ht[n]] - def[at[n]]); real la = exp(mu + att[at[n]] - def[ht[n]]);
lam_h[n] = lh; lam_a[n] = la;
for (k in 0:10) {
real pk = rect(poisson_cdf(k | lh), k > 0 ? poisson_cdf(k - 1 | lh) : 0, poisson_cdf(k | la), k > 0 ? poisson_cdf(k - 1 | la) : 0, theta);
real pind = exp(poisson_lpmf(k | lh) + poisson_lpmf(k | la));
p_draw += pk / N; p_draw_ind += pind / N;
if (k == 0) { p00 += pk / N; p00_ind += pind / N; }
if (k == 1) { p11 += pk / N; p11_ind += pind / N; }
}
}
}"
foot_path <- "fits/ch11_foot_frank.rds"
if (file.exists(foot_path)) foot_fit <- readRDS(foot_path) else {
foot_fit <- stan(model_code = foot_code, chains = 4, iter = 1000, seed = 11, refresh = 0,
data = list(N = nrow(epl), T = length(teams), hg = epl$hg, ag = epl$ag,
ht = match(paste(epl$season, epl$home), teams), at = match(paste(epl$season, epl$away), teams)))
saveRDS(foot_fit, foot_path) }
summary(foot_fit, pars = c("theta", "home", "s_att", "s_def"), probs = c(0.05, 0.95))$summary[, c("mean", "5%", "95%", "Rhat")] |> round(3)
lam <- rstan::extract(foot_fit, c("lam_h", "lam_a"))
c(divergences = get_num_divergent(foot_fit),
cor_of_expected_goals = round(cor(colMeans(lam$lam_h), colMeans(lam$lam_a)), 3))
pred <- summary(foot_fit, pars = c("p_draw", "p_draw_ind", "p00", "p00_ind", "p11", "p11_ind"), probs = c(0.05, 0.95))$summary[, c("mean", "5%", "95%")]
cbind(observed = c(rep(mean(epl$hg == epl$ag), 2), rep(mean(epl$hg == 0 & epl$ag == 0), 2), rep(mean(epl$hg == 1 & epl$ag == 1), 2)), pred) |> round(4)
matches team_seasons raw_correlation
2280.000 120.000 -0.152
mean 5% 95% Rhat
theta -0.283 -0.514 -0.044 0.999
home 0.157 0.115 0.198 0.999
s_att 0.285 0.248 0.325 1.002
s_def 0.207 0.174 0.242 1.002
divergences cor_of_expected_goals
0.000 -0.537
observed mean 5% 95%
p_draw 0.2303 0.2254 0.2196 0.2315
p_draw_ind 0.2303 0.2313 0.2273 0.2352
p00 0.0539 0.0602 0.0554 0.0657
p00_ind 0.0539 0.0646 0.0609 0.0683
p11 0.1105 0.1052 0.1031 0.1074
p11_ind 0.1105 0.1049 0.1029 0.1070
The raw scores are negatively correlated (−0.15), but most of that is team strength: across matches, the model's expected home and away goals correlate −0.54, because a strong team both scores more and concedes less. Given the strengths, the Frank parameter is (90% interval −0.51 to −0.04), a Kendall's of −0.03: weak negative dependence within a match, with no divergent transitions and . Where does it show? Independence predicts that 6.5% of matches end 0–0 (90% interval 6.1% to 6.8%), the copula 6.0% (5.5% to 6.6%), and 5.4% did, so the copula moves toward the data. But it also lowers the predicted share of all draws, from 23.1% to 22.5% (22.0% to 23.2%), while 23.0% of matches were drawn, and both models predict 1–1 at 10.5% against 11.1% observed. Dixon and Coles (1997), with English league matches from the 1990s, found the opposite adjustment, more low-scoring draws than independence predicts, and McHale and Scarf (2011), modelling international matches with copulas, found dependence that was weak between evenly matched teams and increasingly negative as matches became less competitive. In the modern Premier League, conditional on the teams' strengths, home and away goals are close to independent, and the small dependence there is cannot be summarised as "more draws". That is a useful negative result: the independence assumption of the Afterword's model survived a check that could have refuted it, and a copula on conditional margins is how the check is done.
Project Connections: Dependence Beyond Correlation
For the cancer gene-expression project, Section 11.13 is a template: co-amplified genes are tail-dependent, not just correlated, and a copula on pairs (or a vine copula, which builds a high-dimensional copula from a tree of bivariate ones; Aas et al., 2009) describes pathway structure that a correlation network misses. For the quantum computing project of the Afterword, the question "do qubits on one device have bad days together?" is a question about upper tail dependence of error rates, and it decides whether choosing qubits independently is safe; fit the copula to the residuals of the state-space model, not to the raw rates, for the same reason as in Section 11.15. For the student evaluation and exam anxiety projects, the semiparametric Gaussian copula of Section 11.14 is the right way to correlate Likert items with grades, ages and indicators, and the Gaussian copula factor model is the right way to measure anxiety from them. For NYC EMS response equity, daily average response times in the five boroughs rise together on bad days (snowstorms, heat waves, holidays), and a copula with upper tail dependence fitted to the daily series describes how often the whole city is stretched at once. For football, Section 11.15 is the check every goals model should report, and the same rectangle likelihood extends to shots, corners and cards. For AI-generated text detection, detectors combine many features as if they were independent given the author; a Gaussian copula on the features, fitted separately for each author type, is the simplest way to stop correlated evidence being counted twice, the failing of naive Bayes in Chapter 12.
Research Practice: Reporting a Copula Model
- Say how the margins were handled: modelled jointly, estimated first and plugged in, or replaced by ranks. The last two understate uncertainty; say so.
- Report the family comparison (LOO or held-out log likelihood, with standard errors), including at least one family with tail dependence in each tail that the question involves, and show the data's corners next to the fitted copula's.
- Report dependence as Kendall's and, when extremes matter, as tail dependence or a joint exceedance probability with an interval, not only as the family's own parameter, which is not comparable across families.
- For discrete or mixed data, state how ties were handled (the rectangle likelihood or the extended rank likelihood), and never apply a continuous-data method to ranks with many ties.
- Fit the copula to conditional margins when predictors explain part of the dependence, as team strength does for goals.
History: Links Between Margins
Wassily Hoeffding (1940) and Maurice Fréchet (1951) found the bounds of Section 11.11 while studying joint distributions with given margins. Abe Sklar (1959) proved the theorem and named the object, from the Latin copula, a link or tie, in a three-page note written in answer to a question of Fréchet's. For two decades copulas were mostly a topic in probabilistic metric spaces, until statisticians, starting with Christian Genest and Jock MacKay's "The joy of copulas" (1986), recognised them as a way to build multivariate models. Harry Joe (1997, 2014) and Roger Nelsen (2006) wrote the standard texts; Kjersti Aas and colleagues (2009) made vine copulas practical in high dimensions; and Peter Hoff's extended rank likelihood (2007) gave a fully Bayesian treatment of mixed data. Copulas spread through actuarial science, hydrology and finance, where joint extremes are the whole problem, and, after 2008, into the cautionary tales of finance.
Key Takeaways: Lecture 23
- A copula is the joint distribution of the ranks: by Sklar's theorem, any continuous joint distribution splits uniquely into its margins and a copula, and any copula combines with any margins.
- Kendall's and Spearman's depend only on the copula; Pearson's correlation does not, and some correlations are unattainable for given margins.
- Families with the same differ in their tails: Gaussian and Frank have no tail dependence, Clayton has lower, Gumbel upper, both. For co-amplified ERBB2 and GRB7 a Gumbel copula beat the Gaussian by 87.5 in LOO, but the data's tails are those of a mixture of subtypes: check joint exceedances at thresholds the data support, and do not trust a fitted beyond them.
- Bayesian copula models can use the full likelihood (margins and copula together) or ranks as pseudo-observations; for mixed and ordinal data, the Gaussian copula with Hoff's extended rank likelihood recovers latent correlations that Pearson's attenuates.
- For discrete outcomes the likelihood is a rectangle probability. In the Premier League, the raw negative correlation of home and away goals comes from team strength; given the strengths, the scores are close to independent.
11.16 Exercises
Each exercise has a hint and an answer button.
Exercises
What five factors miss. For the five-factor model, compute the residual correlations: observed item correlations minus those implied by the posterior mean of . Which item pairs are worst fitted, and how many of the 300 pairs have residuals larger than 0.1 in absolute value?
cov2cor(Lm %*% t(Lm) + diag(psi^2))gives the implied correlations.The five-factor model reproduces the correlations well: only one of the 300 item pairs has a residual correlation above 0.1 in absolute value, A1 with A2 (0.123), and 25 exceed 0.05. The worst-fitted pairs are A1–A2, A1–C4 (0.085), E1–A1 (0.079), N3–N4 (0.076), C4–O5 (0.075) and O2–O5 (0.072). Several involve reverse-scored items (A1, C4, E1, O2, O5): negatively worded statements share a little variance of their own, a well-known "method factor" in questionnaire data, and one candidate for what the sixth factor of Section 11.5 picks up.
Factor scores. Compute each person's estimated factor scores, , from the posterior means, and compare women and men on each factor in standard-deviation units (in
bfi,genderis 1 for men and 2 for women). Is this a causal comparison?Keep only the rows used in the fit (
complete.cases(bfi[, 1:25])).The data contain 805 men and 1,631 women. In standard-deviation units, women score higher than men on Agreeableness (0.40), Neuroticism (0.27), Conscientiousness (0.19) and Extraversion (0.15), and lower on Openness (−0.25). These differences are descriptive, not causal: the sample is self-selected volunteers on a website, the comparison mixes everything that differs between women and men in this sample (age, education, culture of response), and it assumes the items measure the same traits in the same way for both groups (measurement invariance), which should itself be tested by fitting the factor model separately or jointly with group-specific loadings.
Bayesian versus classical. Compare the posterior mean loadings with the maximum-likelihood varimax solution from
factanal(), after aligning the two by a Procrustes rotation. How different are they, and why?Align with the singular value decomposition of , as in
rotate_draws().After alignment, the largest difference between the Bayesian posterior means and the maximum-likelihood loadings is 0.003 (mean 0.0009), against posterior standard deviations of up to 0.024. With 2,436 people and weak priors, the likelihood dominates and the two methods agree; the Bayesian fit adds uncertainty for every loading and every derived quantity (communalities, factor scores, residual correlations), and it would matter more with a few hundred respondents or with informative priors on the loadings.
Simpler groups. Refit the three-group penguin mixture with diagonal covariance matrices (each measurement independent within a group). How well does it recover the species, and how does it compare with the full-covariance model by PSIS-LOO?
Replace
multi_normal_cholesky_lpdfbynormal_lpdf(y[n] | mu[k], sigma[k])and dropOmega.The diagonal model is both worse and harder to fit. Its chains split: three reached an average log density near −1404 and one −1385, so
align_chains()keeps only the last. That chain's solution puts all Adélie and Chinstrap penguins in one group and splits the Gentoo into two (61 and 62 birds, very likely by sex). By PSIS-LOO it is far worse than the full-covariance model (difference −174.7, standard error 22.6). The reason is that bill length, bill depth, flipper length and mass are strongly correlated within each species (bigger birds are bigger everywhere); a diagonal model can only represent that correlation by adding groups along the size axis, which is exactly what it did with the Gentoo.k-means. Run k-means with three clusters on the standardised penguin measurements. How does it compare with the species, and with the mixture's most probable groups? What does k-means implicitly assume that the mixture does not?
kmeans(Yp, 3, nstart = 50).k-means puts all 123 Gentoo together, but it assigns 24 Adélie to the Chinstrap cluster (and 5 Chinstrap to the Adélie cluster), so 29 penguins are grouped with another species, against 6 for the mixture; the two methods disagree about 23 birds. k-means implicitly fits a mixture with equal weights and the same spherical covariance in every group, and assigns each point wholly to the nearest centre. Adélie are far more numerous than Chinstrap and their clouds are elongated and correlated, so equal spheres misplace the boundary; and k-means gives no measure of how borderline each assignment is.
More components of expression. Repeat the TCGA analysis of Section 11.9 with the first five principal components instead of three. Do the groups match the subtypes better?
Use
fit_mix()or the samesampling()call with .With five components the groups match the subtypes somewhat better, and differently. The Basal group is unchanged (132 of 141). The other two groups now split along a new line: one group holds 62 of the 67 HER2-enriched tumours, with 41 Luminal B, 16 Luminal A, 9 Basal and 6 Normal-like, so HER2-enriched tumours are its largest class but under half of it; the other holds 405 of the 421 Luminal A and 151 of the 192 Luminal B tumours, which are no longer separated at all. The chains agree (log densities within 1 unit). Only 116 of the 844 tumours are now uncertain, against 279 with three components, and the adjusted Rand index with the PAM50 calls rises from 0.42 to 0.49. More components capture more biology, but with three groups the mixture must choose which division to show, and it shows whichever the reduction makes largest; the Luminal A and B distinction lost out here. The number of components is a modelling choice to report, not a nuisance detail.
A prior on the number of groups. Under a Dirichlet process with , what is the prior expected number of groups among the 342 penguins? What value of makes the expectation 3? With that , what is the expectation for 10,000 penguins?
; solve for with
uniroot().With the prior expects 6.41 groups among 342 penguins, already twice the number of species. Setting the expectation to 3 requires ; with that , the expected number of groups among 10,000 penguins is 4.13. The logarithmic growth means a prior tuned to one sample size gives a different expected number of groups at another, which is why the concentration parameter is often given its own prior rather than fixed.
Bad anchors. Refit the five-factor model with the hand-picked anchors N1, E4, C2, A2 and O1 and Stan's default random initial values (four chains). Report each chain's posterior mean of the five diagonal loadings and the largest . What went wrong, or right?
Reorder the columns of
Yso that the anchors come first, and callsampling()withoutinit.Three chains agree, and one does not. The anchors' diagonal loadings are 0.86, 0.72, 0.65, 0.41 and 0.50 in chains 1 to 3, but chain 4 has 0.01 for the fourth anchor (A2) and 0.23 for the fifth (O1), and the largest is 8.2. The fourth chain settled in a mode where A2 does not define its own factor: the factor that should be Agreeableness has taken a different direction, with A2's loading forced to be positive but almost zero. A2 was a weak anchor, because it also loads on Extraversion. The fix used in Section 11.4 (anchors chosen from a classical solution, and chains started there) is not cosmetic; without it, the model is identified in theory but multimodal in practice.
Unattainable correlations. Let be lognormal(0, 1) and lognormal(0, ) with . Using the Fréchet–Hoeffding bounds, find the largest and smallest Pearson correlations the pair can have, whatever their copula. Check by simulation. What does this say about specifying a joint distribution by margins and a correlation matrix?
The bounds are attained by and with , standard normal. Use to show that the correlation is .
The largest attainable correlation is and the smallest , even though in the first case is an increasing function of and in the second a decreasing one. A simulation of a million pairs gives 0.717 and −0.095: the sample correlation of such skewed variables converges slowly, which is a second warning about using it. So a correlation of 0.8 between these two variables is impossible, and "perfect dependence" can show up as a correlation of 0.67. A correlation matrix with given margins may not correspond to any joint distribution, and, when it does, it does not determine one; the copula, with or as the summary, has neither problem.
The Gaussian copula's vanishing tail. For the Gaussian and Gumbel copulas fitted to ERBB2 and GRB7 (posterior mean parameters), compute for = 0.9, 0.99, 0.999 and 0.9999. What happens as ?
; use
pCopula(c(q, q), normalCopula(rho))for the Gaussian and for the Gumbel.With the Gaussian copula gives 0.49, 0.29, 0.18 and 0.12: the conditional probability keeps falling, toward its limit of , although very slowly (for a Gaussian copula the decay is roughly a power of ). The Gumbel copula with gives 0.66, 0.63, 0.63 and 0.63, settling at . At the 90th percentile the two differ by a third; at one in ten thousand by a factor of five. Data with a thousand observations cannot tell which limit is right, so the choice of family is an assumption about how extremes behave beyond the data, and it should be reported as one.
A mixture of copulas. Split the tumours by clinical HER2 status (
HER2_Final_Status_nature2012in the clinical file of Chapter 10). Compute Kendall's of ERBB2 and GRB7 within the positive and the negative tumours, and count the HER2-positive tumours among those in the top 10%, 5% and 2% of both genes. How does this explain the shape of the observed curve in the widget of Section 11.13?Join the clinical file to
Gby tumour; for the counts, use the pseudo-observations computed from all tumours.Within the 114 HER2-positive tumours ; within the 652 negative ones . Of the 102 tumours in the top 10% of both genes, 75 are positive, 2 negative and 25 have no recorded status; of the 43 in the top 5%, 28 are positive and 1 negative. The pooled data are a mixture: a large group with moderate dependence, and a group of about one tumour in seven whose amplified genes sit above everyone else's. Being in the top 10% of one gene almost guarantees membership of the amplified group, and hence being in the top 10% of the other, which is why the observed conditional probability peaks near 0.94 at , roughly where the amplified group begins. Further into the tail we are ranking tumours within the amplified group, where the dependence is strong but not perfect (Kendall's is 0.59 among the top 10% of ERBB2), so the curve falls. A model with a latent subgroup and a copula within each, the mixture of Lecture 22 with copula components, would reproduce both features; any single one-parameter family must compromise.
Polychoric correlations. For the five Neuroticism items, compare the latent correlations of Section 11.14 with the polychoric correlations from
psych::polychoric()and with Pearson's correlations of the codes. Why do the first two agree?psych::polychoric(D[, 1:5])$rho.The polychoric and copula correlations agree to within 0.004 for all ten pairs (for example 0.764 and 0.767 for N1 and N2, 0.437 and 0.440 for N1 and N4), while Pearson's are 0.03 to 0.06 lower (0.707 and 0.398). A polychoric correlation is the correlation of a bivariate normal distribution whose thresholds reproduce the observed table of two ordinal items, which is exactly the Gaussian copula model for a pair of ordinal variables; estimated by maximum likelihood with thresholds fixed from the margins, it is the classical version of the same estimate. The Bayesian copula adds three things: it handles ordinal, binary and continuous variables in one model rather than pair by pair, so the matrix is guaranteed to be a valid correlation matrix; it imputes missing values; and it gives posterior intervals.
Low scores. Using the posterior means of the expected goals and of from Section 11.15, compute the expected number of matches ending in each score from 0–0 to 2–2 under independence and under the Frank copula, and compare with the observed counts. Which discrepancies could a copula fix?
Sum the rectangle probabilities over the 2,280 matches; under independence the probability of – is
dpois(x, lh) * dpois(y, la).Observed, independent and Frank counts are: 0–0, 123, 140 and 130; 1–0, 200, 201 and 197; 0–1, 158, 177 and 175; 1–1, 252, 244 and 245; 2–0, 157, 157 and 161; 0–2, 136, 121 and 126; 2–1, 187, 183 and 184; 1–2, 157, 161 and 163; 2–2, 125, 115 and 113. The copula moves the 0–0 count halfway to the data and changes little else. The other discrepancies, such as too few 0–1 results and too many 0–2 and 2–2 results, are not of the form a single dependence parameter can fix; with counts of this size the Poisson standard deviation is about 12 to 14, so none is more than about 1.5 standard deviations from either model. The check supports independence given team strengths; if a larger data set confirmed a pattern like the 0–1 deficit, the margins (for example overdispersion, or scoring that depends on the current score) would be the place to look, not the copula.
Further reading and references
Further Reading
- Bishop, Pattern Recognition and Machine Learning (2006), Chapters 9 and 12: mixtures, EM and probabilistic PCA, with clear derivations.
- Frühwirth-Schnatter, Celeux and Robert (eds.), Handbook of Mixture Analysis (2019): the modern reference for finite mixtures, label switching and choosing .
- Betancourt's case study "Identifying Bayesian mixture models" (2017, on the Stan website): why mixtures are hard for HMC, shown with care.
- Borsboom, Mellenbergh and van Heerden, "The theoretical status of latent variables" (2003): what it means to say a factor causes test scores.
- Gelman et al., Bayesian Data Analysis (3rd edition, 2013), Chapters 22 and 23: finite mixtures and Dirichlet process models.
- Nelsen, An Introduction to Copulas (2nd edition, 2006): the gentlest rigorous introduction, with Sklar's theorem, the families and the dependence measures; Joe, Dependence Modeling with Copulas (2014) is the comprehensive reference, including vines.
- Genest and Nešlehová, "A primer on copulas for count data" (2007): what goes wrong with discrete margins, and why.
- Hoff, "Extending the rank likelihood for semiparametric copula estimation" (2007): the Gaussian copula for mixed data, short and readable.
- Aas, K., Czado, C., Frigessi, A. and Bakken, H. (2009). Pair-copula constructions of multiple dependence. Insurance: Mathematics and Economics, 44(2), 182–198.
- Allport, G. W. and Odbert, H. S. (1936). Trait-names: a psycho-lexical study. Psychological Monographs, 47(1), Whole No. 211.
- Bishop, C. M. (1999). Bayesian PCA. Advances in Neural Information Processing Systems, 11, 382–388.
- Borsboom, D., Mellenbergh, G. J. and van Heerden, J. (2003). The theoretical status of latent variables. Psychological Review, 110(2), 203–219.
- Cancer Genome Atlas Network (2012). Comprehensive molecular portraits of human breast tumours. Nature, 490(7418), 61–70.
- Dempster, A. P., Laird, N. M. and Rubin, D. B. (1977). Maximum likelihood from incomplete data via the EM algorithm. Journal of the Royal Statistical Society, Series B, 39(1), 1–38.
- Diebolt, J. and Robert, C. P. (1994). Estimation of finite mixture distributions through Bayesian sampling. Journal of the Royal Statistical Society, Series B, 56(2), 363–375.
- Dixon, M. J. and Coles, S. G. (1997). Modelling association football scores and inefficiencies in the football betting market. Journal of the Royal Statistical Society: Series C, 46(2), 265–280.
- Embrechts, P., McNeil, A. J. and Straumann, D. (2002). Correlation and dependence in risk management: properties and pitfalls. In M. A. H. Dempster (ed.), Risk Management: Value at Risk and Beyond, 176–223. Cambridge University Press.
- Ferguson, T. S. (1973). A Bayesian analysis of some nonparametric problems. Annals of Statistics, 1(2), 209–230.
- Fréchet, M. (1951). Sur les tableaux de corrélation dont les marges sont données. Annales de l'Université de Lyon, Section A, 14, 53–77.
- Fried, L. P., Tangen, C. M., Walston, J. et al. (2001). Frailty in older adults: evidence for a phenotype. Journals of Gerontology Series A, 56(3), M146–M156.
- Genest, C. and MacKay, J. (1986). The joy of copulas: bivariate distributions with uniform marginals. The American Statistician, 40(4), 280–283.
- Genest, C. and Nešlehová, J. (2007). A primer on copulas for count data. ASTIN Bulletin, 37(2), 475–515.
- Genest, C., Ghoudi, K. and Rivest, L.-P. (1995). A semiparametric estimation procedure of dependence parameters in multivariate families of distributions. Biometrika, 82(3), 543–552.
- Geweke, J. and Zhou, G. (1996). Measuring the pricing error of the arbitrage pricing theory. Review of Financial Studies, 9(2), 557–587.
- Goldman, M. J., Craft, B., Hastie, M., Repečka, K., McDade, F., Kamath, A., Banerjee, A., Luo, Y., Rogers, D., Brooks, A. N., Zhu, J. and Haussler, D. (2020). Visualizing and interpreting cancer genomics data via the Xena platform. Nature Biotechnology, 38(6), 675–678.
- Gorman, K. B., Williams, T. D. and Fraser, W. R. (2014). Ecological sexual dimorphism and environmental variability within a community of Antarctic penguins (genus Pygoscelis). PLoS ONE, 9(3), e90081.
- Hoeffding, W. (1940). Masstabinvariante Korrelationstheorie. Schriften des Mathematischen Instituts und des Instituts für Angewandte Mathematik der Universität Berlin, 5(3), 179–233.
- Hoff, P. D. (2007). Extending the rank likelihood for semiparametric copula estimation. Annals of Applied Statistics, 1(1), 265–283.
- Horst, A. M., Hill, A. P. and Gorman, K. B. (2020). palmerpenguins: Palmer Archipelago (Antarctica) penguin data. R package version 0.1.0.
- Joe, H. (1997). Multivariate Models and Dependence Concepts. Chapman & Hall.
- Joe, H. (2014). Dependence Modeling with Copulas. CRC Press.
- Kaiser, H. F. (1958). The varimax criterion for analytic rotation in factor analysis. Psychometrika, 23(3), 187–200.
- Li, D. X. (2000). On default correlation: a copula function approach. Journal of Fixed Income, 9(4), 43–54.
- MacKenzie, D. and Spears, T. (2014). 'The formula that killed Wall Street': the Gaussian copula and modelling practices in investment banking. Social Studies of Science, 44(3), 393–417.
- McHale, I. and Scarf, P. (2011). Modelling the dependence of goals scored by opposing teams in international soccer matches. Statistical Modelling, 11(3), 219–236.
- Miller, J. W. and Harrison, M. T. (2013). A simple example of Dirichlet process mixture inconsistency for the number of components. Advances in Neural Information Processing Systems, 26.
- Mitnitski, A. B., Mogilner, A. J. and Rockwood, K. (2001). Accumulation of deficits as a proxy measure of aging. The Scientific World Journal, 1, 323–336.
- Murray, J. S., Dunson, D. B., Carin, L. and Lucas, J. E. (2013). Bayesian Gaussian copula factor models for mixed data. Journal of the American Statistical Association, 108(502), 656–665.
- Nelsen, R. B. (2006). An Introduction to Copulas, 2nd edition. Springer.
- Parker, J. S., Mullins, M., Cheang, M. C. U., Leung, S., Voduc, D., Vickery, T., Davies, S., Fauron, C., He, X., Hu, Z., Quackenbush, J. F., Stijleman, I. J., Palazzo, J., Marron, J. S., Nobel, A. B., Mardis, E., Nielsen, T. O., Ellis, M. J., Perou, C. M. and Bernard, P. S. (2009). Supervised risk predictor of breast cancer based on intrinsic subtypes. Journal of Clinical Oncology, 27(8), 1160–1167.
- Revelle, W. (2024). psychTools: tools to accompany the 'psych' package for psychological research. R package.
- Sklar, A. (1959). Fonctions de répartition à n dimensions et leurs marges. Publications de l'Institut de Statistique de l'Université de Paris, 8, 229–231.
- Spearman, C. (1904). "General intelligence," objectively determined and measured. American Journal of Psychology, 15(2), 201–292.
- Stephens, M. (2000). Dealing with label switching in mixture models. Journal of the Royal Statistical Society, Series B, 62(4), 795–809.
- Thurstone, L. L. (1935). The Vectors of Mind: Multiple-Factor Analysis for the Isolation of Primary Traits. University of Chicago Press.
- Thurstone, L. L. (1947). Multiple-Factor Analysis. University of Chicago Press.
- Tipping, M. E. and Bishop, C. M. (1999). Probabilistic principal component analysis. Journal of the Royal Statistical Society, Series B, 61(3), 611–622.
- Yao, Y., Vehtari, A. and Gelman, A. (2022). Stacking for non-mixing Bayesian computations: the curse and blessing of multimodal posteriors. Journal of Machine Learning Research, 23(79), 1–45.