Applied Bayesian Inference · Chapter 1

Chapter 1
The causal–Bayesian workflow

Lectures 1–2 · small worlds, graphs, priors and posteriors: the loop that every later chapter runs

Cartoon: a model and the worldA stick figure holds up a small snow globe labelled "my model" and says: In here, the vaccine is 95% effective. A second figure, standing in front of a sprawling hand-drawn city, asks: And out here? In here, the vaccineis 95% effective. my model And out here,in the world?
every conclusion is made inside a model; the work is making the model resemble the world

In November 2020, a pharmaceutical company announced that its new COVID-19 vaccine was about 95% effective. The number came from 170 sick people. Eight of them had received the vaccine and 162 had received a placebo injection. From those two counts, and from a careful plan written down months earlier, the company's statisticians produced a statement that billions of people would act on: given these data, the probability that the vaccine's efficacy exceeds 30% is essentially 1.

That statement is Bayesian. It is a probability about something we cannot see (the vaccine's true efficacy), computed from something we can see (the counts), using rules stated in advance. You will reproduce it, exactly, in Section 1.2. But the more important lesson of this chapter is what stands behind such a number. Two kinds of assumption make it meaningful. One is a set of claims about what causes what: that the vaccine was assigned by a coin toss, so nothing else differs systematically between the two groups. The other is a set of claims about how the data were generated and what was plausible beforehand. Make the first kind of assumption wrongly and the most precise posterior in the world answers a question nobody asked. Make the second kind wrongly and the posterior answers the right question badly.

This course is about doing applied Bayesian research well enough to publish it and to rely on it. This chapter sets out the way of working that every later chapter follows. It has two halves that must be learned together:

Neither half is much use alone. A graph with no probability model cannot be fitted. A probability model with no graph can be fitted, but it cannot be interpreted. Here is the route through the chapter.

Think About It: Before You Start

Think of a claim you have read recently of the form "X increases Y by Z%". What would have to be true about how the data were collected for that sentence to describe what would happen if you changed X yourself? Keep your answer in mind; Section 1.4 gives you a precise way to write it down.

Lecture 1 — Small worlds and the Bayesian update

1.1 Small worlds and large worlds

A statistical model is a small world: a self-contained, fully specified cartoon of reality inside which every event has a probability and every question has an answer. The large world is the messy reality the data actually came from. All inference happens in the small world. Its conclusions are only as trustworthy as the resemblance between the two.

That is why a model can be computed perfectly and still be wrong, and why most of the intellectual work in applied Bayesian analysis is not computation. It is construction (deciding what to put into the small world) and criticism (checking afterwards whether the small world could plausibly have produced what we saw). Computers have made the middle step, computing the posterior, almost free. They have done nothing for the first and last steps, which is where analyses succeed or fail.

Every model in this course has three layers, and we will write each one down explicitly.

  1. A causal layer: a graph saying what influences what. It decides which variables enter the model and what their coefficients mean.
  2. A stochastic layer: a likelihood, saying how the data are generated if we knew the unknowns, and a prior, saying which values of the unknowns are plausible before we see the data.
  3. A computational layer: the algorithm that turns the first two layers into a posterior distribution.

Most failures of applied analysis are failures of layer one disguised as questions about layer three. "Should I run the sampler longer?" is rarely the question that matters. "Should this variable be in the model at all?" often is.

Definition: Likelihood, Prior, Posterior

Let θ be the unknown quantities (parameters) and y the data.

  • The likelihood p(y∣θ) is the probability of the data we observed, as a function of the unknowns. It is the small world's account of how data are produced.
  • The prior p(θ) is a probability distribution over the unknowns, describing what is plausible before seeing y.
  • The posterior p(θ∣y) is the distribution of the unknowns after seeing y. Bayes' theorem gives it:
p(θ∣y)=p(y∣θ)p(θ)p(y),p(y)=∫p(y∣θ)p(θ)dθ.

The denominator p(y) does not depend on θ; it only makes the posterior integrate to 1. So the working version is posterior ∝ likelihood × prior.

The posterior is the entire output of a Bayesian analysis. It is not an estimate; it is a distribution, a weighting over every version of the small world that is consistent with the data and the prior. Point estimates and intervals are summaries we extract from it at the end, and we will resist collapsing it to a single number until the last possible moment.

History: An Essay Published by a Friend

Thomas Bayes, a Presbyterian minister in Tunbridge Wells, never published the theorem that carries his name. After his death in 1761, his friend Richard Price found the manuscript among his papers, edited it, and read it to the Royal Society in 1763 as An Essay towards solving a Problem in the Doctrine of Chances. Bayes's problem was exactly the vaccine problem: given the number of times an event has happened and failed, what is the probability that its chance lies between two given values? The general machinery was built independently, and far more completely, by Pierre-Simon Laplace, who from 1774 onward used it to estimate everything from the ratio of boys to girls born in Paris to the masses of Jupiter and Saturn. For most of the twentieth century the approach was out of favour, partly for philosophical reasons and partly because the integral in the denominator could rarely be computed. The second obstacle fell in the 1990s, when simulation methods (Chapter 5) made almost any posterior computable.

Key Takeaways

  • A model is a small world. Inference is exact inside it and only as good as its resemblance to the large world.
  • Every model has a causal, a stochastic and a computational layer. Write all three down.
  • Posterior ∝ likelihood × prior. The posterior is a distribution, not a number.

1.2 A first Bayesian analysis: did the vaccine work?

The data and the question

The trial of the Pfizer–BioNTech vaccine (Polack et al., 2020) randomised about 43,000 adults to two injections of either the vaccine or a saltwater placebo, in equal numbers. Then everyone went back to their lives, and the investigators waited for people to catch COVID-19. By the final analysis, 170 participants had developed symptomatic, laboratory-confirmed COVID-19 at least a week after their second dose. Eight were in the vaccine group. 162 were in the placebo group.

The question is: how effective is the vaccine? Vaccine efficacy is defined as the proportional reduction in the rate of disease:

VE=1−rate of disease among the vaccinatedrate of disease among the unvaccinated.

A VE of 0 means the vaccine does nothing. A VE of 1 means it prevents every case. A negative VE means it makes disease more likely. Regulators had set the bar in advance: a vaccine would be considered effective if VE was convincingly above 30%.

The causal sketch

Before any probability, a graph. The question is causal: what would the rate of disease be if a person received the vaccine, compared with if they did not? Many things affect whether a person catches COVID-19: their age, their job, how many people they meet, how careful they are. Call all of that U. In an observational study, U would also affect whether people chose to be vaccinated (cautious people get vaccinated and avoid crowds), and the comparison would be hopelessly confounded. Randomisation deletes that arrow.

coinVCU
The trial's causal sketch. Vaccination V affects COVID-19 C. Everything else that affects infection, U (drawn dashed because most of it is unmeasured), also affects C. Because a random number generator decided V, nothing points into V. The coin toss is why the simple comparison of the two arms estimates the causal effect.

That one missing arrow, from U into V, is the entire reason the trial's numbers can be read causally. We will come back to this in Section 1.6. For now, note that the graph has already done its work: it has told us that a simple model comparing the two arms is enough.

A clever likelihood

The trial's statisticians used a neat trick. Because the two arms were the same size and followed for almost exactly the same length of time, we can condition on the total number of cases, 170, and ask a simpler question: of the people who got sick, what fraction were in the vaccine arm? Call that fraction θ.

If the vaccine did nothing, a sick person would be equally likely to come from either arm, and θ would be 1/2. If the vaccine worked perfectly, nobody in the vaccine arm would get sick, and θ would be 0. In general, with equal-sized arms, the case split and the efficacy are linked by

θ=1−VE2−VE⟺VE=1−2θ1−θ.
Where does that formula come from?

Let the rate of disease be λ per person-year among the unvaccinated and (1−VE)λ among the vaccinated. With equal person-time T in each arm, the expected numbers of cases are λT and (1−VE)λT. The probability that a given case came from the vaccine arm is the vaccine arm's share of the expected cases:

θ=(1−VE)λT(1−VE)λT+λT=1−VE2−VE.

Solving for VE gives the second formula. Notice that λ and T cancel: the conditional trick removes the unknown background rate of disease from the problem entirely. (In the real trial the person-time was 2.214 versus 2.222 thousand person-years, close enough that the difference changes nothing at the precision reported.)

Given θ, each of the 170 cases is an independent "draw" that lands in the vaccine arm with probability θ. So the number of vaccine-arm cases follows a binomial distribution:

y∼Binomial(170,θ),observed y=8.

A prior written down in advance

Here is the part that made headlines among statisticians. The trial's protocol, published before the data came in, specified the prior:

θ∼Beta(0.700102,1).

The strange first parameter was chosen for a reason. The mean of a Beta(a,b) distribution is a/(a+b)=0.700102/1.700102=0.4118, and plugging θ=0.4118 into the formula above gives VE = 30%. So the prior is centred on the regulators' minimum acceptable efficacy, and the total "weight" a+b≈1.7 makes it worth less than two observations. It is deliberately modest: the prior is centred on "just barely good enough", and is weak enough that the data will decide.

The update

The Beta distribution and the binomial likelihood fit together perfectly. If the prior is Beta(a,b) and we observe y successes in n trials, the posterior is again a Beta distribution:

θ∣y∼Beta(a+y,b+n−y).

Result: Beta–Binomial Conjugacy

If θ∼Beta(a,b) and y∣θ∼Binomial(n,θ), then θ∣y∼Beta(a+y,b+n−y). The prior acts like a earlier "successes" and b earlier "failures", which are simply added to the observed counts.

Up to constants that do not involve θ, the prior density is θa−1(1−θ)b−1 and the likelihood is θy(1−θ)n−y. Their product is

θa+y−1(1−θ)b+n−y−1,

which is, up to a constant, the density of a Beta(a+y,b+n−y) distribution. Since the posterior must integrate to 1, that constant is forced, and the posterior is exactly that Beta distribution. ∎

So the vaccine posterior is Beta(0.700102+8,1+162)=Beta(8.700102,163). Every question about efficacy can now be answered by transforming this distribution. The easiest way to do that is by simulation: draw many values of θ, convert each one to VE, and summarise the draws. Here is the first R code of the course. It starts by loading the packages we will use throughout and a plotting theme that matches these notes.

R
library(tidyverse)   # data wrangling and ggplot2
library(brms)        # Bayesian regression models, fitted with Stan
library(dagitty)     # causal graphs

# colours and a ggplot2 theme matching these notes
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())

Now the analysis. Read it line by line: it is the whole of Bayesian inference in six lines.

R
a_prior <- 0.700102; b_prior <- 1          # the protocol's prior on theta
y <- 8; n <- 170                           # cases in the vaccine arm, total cases
a_post <- a_prior + y; b_post <- b_prior + n - y

ve <- function(theta) (1 - 2 * theta) / (1 - theta)   # theta -> efficacy

set.seed(2020)
ve_draws <- ve(rbeta(100000, a_post, b_post))    # 100,000 posterior draws of VE
round(quantile(ve_draws, c(0.025, 0.5, 0.975)), 3)
mean(ve_draws > 0.30)                            # Pr(VE > 30% | data)
pbeta(0.7 / 1.7, a_post, b_post, lower.tail = FALSE)   # exact Pr(VE <= 30% | data)
output
 2.5%   50% 97.5% 
0.903 0.949 0.976 
[1] 1
[1] 1.96e-28

The posterior median efficacy is 94.9%, and the central 95% of the posterior lies between 90.3% and 97.6%. The interval is exactly the 95% credible interval reported to the US Food and Drug Administration in December 2020 (FDA, 2020): 95.0% (90.3%, 97.6%). The point estimate of 95.0% there is not a posterior median but the observed efficacy, one minus the ratio of the two arms' case rates per person-year, 1−(8/2.214)/(162/2.222); the exact posterior median is 94.9%. None of our 100,000 draws fell below 30%. The exact calculation in the last line shows why: the posterior probability that VE is 30% or less is about 2×10−28. The protocol required that probability to be below 1.4%.

Picture the prior and the posterior on the scale people care about, the VE scale.

R
draws <- tibble(
  prior     = ve(rbeta(100000, a_prior, b_prior)),
  posterior = ve_draws
) |>
  pivot_longer(everything(), names_to = "distribution", values_to = "VE") |>
  filter(VE > -1)

draws$distribution <- factor(draws$distribution, levels = c("prior", "posterior"))

ggplot(draws, aes(VE, fill = distribution, colour = distribution)) +
  geom_density(alpha = 0.35, adjust = 1.2, linewidth = 0.8) +
  geom_vline(xintercept = 0.3, linetype = "dotted", colour = harbour[["rose"]]) +
  facet_wrap(~ distribution, ncol = 1, scales = "free_y") +
  scale_fill_manual(values = c(prior = harbour[["amber"]], posterior = harbour[["sky"]])) +
  scale_colour_manual(values = c(prior = harbour[["amber"]], posterior = harbour[["sky"]])) +
  scale_x_continuous(labels = scales::percent) +
  labs(x = "vaccine efficacy", y = "density") +
  theme(legend.position = "none")
Two stacked density plots on an axis of vaccine efficacy from minus 100% to 100%Two stacked density plots on an axis of vaccine efficacy from minus 100% to 100%. Top, the prior: low and spread out, rising steadily towards 100%. Bottom, the posterior: a narrow spike between about 90% and 98%. A dotted vertical line marks the 30% threshold in both. posterior prior -100% -50% 0% 50% 100% 0.0 0.5 1.0 1.5 0 5 10 15 20 vaccine efficacy density
Prior (top) and posterior (bottom) for vaccine efficacy, each on its own vertical scale. The prior is spread over every value from harmful to perfect; 170 cases concentrate it between 90% and 98%. The dotted line is the regulators' 30% threshold.

Notice the shape of the prior on this scale. Efficacy can be at most 100%, but it is unbounded below: a vaccine that tripled your risk would have VE = −200%. The protocol's prior gives such catastrophes some weight (its 2.5th percentile is around −2,600%), but it also gives just over half its weight to efficacy above 40%. A prior that looks modest on one scale can look strange on another, and that is a lesson we will meet again in Chapter 2.

Now experiment with the update yourself. The widget below performs exactly the calculation above, but lets you change the data and the prior.

Bayesian updating for vaccine efficacy

Choose how many of the cases occurred in each arm, and choose a prior. The dashed curve is the prior for VE and the filled curve is the posterior. Press replay the trial to watch the 170 real cases arrive one at a time, in a random order.

Three things are worth trying. First, set both counts to zero: the posterior is the prior, because there are no data. Second, replay the trial and watch how quickly the posterior abandons the prior: after twenty or thirty cases, the choice of prior barely matters. Third, switch to the sceptical prior, which is centred on VE = 0 and worth 40 observations, and lower the case counts to something small like 2 and 15. With few data, the prior visibly holds the posterior back. Priors matter most when data are scarce, which is exactly when you should think hardest about them.

Research Practice: Decide Before You Look

The trial's statistical analysis plan stated the prior, the likelihood, the number of cases at which the data would be analysed, and the success criterion (posterior probability that VE > 30% above 98.6% at the final analysis) before any results were seen. That is why nobody could accuse the company of choosing the prior that gave the answer they wanted. For your own work:

  • Pre-register confirmatory analyses (for example on OSF or AsPredicted, or as a registered report): the question, the graph, the model, the priors, and the decision rule.
  • Keep exploratory analyses, which are valuable, clearly labelled as exploratory.
  • If you change the plan after seeing data, say so and say why. Reviewers forgive changes they are told about; they do not forgive changes they discover.

Check Yourself

If the vaccine had no effect at all (VE = 0), what value would θ, the share of cases in the vaccine arm, take?

A trial with the same design observes 4 cases in the vaccine arm and 36 in the placebo arm, with the protocol prior Beta(0.700102, 1). What is the posterior distribution of θ?

Key Takeaways

  • A Bayesian analysis needs a likelihood and a prior; the posterior follows from Bayes' theorem.
  • For a binomial count with a Beta prior, the posterior is Beta with the counts added to the prior parameters.
  • To learn about any function of a parameter (such as VE), transform posterior draws and summarise them.
  • The 2020 vaccine analysis gives VE = 94.9% (95% interval 90.3% to 97.6%), with posterior probability of VE ≤ 30% around 10−28.

1.3 Reading a posterior

A posterior is a distribution, and a distribution answers many questions at once. Three summaries cover most needs.

Definition: Credible Interval

A 100(1−α)% credible interval for θ is an interval [l,u] with Pr(l≤θ≤u∣y)=1−α. It is a direct probability statement about the unknown, given the model and the data.

The phrase "given the model" carries a lot of weight. A credible interval is a statement in the small world. If the likelihood is wrong, or the prior is absurd, or (most often) the causal assumptions that make the parameter meaningful are false, the interval is a precise answer to the wrong question. That is not a weakness peculiar to Bayesian methods. A frequentist confidence interval rests on the same model; it just states its conclusion differently.

Credible intervals and confidence intervals: what is the difference?

A 95% confidence interval comes from a procedure that, if the study were repeated many times, would produce intervals covering the true value 95% of the time. Any single interval either contains the truth or it doesn't; the 95% describes the procedure, not this interval. A 95% credible interval says that, given the model, prior and data, there is a 95% probability that the parameter lies inside it.

In large, well-behaved problems with weak priors the two kinds of interval are numerically almost identical, and the difference is one of interpretation. They differ in small samples, with strong priors, near boundaries (efficacy cannot exceed 100%), and when the question is a probability of a hypothesis, which only the Bayesian interval can answer directly. The FDA subgroup analyses in Chapter 2 show a case where they differ a great deal.

Two habits will serve you well. First, report the distribution, not just a point: a posterior median without an interval is a sentence with its verb missing. Second, prefer probabilities of substantive statements to tests against zero. "There is a 97% probability that the programme raised earnings by more than $500" is something a policymaker can use. "The effect is significantly different from zero" is not.

Research Practice: Reporting a Posterior

The Bayesian Analysis Reporting Guidelines (BARG; Kruschke, 2021) are the closest thing the field has to a standard checklist. For each key parameter or effect, a publishable report gives:

  • a central value and a credible interval, saying which kind (equal-tailed or highest-density) and what probability;
  • posterior probabilities for the substantive claims the paper makes, or for a region of practical equivalence around "no effect";
  • enough precision to be useful but not more than the simulation supports (two or three significant figures, not seven);
  • a figure of the posterior for the headline quantity, as above.

Check Yourself

A colleague writes: "The 95% credible interval for VE is (90.3%, 97.6%), so if we repeated the trial many times, 95% of such intervals would contain the true VE." What is wrong?

Key Takeaways

  • Summarise a posterior with a central value, a credible interval and probabilities of substantive statements.
  • Every posterior statement is conditional on the model, including its causal assumptions.

1.4 Drawing assumptions as a graph

The vaccine analysis was easy to interpret because of a coin toss. Most data are not like that. Most data come from observing the world as it is, where the things we want to compare differ in many ways at once. To reason about such data we need a precise language for causal assumptions. This course uses the directed acyclic graph, or DAG.

Definition: Directed Acyclic Graph (DAG)

A DAG is a set of variables (nodes) joined by arrows (directed edges), with no way to follow the arrows from a node back to itself. An arrow X→Y is a claim: if we could intervene and change X, Y would tend to change. The absence of an arrow is also a claim, and usually the stronger one: that X has no direct effect on Y.

We draw observed variables as solid circles and unobserved ones as dashed circles.

A DAG is a compact, checkable statement of a scientific theory. It is not a statistical model: it says nothing about the size or shape of any effect. It says only what can affect what. That turns out to be enough to decide which statistical model to fit, and that is the remarkable fact this course is built on.

An example: penguin bills

The palmerpenguins data (Horst, Hill and Gorman, 2020) record measurements of 344 penguins of three species (Adélie, Chinstrap and Gentoo) collected from 2007 to 2009 by Kristen Gorman at Palmer Station, Antarctica. Two of the measurements describe the bill: its length and its depth (top to bottom). Is a longer bill a shallower one?

R
library(palmerpenguins)
bills <- penguins |> drop_na(bill_length_mm, bill_depth_mm)

# one regression line through all 342 penguins
coef(lm(bill_depth_mm ~ bill_length_mm, data = bills))

# a separate line for each species
bills |>
  group_by(species) |>
  summarise(n = n(), slope = coef(lm(bill_depth_mm ~ bill_length_mm))[2])
output
   (Intercept) bill_length_mm 
      20.88547       -0.08502 
# A tibble: 3 x 3
  species       n slope
  <fct>     <int> <dbl>
1 Adelie      151 0.179
2 Chinstrap    68 0.222
3 Gentoo      123 0.205

Pooling all the penguins, each extra millimetre of bill length goes with a bill about 0.085 mm shallower. Within every one of the three species, each extra millimetre goes with a bill about 0.18 to 0.22 mm deeper. The sign flips. This is Simpson's paradox, and it is not a paradox at all once you draw the graph.

R
species_cols <- c(Adelie = harbour[["sky"]], Chinstrap = harbour[["amber"]],
                  Gentoo = harbour[["moss"]])
ggplot(bills, aes(bill_length_mm, bill_depth_mm)) +
  geom_point(aes(colour = species), alpha = 0.6, size = 1.6) +
  geom_smooth(method = "lm", formula = y ~ x, se = FALSE,
              colour = harbour[["muted"]], linetype = "dashed") +
  geom_smooth(aes(colour = species), method = "lm", formula = y ~ x, se = FALSE) +
  scale_colour_manual(values = species_cols) +
  labs(x = "bill length (mm)", y = "bill depth (mm)", colour = NULL)
Scatterplot of bill depth against bill length for 342 penguins, coloured by speciesScatterplot of bill depth against bill length for 342 penguins, coloured by species. Adelie penguins sit top left (short, deep bills), Gentoo bottom right (long, shallow bills) and Chinstrap top right. A dashed grey line through all the points slopes gently downward. Three solid lines, one per species, all slope upward. 15.0 17.5 20.0 40 50 60 bill length (mm) bill depth (mm) Adelie Chinstrap Gentoo
Bill depth against bill length. The dashed line ignores species and slopes down; the three solid lines, one per species, all slope up. Species differ in both measurements, and that difference dominates the pooled line.

Species affects both measurements. Gentoo penguins have long, shallow bills; Adélie penguins have short, deep ones. Within a species, bigger birds have bigger bills in every direction, so length and depth rise together. Here is that story as a graph:

SBLD
A causal sketch for penguin bills. Species S affects bill length L and bill depth D. Within a species, overall body size B (unmeasured, so dashed) affects both. Neither bill measurement causes the other.

There is no arrow between length and depth. Both associations, the negative pooled one and the positive within-species one, are produced by common causes. Pooling mixes the between-species pattern (driven by S) with the within-species pattern (driven by B). Separating by species removes the first and leaves the second. Neither line measures what would happen if you could stretch a penguin's bill.

Simpson's paradox in the penguin data

Show all penguins with one line, or condition on species to fit one line per species. The readout gives the slopes.

AdélieChinstrapGentoo

History: Arrows Before Algebra

The first causal diagrams in science were drawn by the American geneticist Sewall Wright in 1920 and 1921, to explain the coat colours of guinea pigs. His "path analysis" attached a number to each arrow and showed how correlations between variables could be decomposed along the paths connecting them. Statisticians largely ignored the method for half a century; economists and sociologists borrowed it as structural equation modelling (Chapter 13). The modern theory came from computer science. In the 1980s and 1990s Judea Pearl, with Peter Spirtes, Clark Glymour, Richard Scheines and others, worked out exactly what a graph implies about which variables are statistically independent, and when a causal effect can be computed from observational data. Pearl received the Turing Award for this work in 2011.

Check Yourself

In the penguin graph, which statement is a claim the graph makes?

Key Takeaways

  • A DAG states which variables can affect which. Missing arrows are claims too.
  • The same data can show opposite associations depending on what you condition on. Only a causal argument says which, if either, answers your question.

Lecture 2 — Graphs, priors and the workflow

1.5 The four elementary structures

Every DAG, however large, is built from four local patterns. Knowing how each behaves is most of what causal reasoning requires in practice. Let X and Y be two variables whose association we are studying, and Z a third.

Definition: Fork, Pipe, Collider, Descendant

  • Fork (common cause): X←Z→Y. Z affects both, so X and Y are associated even though neither affects the other. Conditioning on Z removes the association. This is confounding.
  • Pipe (mediator, or chain): X→Z→Y. Z carries the effect of X on to Y. Conditioning on Z blocks the effect: the model then reports only the part of X's effect that does not go through Z.
  • Collider (common effect): X→Z←Y. X and Y are not associated through Z, until you condition on Z. Conditioning creates an association between two unrelated causes.
  • Descendant: a variable D caused by Z. Conditioning on D partly does whatever conditioning on Z would do, because D carries information about Z.

The fork and the pipe behave identically in the data: in both, X and Y are associated, and the association disappears when you hold Z fixed. The collider is the opposite: no association until you hold Z fixed. The collider deserves special attention, because it is the structure that defeats intuition most reliably.

Suppose a city's restaurants survive if they are either genuinely good or cheaply located, and that food quality and rent are unrelated across all the restaurants that ever open. Among surviving restaurants, the only ones you can sample, a negative association appears: the expensive survivors must be good, and the mediocre survivors must be cheap. Survival is a collider, quality → survival ← cheap location, and sampling only survivors conditions on it. The association is created entirely by the selection. Explore all four structures in the widget.

What conditioning does to each structure

Each dot is a simulated unit. Choose a structure, then condition on Z: only units whose Z is close to the chosen value are highlighted, and the solid line is fitted to them alone. The dashed line uses everyone. Watch what happens to the association between X and Y.

The practical warning is blunt. "Control for more variables" is not a safe default. Conditioning on a variable is the right move for a fork and the wrong move for a collider, and only the graph, not the data, tells you which you are facing. The data look the same either way.

History: Berkson's Hospital, and a Pandemic Paradox

In 1946 the Mayo Clinic statistician Joseph Berkson pointed out that two diseases unrelated in the population can appear associated among hospital patients, because having either disease raises your chance of being admitted. Admission is a collider. Seventy-four years later the same mechanism struck again. Early COVID-19 studies, drawn from people who had been tested or hospitalised, reported that smokers seemed protected against severe disease. Griffith and colleagues (2020) showed how this could be pure collider bias: being tested or admitted depended both on smoking-related illness and on COVID-19 severity, so studying only those people manufactured a spurious protective association. The lesson is now a standard reviewer question: how did units get into your sample, and does that process depend on both the exposure and the outcome?

Check Yourself

A study of a job-training programme X on later earnings Y "controls for" the job the person held a year after training, Z. The programme helps people get better jobs, which pay more. What does conditioning on Z do?

Key Takeaways

  • Fork: condition to remove confounding. Pipe: conditioning removes the mediated effect. Collider: conditioning creates a spurious association. Descendant: a weaker version of conditioning on its parent.
  • Selection into a sample is conditioning. Ask how your units got into your data.

1.6 Paths, blocking and the back-door criterion

The four rules combine into one procedure for any graph. Between an exposure X and an outcome Y there may be many paths: sequences of arrows connecting them, followed in any direction. A path transmits association unless something blocks it.

Definition: Blocked Path (d-separation)

Given a set 𝐙 of variables we condition on, a path between X and Y is blocked if it contains

  • a fork or pipe node that is in 𝐙, or
  • a collider that is not in 𝐙 and has no descendant in 𝐙.

Otherwise the path is open. If every path between X and Y is blocked, X and Y are d-separated by 𝐙, and the graph implies that they are independent given 𝐙.

Paths come in two kinds. A causal path follows the arrows forward from X to Y; it carries the effect we want. A back-door path starts with an arrow pointing into X; it carries association that is not caused by X. The vaccine graph had no back-door paths because nothing pointed into V. That is precisely what randomisation buys.

Result: The Back-Door Criterion (Pearl, 1993)

A set of observed variables 𝐙 is sufficient to estimate the total causal effect of X on Y if

  1. no variable in 𝐙 is a descendant of X, and
  2. 𝐙 blocks every back-door path between X and Y.

Then a model of Y given X and 𝐙, averaged over the distribution of 𝐙, gives the effect of setting X by intervention. Chapter 13 proves this and develops what to do when no such set exists.

Condition 1 rules out mediators and their descendants (Section 1.5). Condition 2 closes the confounding. You do not have to trace paths by hand: the dagitty package (Textor et al., 2016) reads a graph and returns the sufficient adjustment sets. Here it is on a small example. Suppose we want the effect of exercise on blood pressure, where age affects both, and both affect whether a person enrolled in the study through a health-screening programme.

R
g <- dagitty("dag {
  Age -> Exercise
  Age -> BP
  Exercise -> BP
  Exercise -> Enrolled
  BP -> Enrolled
}")
adjustmentSets(g, exposure = "Exercise", outcome = "BP")
isAdjustmentSet(g, c("Age", "Enrolled"), exposure = "Exercise", outcome = "BP")
output
{ Age }
[1] FALSE

The answer is to adjust for age and nothing else. Adding enrolment status, a collider, is declared invalid, and the data could never have told you that.

It is worth pausing on what has just happened, because it is the organising principle of the whole course. The back-door criterion is a statement about which variables enter the regression, and it was settled before any Bayesian machinery appeared. The causal layer chooses the model; the Bayesian layer fits it and quantifies uncertainty. Reversing the order, fitting first and interpreting causally later, is the most common way good computation produces a wrong conclusion. Collecting more data or running a longer chain cannot repair an adjustment set chosen by habit.

Research Practice: Put the DAG in the Paper

Epidemiology journals increasingly expect causal diagrams, and reviewers in economics, psychology and the social sciences increasingly ask for them. A review of 234 health-research articles that used DAGs (Tennant et al., 2021) found that more than a third did not show the graph at all, and that only 6% of those that did cited any support for its arrows. Do better:

  • show the whole DAG as a figure, including unmeasured variables;
  • justify each arrow, and each missing arrow that matters, with a citation or argument (a supplementary table works well);
  • state the estimand (total or direct effect, for whom) and the adjustment set the graph implies;
  • share the dagitty code so readers can test alternatives.

Check Yourself

In the exercise example, why is adjusting for Age needed?

1.7 Priors are assumptions, so simulate them

Now we combine the two halves in a complete analysis. The question is practical. Weighing a penguin in the field means catching it and holding it on a scale in a bag, which is stressful for the bird and awkward for the researcher. Measuring a flipper with calipers is quick. How well can we predict a penguin's body mass from its flipper length?

First the graph. Flipper length does not cause body mass: stretching a penguin's flipper will not make it heavier. Both are consequences of the bird's overall size, which depends on its species and its sex.

SXBFM
A causal sketch for penguin body mass M and flipper length F. Overall size B (unmeasured, so dashed) drives both. Species S and sex X affect size, and species also affects flipper length directly: Gentoo penguins have long flippers for their weight.

For prediction we do not need an arrow from F to M. Flipper length is useful because it is a descendant of size: it carries information about B, which drives M. The graph also warns us that species and sex will matter, and that a model ignoring them will mix several different relationships. We will start with the simplest model anyway, and let the checks tell us what is missing.

Measuring mass Mi in kilograms and flipper length Fi in millimetres, centred at 200 mm, the first model is a straight line with normal scatter:

Mi∼Normal(μi,σ)
μi=α+β(Fi−200)

Now we need priors for α (the expected mass of a penguin with 200 mm flippers), β (the change in expected mass per millimetre of flipper) and σ (the scatter). It is tempting to use "uninformative" priors, such as Normal(0, 10) for both. Before using any prior, we should look at what it says.

Definition: Prior Predictive Simulation

To see what a prior implies, draw parameters from the prior, push them through the model, and look at the data or relationships the model considers plausible before seeing any data. If they are absurd, the prior is absurd, however harmless it looked on the parameter scale.

R
set.seed(11)
n_lines <- 50
prior_lines <- tibble(
  prior = rep(c("vague: Normal(0, 10) on both",
                "weakly informative: Normal(4, 1), Normal(0, 0.05)"), each = n_lines),
  alpha = c(rnorm(n_lines, 0, 10), rnorm(n_lines, 4, 1)),
  beta  = c(rnorm(n_lines, 0, 10), rnorm(n_lines, 0, 0.05))
)

ggplot(prior_lines) +
  geom_segment(aes(x = 170, xend = 235,
                   y = alpha + beta * (170 - 200), yend = alpha + beta * (235 - 200)),
               colour = harbour[["sky"]], alpha = 0.45) +
  geom_hline(yintercept = c(0, 22), linetype = "dashed", colour = harbour[["rose"]]) +
  facet_wrap(~ prior) +
  coord_cartesian(ylim = c(-15, 35)) +
  labs(x = "flipper length (mm)", y = "body mass (kg)")

# share of prior lines that imply an impossible penguin somewhere in 170-235 mm
prior_lines |>
  mutate(lo = pmin(alpha - 30 * beta, alpha + 35 * beta),
         hi = pmax(alpha - 30 * beta, alpha + 35 * beta)) |>
  group_by(prior) |>
  summarise(impossible = mean(lo < 0 | hi > 22))
output
# A tibble: 2 x 2
  prior                                             impossible
  <chr>                                                  <dbl>
1 vague: Normal(0, 10) on both                            1   
2 weakly informative: Normal(4, 1), Normal(0, 0.05)       0.02
Two panels of 50 straight lines each, showing body mass in kilograms against flipper lengtTwo panels of 50 straight lines each, showing body mass in kilograms against flipper length from 170 to 235 millimetres. In the left panel, labelled vague, the lines fan out wildly, most leaving the plot, many implying negative masses. In the right panel, labelled weakly informative, the lines cluster between about 0 and 10 kilograms with modest slopes. Dashed lines mark 0 kilograms and 22 kilograms. vague: Normal(0, 10) on both weakly informative: Normal(4, 1), Normal(0, 0.05) 180 200 220 180 200 220 -10 0 10 20 30 flipper length (mm) body mass (kg)
Prior predictive regression lines. Left: vague priors imply penguins weighing hundreds of kilograms or less than nothing. Right: weakly informative priors allow anything a biologist would call possible, and little else. Dashed lines: 0 kg and 22 kg (an emperor penguin, the heaviest living species).

Under the vague priors, every one of the 50 lines implies penguins that weigh less than nothing, or more than any bird alive, somewhere within the range of real flipper lengths. Under the weakly informative priors, one line in fifty does. The weakly informative priors were not chosen by looking at these data. They came from things anyone could look up: penguins of these species weigh a few kilograms (so α∼Normal(4,1)), and across the 60 mm range of flipper lengths the mass is unlikely to change by more than a few kilograms (so β∼Normal(0,0.05), allowing slopes up to about ±0.1 kg per mm). Notice that we did not even assume the slope is positive.

Experiment with the priors yourself.

Prior predictive lines for the penguin model

Each line is one draw of (α,β) from the prior, plotted as μ=α+β(F−200). The shaded band is what a biologist would call possible (0 to 22 kg). Adjust the prior standard deviations and see how many lines leave it.

Warning: "Uninformative" Is Not Neutral

A very wide prior is not an absence of assumptions. It is the assumption that absurd values are as plausible as sensible ones. In small samples and in models with many parameters (Chapters 6 and 10), wide priors actively harm inference: they let the model chase noise, and they can make the sampler fail. Weakly informative priors regularise, ruling out the absurd while letting the data speak about everything else. Chapter 2 shows how to choose them in a principled way.

Check Yourself

Under the prior β∼Normal(0,0.05), a slope two standard deviations above zero is 0.1 kg per mm. How many kilograms would expected mass change across the 30 mm from 185 mm to 215 mm flippers at that slope?

kg

1.8 Fit, diagnose, check, revise

Fit

With the graph drawn and the priors simulated, we fit the model. The brms package translates a model formula into a program in the probabilistic language Stan, compiles it, and draws samples from the posterior using Hamiltonian Monte Carlo (Chapter 5). Treat the sampler as a black box for now; we will open it in Chapter 5.

R
pen <- penguins |>
  drop_na(body_mass_g, flipper_length_mm, sex) |>
  mutate(mass = body_mass_g / 1000,              # kilograms
         flipper = flipper_length_mm - 200)      # mm, centred at 200

m1 <- brm(
  mass ~ 1 + flipper, data = pen, family = gaussian(),
  prior = c(prior(normal(4, 1), class = Intercept),
            prior(normal(0, 0.05), class = b),
            prior(exponential(1), class = sigma)),
  seed = 1, refresh = 0, file = "fits/ch1_m1"     # 'file' caches the fitted model
)
summary(m1)
output
 Family: gaussian 
  Links: mu = identity; sigma = identity 
Formula: mass ~ 1 + flipper 
   Data: pen (Number of observations: 333) 
  Draws: 4 chains, each with iter = 2000; warmup = 1000; thin = 1;
         total post-warmup draws = 4000

Population-Level Effects: 
          Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS Tail_ESS
Intercept     4.16      0.02     4.12     4.20 1.00     2006     1767
flipper       0.05      0.00     0.05     0.05 1.00     5705     3167

Family Specific Parameters: 
      Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS Tail_ESS
sigma     0.39      0.02     0.36     0.43 1.00     2157     2134

Draws were sampled using sampling(NUTS). For each parameter, Bulk_ESS
and Tail_ESS are effective sample size measures, and Rhat is the potential
scale reduction factor on split chains (at convergence, Rhat = 1).

Read the table from the bottom up. sigma, the typical deviation of a penguin from the line, is about 0.39 kg. The Intercept says a penguin with 200 mm flippers is expected to weigh 4.16 kg (95% interval 4.12 to 4.20). The flipper coefficient says expected mass rises about 0.05 kg, or 50 g, for each millimetre of flipper.

Diagnose

Before believing those numbers, check that the computation worked. Three columns of the summary do that job.

Here every Rhat is 1.00 and every ESS is in the thousands, so the computation is sound. An algorithm that fails these checks has not given you a slightly worse answer. It has given you no answer.

Research Practice: What to Report About Computation

Following Vehtari et al. (2021) and the BARG, a methods section should state: the software and version (here brms 2.20 with Stan 2.32), the number of chains, warm-up and sampling iterations, that all R^<1.01, the minimum bulk and tail ESS (at least 400 is a common requirement), and the number of divergent transitions (ideally zero). Put the full model code and the random seed in a public repository. Reviewers increasingly check.

Check

The computation worked. Does the model? A posterior predictive check draws parameters from the posterior, simulates new datasets, and compares them with the data actually observed. If the model is a good small world, its simulated penguins should look like the real ones in every way we care about. Our graph told us that species and sex matter, so a natural check is: does the model reproduce the average mass of each species and sex?

We also fit the model the graph suggests, adding species and sex, so that the check can compare the two.

R
m2 <- brm(
  mass ~ 1 + flipper + species + sex, data = pen, family = gaussian(),
  prior = c(prior(normal(4, 1), class = Intercept),
            prior(normal(0, 1), class = b),
            prior(normal(0, 0.05), class = b, coef = flipper),
            prior(exponential(1), class = sigma)),
  seed = 1, refresh = 0, file = "fits/ch1_m2"
)
round(fixef(m2), 3)
output
                 Estimate Est.Error   Q2.5 Q97.5
Intercept           3.643     0.047  3.548 3.732
flipper             0.020     0.003  0.014 0.026
speciesChinstrap   -0.089     0.046 -0.178 0.001
speciesGentoo       0.829     0.084  0.667 0.997
sexmale             0.528     0.038  0.452 0.602
R
group_means <- function(fit) {
  yrep <- posterior_predict(fit, ndraws = 500)      # 500 simulated datasets
  grp  <- paste(pen$species, pen$sex)
  sims <- apply(yrep, 1, function(y) tapply(y, grp, mean))
  tibble(group = rownames(sims),
         lo = apply(sims, 1, quantile, 0.05),
         hi = apply(sims, 1, quantile, 0.95))
}
set.seed(3)
checks <- bind_rows("flipper only" = group_means(m1),
                    "flipper + species + sex" = group_means(m2), .id = "model")
observed <- pen |> group_by(group = paste(species, sex)) |> summarise(mass = mean(mass))

ggplot(checks, aes(y = group)) +
  geom_linerange(aes(xmin = lo, xmax = hi, colour = model), linewidth = 3,
                 position = position_dodge(width = 0.55)) +
  geom_point(data = observed, aes(x = mass), shape = 18, size = 3.6,
             colour = harbour[["ink"]]) +
  scale_colour_manual(values = c("flipper only" = harbour[["amber"]],
                                 "flipper + species + sex" = harbour[["sky"]])) +
  labs(x = "average body mass (kg)", y = NULL, colour = NULL)

# the same comparison as a table
checks |>
  left_join(observed, by = "group") |>
  mutate(inside = mass >= lo & mass <= hi) |>
  select(model, group, observed = mass, lo, hi, inside) |>
  arrange(group, model) |>
  print(n = 12, width = 82)
output
# A tibble: 12 x 6
   model                   group            observed    lo    hi inside
   <chr>                   <chr>               <dbl> <dbl> <dbl> <lgl> 
 1 flipper + species + sex Adelie female        3.37  3.32  3.47 TRUE  
 2 flipper only            Adelie female        3.37  3.47  3.64 FALSE 
 3 flipper + species + sex Adelie male          4.04  3.94  4.09 TRUE  
 4 flipper only            Adelie male          4.04  3.69  3.88 FALSE 
 5 flipper + species + sex Chinstrap female     3.53  3.29  3.50 FALSE 
 6 flipper only            Chinstrap female     3.53  3.63  3.88 FALSE 
 7 flipper + species + sex Chinstrap male       3.94  3.98  4.18 FALSE 
 8 flipper only            Chinstrap male       3.94  4.05  4.27 FALSE 
 9 flipper + species + sex Gentoo female        4.68  4.65  4.82 TRUE  
10 flipper only            Gentoo female        4.68  4.70  4.89 FALSE 
11 flipper + species + sex Gentoo male          5.48  5.36  5.52 TRUE  
12 flipper only            Gentoo male          5.48  5.13  5.34 FALSE 
Six rows, one for each species and sexSix rows, one for each species and sex. For each, an amber bar shows the range of average masses predicted by the flipper-only model and a blue bar shows the range predicted by the model with species and sex. A black diamond marks the observed average. The diamonds fall outside every amber bar but inside or very close to most blue bars. Adelie female Adelie male Chinstrap female Chinstrap male Gentoo female Gentoo male 3.5 4.0 4.5 5.0 5.5 average body mass (kg) flipper + species + sex flipper only
Posterior predictive check of group means. Bars: the middle 90% of the average mass in 500 simulated datasets from each model. Diamonds: the observed averages. The flipper-only model (amber) misses all six groups; the model with species and sex (blue) fits four of six and narrowly misses both Chinstrap groups.

The flipper-only model fails the check badly. It predicts that female Adélies average 3.47 to 3.64 kg (they actually average 3.37 kg), that male Adélies average 3.69 to 3.88 kg (they average 4.04 kg), and that male Gentoos average 5.13 to 5.34 kg (they average 5.48 kg). Look at the pattern: every female group is over-predicted, and the heavier Adélie and Gentoo males are under-predicted. The model is not merely noisy; it is systematically wrong in exactly the way the graph warned, because it has no way to know that males are heavier than females with the same flippers. The model with species and sex fits four of the six groups, and its residual misses are informative: it over-predicts Chinstrap males and under-predicts Chinstrap females, which suggests that the sex difference in mass is smaller in Chinstraps than in the other species. That calls for a species-by-sex interaction, and the loop goes round again.

Adding species and sex also changes the flipper coefficient, from 0.050 to about 0.020 kg per mm. Within a species and sex, flipper length is a weaker guide to mass than across all penguins, because much of the pooled relationship was between-group differences (the same mechanism as Simpson's paradox in Section 1.4). Neither number is "the effect of flippers on mass"; the graph told us there is no such effect. They are two different predictive summaries, answering different questions.

For prediction, we can also ask directly which model would predict new penguins better. Approximate leave-one-out cross-validation, computed by the loo package from the posterior draws without refitting the model, estimates each model's expected log predictive density for new data (Section 9.7 explains the importance-sampling idea behind it):

R
loo_compare(loo(m1), loo(m2))
output
   elpd_diff se_diff
m2   0.0       0.0  
m1 -93.6      11.2  

The richer model is better by about 94 units of expected log predictive density, with a standard error of 11: an overwhelming difference.

Check Yourself

A model's summary shows Rhat = 1.00 and Bulk_ESS = 3,000 for every parameter, but its posterior predictive check shows it cannot reproduce the data. What should you conclude?

Key Takeaways

  • Simulate priors before fitting. Choose priors that rule out the absurd, using knowledge you could have had without these data.
  • Diagnose computation with R^, ESS and divergences. Check the model with posterior predictive checks aimed at what the graph says matters.
  • A failed check sends you back to the model, not on to the conclusions.

1.9 The workflow loop, and graphs that are not small

Putting the pieces together gives the loop below, which follows the "Bayesian workflow" of Gelman et al. (2020). It is a loop, not a pipeline: a failed check sends you back to the graph or the likelihood, never onward to the write-up.

1 · draw the DAG2 · likelihood, priors3 · prior predictive 4 · fit5 · diagnose6 · posterior check7 · critique revise
The causal–Bayesian workflow. Steps 1–3 are construction (before the data are used), 4–5 are computation, 6–7 are criticism. Failure at 5 or 6 returns you to the model, not to the conclusions.

Three principles travel with the loop. Build models you understand: start simple and add structure when a check demands it, as we did with species and sex. Computation is part of the model: a sampler that fails to converge has given you no answer. And criticism is not optional: the posterior predictive check is where the small world is held up against the large world, and skipping it skips the only honest test the method offers.

When the graph is not small

The graphs so far have three to five nodes. That is deliberate: a small graph isolates one structure so that its rule can be seen. But a DAG drawn for a question somebody actually needs answered routinely has ten or twenty nodes, several of them unobserved, mixing all four structures. The reasoning does not change; what changes is that you can no longer trace the paths in your head.

Here is a causal sketch for a real research question: do New York City neighbourhoods receive equitable emergency medical service? Specifically, does a neighbourhood's racial and ethnic composition R affect the clinical outcome O of residents after a 911 call, and does it do so by way of ambulance response time RT? The graph encodes that hypothesis and every rival explanation a sceptical reviewer would raise.

Paths and blocking in a ten-variable graph

Click an observed node to condition on it (click again to release it). The list shows every path between R and O: causal paths should stay open, and non-causal paths should be blocked. L is unobserved and cannot be conditioned on. Variables: R composition (exposure), O outcome, RT response time, E socioeconomic status, W distance to nearest available unit, P population density, K clinical acuity, G traffic, V ambulance availability, L structural history (redlining, disinvestment).

Read the graph as a set of claims. The latent node L is the analyst's honesty made visible. Historical segregation and disinvestment shaped, together, the racial composition of neighbourhoods (L→R), their present wealth (L→E), and the siting of emergency infrastructure (L→W). Population density P drives traffic, distance to units and how busy ambulances are. Socioeconomic status E shapes how severe calls are, through baseline health, and affects outcomes directly. Response time is produced by four operational variables and, possibly, by the exposure itself: the arrow R→RT is the disparity the project exists to detect.

The target is the total effect of R on O, which flows along the causal paths from R through RT to O. Before reaching for software, try to find an adjustment set by hand. Four open back-door paths leave R through the latent L: one through wealth (R←L→E→O), two through wealth and call acuity (R←L→E→K→O and R←L→E→K→RT→O), and one through where stations were built (R←L→W→RT→O). Conditioning on E blocks the first three and conditioning on W blocks the fourth, so {E,W} looks like the answer. Press condition on {E, W} in the widget.

It fails. Distance to the nearest unit has two parents, structural history and population density: L→W←P. So W is a collider on the path R←L→W←P→G→RT→O, and conditioning on it opens that path (and a similar one through V). Blocking one path opened two others. This is exactly the kind of mistake that is easy to make by hand and impossible to make with software:

R
ems <- dagitty("dag {
  L [latent]
  L -> R   L -> E   L -> W
  P -> W   P -> G   P -> V
  E -> K   E -> O
  R -> RT
  W -> RT  K -> RT  G -> RT  V -> RT
  K -> O   RT -> O
}")
adjustmentSets(ems, exposure = "R", outcome = "O")
isAdjustmentSet(ems, c("E", "W"), exposure = "R", outcome = "O")
isAdjustmentSet(ems, c("E", "W", "P", "K", "G", "V", "RT"), exposure = "R", outcome = "O")
output
{ E, G, V, W }
{ E, P, W }
[1] FALSE
[1] FALSE

There are two minimal sufficient adjustment sets: {E,P,W} (also condition on density, which closes the opened paths at their source) and {E,G,V,W} (condition on traffic and availability instead). Which to use is a practical question: population density is measured precisely from census data, while traffic and ambulance availability at the moment of each call are harder to obtain. Neither set is the one that "control for everything available" would produce. That habit would condition on response time, the mediator, which erases the very effect under study, and because RT is a collider with five parents, it would open still more non-causal paths. Try it in the widget. The graph turns a vague instinct into a precise instruction, and the instruction is different from both the instinct and the first careful guess.

One feature of the graph carries the deepest lesson. The set {E,P,W} works only if structural history L affects outcomes through no other route. Suppose it also raises residents' chronic exposure to environmental hazards, and so directly affects health: an arrow L→O. Press add arrow L → O in the widget. A back-door path R←L→O opens that no observed variable can block, because L is unobserved.

R
ems_lo <- dagitty("dag {
  L [latent]
  L -> R   L -> E   L -> W   L -> O
  P -> W   P -> G   P -> V
  E -> K   E -> O
  R -> RT
  W -> RT  K -> RT  G -> RT  V -> RT
  K -> O   RT -> O
}")
adjustmentSets(ems_lo, exposure = "R", outcome = "O")   # prints nothing: no valid set
length(adjustmentSets(ems_lo, exposure = "R", outcome = "O"))
output
[1] 0

No adjustment set exists. The total effect is then not identified from these observational data, however large the sample. The right response is not to delete the arrow so that the graph looks solvable. It is to draw the arrow if you believe it belongs, and to say honestly that the effect can only be bounded, or reached through other designs such as instruments, natural experiments or sensitivity analysis (Chapter 13). A graph is allowed to tell you that your question cannot be answered with the data in hand. That message, delivered before any model is fitted, is one of the most valuable things the causal layer produces, and no Bayesian computation downstream can substitute for it.

Project Connection: EMS Equity

For a publishable version of this study, the graph above is the first figure of the paper. Each arrow needs a source (the literature on redlining and health, NYC Fire Department operational reports on dispatch), and the arrow L→O needs an explicit argument. Expect a reviewer to propose it. A strong paper estimates the effect under {E,P,W}, checks that {E,G,V,W} gives a similar answer (two valid adjustment sets that disagree are evidence against the graph), then reports a sensitivity analysis asking how strong an unmeasured L→O path would have to be to explain the result away (Chapter 13 shows how).

Research Practice: Reproducible from the First Line

Every analysis in these notes is a script that runs from top to bottom: data are read from a stable source, random seeds are set, and fitted models are cached with file =. For a paper, also record package versions (sessionInfo(), or better, an renv lock file), keep raw data read-only and do all cleaning in code, and deposit the code and data (or instructions to obtain them) in a public archive with a DOI, such as Zenodo or OSF. A reader who cannot rerun your analysis cannot fully trust it, and increasingly neither can an editor.

Check Yourself

Why is W (distance to the nearest unit) in both adjustment sets, but RT (response time) in neither?

Key Takeaways

  • The workflow is a loop: DAG → likelihood and priors → prior check → fit → diagnose → posterior check → critique, and back again.
  • For real graphs, let software (dagitty) find adjustment sets: a variable can block one path and open another. The judgement about the graph remains yours.
  • A DAG can tell you an effect is not identified. Report that; do not hide it.

1.10 Exercises

Each exercise has a hint and an answer button. Try the exercise first, open the hint if you are stuck, and look at the answer only to check your own.

Exercises

  1. A smaller trial. A vaccine trial with equal arms observes 3 cases in the vaccine arm and 17 in the placebo arm. Using the protocol prior Beta(0.700102, 1), (a) write down the posterior for θ; (b) use R to compute the posterior median and 95% equal-tailed interval for VE; (c) compute Pr(VE > 30% | data). Would this trial have met the 98.6% success criterion?

    Adapt the vax_post code with y <- 3; n <- 20. For (c), use pbeta(0.7 / 1.7, a_post, b_post).

    (a) Beta(3.700102, 18). (b) The posterior median VE is about 80.9%, with a 95% interval from about 46.1% to 95.0%. (c) Pr(VE > 30% | data) = pbeta(0.7/1.7, 3.700102, 18) = 0.994, which exceeds 0.986, so the criterion would be met. But the interval is wide: twenty cases establish that the vaccine works, not how well.

  2. Prior sensitivity. Repeat Exercise 1 with a flat prior Beta(1, 1) and with the sceptical prior Beta(20, 20). Report the posterior median VE under each. Which prior moves the answer most, and why?

    Change a_prior and b_prior. The sceptical prior is worth 40 observations centred on θ=1/2, that is, VE = 0.

    Flat prior: median VE about 79.2% (95% interval 43% to 94%), almost the same as the protocol prior, because both priors are worth fewer than two observations. Sceptical prior: median about 38.2% (95% interval −3% to 64%), and Pr(VE > 30%) falls to 0.68, because the prior's 40 pseudo-cases, split evenly between the arms, outweigh the 20 real cases. With small samples, the weight of the prior relative to the data is what matters.

  3. Name the structure. For each, name the structure (fork, pipe, collider) linking X and Y through Z, and say whether you should condition on Z to estimate the total effect of X on Y: (a) X = years of education, Z = occupation, Y = income; (b) X = ice-cream sales, Z = temperature, Y = drownings; (c) X = a student's maths aptitude, Z = admitted to an elite university, Y = artistic talent (admission rewards either).

    Ask in each case: which way do the arrows between Z and the other two variables point?

    (a) Pipe, education → occupation → income: do not condition (that would remove the mediated effect). (b) Fork, sales ← temperature → drownings: condition on temperature (and you will find no effect of ice cream). (c) Collider, aptitude → admission ← talent: do not condition; among admitted students the two will look negatively associated.

  4. dagitty practice. Write the following graph in dagitty and find the adjustment sets for the effect of A on Y: C→A, C→Y, A→M→Y, A→Y, U→M, U→Y with U latent. Then check whether {C,M} is a valid adjustment set.

    Use U [latent] inside the graph string, then adjustmentSets() and isAdjustmentSet().

    adjustmentSets returns { C }. isAdjustmentSet(g, c("C","M"), "A", "Y") is FALSE: M is a descendant of A (a mediator), and it is also a collider on A→M←U→Y, so conditioning on it would open that path.

  5. Selection into the sample. A university studies only students who hold its scholarships, which are awarded for high grades or for financial need (or both). Among scholarship holders, students from wealthier families have higher grades. Draw a DAG and explain how this association could arise even if family wealth and grades are unrelated among all students.

    Holding a scholarship is a selection variable. What points into it? Studying only holders means conditioning on it.

    The graph is grades → scholarship ← need, with need determined by (low) wealth: wealth → need. Scholarship is a collider. Among holders, a student from a wealthy family (low need) can only have qualified through high grades, while a poorer student may have qualified through need alone. So among holders, wealth and grades are positively associated, even if they are independent in the whole student body. The association is created by the selection, not by any effect of wealth on grades.

  6. Prior predictive in R. Using the prior predictive code in Section 1.7, try β∼Normal(0,0.5) and β∼Lognormal(−3,0.5) (which forces a positive slope). For each, report the share of 1,000 simulated lines that imply an impossible penguin. Which would you use, and why?

    Replace the rnorm call for beta with rlnorm(n_lines, -3, 0.5), and set n_lines <- 1000.

    With α∼Normal(4,1) and 1,000 lines: Normal(0, 0.5) for β gives about 80% impossible lines, so it is still far too wide. Lognormal(−3, 0.5), whose median is e−3≈0.05 kg per mm, gives about 5%, almost all from the uncertainty in α (Normal(0, 0.05) gives about 4% with 1,000 lines). If you are confident that larger penguins weigh more, the lognormal prior encodes that honestly; if you want the data alone to establish the sign, keep Normal(0, 0.05). Either is defensible if it is stated and justified.

  7. A species-by-sex model. The posterior predictive check in Section 1.8 suggested that the sex difference in mass differs by species. Fit mass ~ flipper + species * sex with the same priors as m2, repeat the group-means check, and compare the three models with loo_compare. Does the interaction fix the Chinstrap misfit?

    Change the formula, give the fit a new file name, and pass the new fit to group_means().

    All six observed group means now fall inside the model's 90% predictive intervals, including both Chinstrap groups. The speciesChinstrap:sexmale coefficient is about −0.33 kg (95% interval −0.50 to −0.16): the sex difference in mass is about 0.58 kg in Adélies and Gentoos but only about 0.24 kg in Chinstraps. loo_compare prefers the interaction model to m2 by about 8 units of elpd (standard error about 5): a real but modest improvement, much smaller than the jump from m1 to m2.

  8. Read a reported result. A paper reports: "Treatment reduced symptoms (β = −0.42, 95% CI [−0.80, −0.04], p = 0.03), controlling for age, baseline symptoms and adherence." Adherence was measured during treatment. Identify one causal problem and one reporting problem, and rewrite the sentence as a Bayesian analysis following the BARG.

    Is adherence measured before or after treatment begins? What is missing from the report of the uncertainty?

    Causal problem: adherence is post-treatment, likely a mediator (and possibly a collider with unmeasured motivation), so conditioning on it biases the total effect. Reporting problem: no statement of the estimand, the model or the priors, and a dichotomous significance claim. A better sentence: "The total effect of treatment on symptoms (adjusting for age and baseline symptoms, as implied by the DAG in Figure 1) had posterior median −0.45 (95% CrI −0.82 to −0.08) on the symptom scale; the posterior probability of a reduction larger than the minimal important difference of 0.2 was 0.91. Priors and sensitivity analyses are in the supplement."

Further reading and references

Further Reading

  • McElreath, Statistical Rethinking (2nd ed., 2020), Chapters 1–2 (small worlds, Bayesian updating) and 5–6 (DAGs, the four structures, back-door paths). The most readable introduction to this way of working.
  • Gelman et al., "Bayesian Workflow" (2020). The definitive account of the loop in Section 1.9, with many worked failures.
  • Pearl, Glymour and Jewell, Causal Inference in Statistics: A Primer (2016). A short, rigorous treatment of d-separation and the back-door criterion.
  • Kruschke, "Bayesian Analysis Reporting Guidelines" (2021). Print it and keep it next to your keyboard.
  1. Berkson, J. (1946). Limitations of the application of fourfold table analysis to hospital data. Biometrics Bulletin, 2(3), 47–53.
  2. FDA (2020). Vaccines and Related Biological Products Advisory Committee Briefing Document: Pfizer-BioNTech COVID-19 Vaccine, 10 December 2020.
  3. Gelman, A., Vehtari, A., Simpson, D., et al. (2020). Bayesian workflow. arXiv:2011.01808.
  4. Griffith, G. J., Morris, T. T., Tudball, M. J., et al. (2020). Collider bias undermines our understanding of COVID-19 disease risk and severity. Nature Communications, 11, 5749.
  5. Horst, A. M., Hill, A. P. and Gorman, K. B. (2020). palmerpenguins: Palmer Archipelago (Antarctica) penguin data. R package. Data originally from Gorman, Williams and Fraser (2014), PLoS ONE 9(3), e90081.
  6. Kruschke, J. K. (2021). Bayesian analysis reporting guidelines. Nature Human Behaviour, 5, 1282–1291.
  7. McElreath, R. (2020). Statistical Rethinking: A Bayesian Course with Examples in R and Stan (2nd ed.). CRC Press.
  8. Pearl, J. (1993). Comment: graphical models, causality and intervention. Statistical Science, 8(3), 266–269.
  9. Pearl, J. (2009). Causality: Models, Reasoning, and Inference (2nd ed.). Cambridge University Press.
  10. Polack, F. P., Thomas, S. J., Kitchin, N., et al. (2020). Safety and efficacy of the BNT162b2 mRNA Covid-19 vaccine. New England Journal of Medicine, 383, 2603–2615.
  11. Tennant, P. W. G., Murray, E. J., Arnold, K. F., et al. (2021). Use of directed acyclic graphs (DAGs) to identify confounders in applied health research: review and recommendations. International Journal of Epidemiology, 50(2), 620–632.
  12. Textor, J., van der Zander, B., Gilthorpe, M. S., Liśkiewicz, M. and Ellison, G. T. H. (2016). Robust causal inference using directed acyclic graphs: the R package 'dagitty'. International Journal of Epidemiology, 45(6), 1887–1894.
  13. Vehtari, A., Gelman, A., Simpson, D., Carpenter, B. and Bürkner, P.-C. (2021). Rank-normalization, folding, and localization: an improved R^ for assessing convergence of MCMC. Bayesian Analysis, 16(2), 667–718.
  14. Wright, S. (1921). Correlation and causation. Journal of Agricultural Research, 20(7), 557–585.