Randomised Controlled Trials

The gold standard of causal inference

Merlin Schaeffer · Department of Sociology

2026-10-26

 

Source: Polack, Thomas, Kitchin, Absalon, Gurtman, Lockhart, Perez, Pérez Marc, Moreira, Zerbini, Bailey, Swanson, Roychoudhury, Koury, Li, Kalina, Cooper, Frenck, Hammitt, Türeci, Nell, Schaefer, Ünal, Tresnan, Mather, Dormitzer, Şahin, Jansen, and Gruber (2020)

How do we know it works?

Goal of empirical sociology

Use data to discover patterns,
and the social mechanisms that bring them about.

By the end of today you can …

  1. see how randomisation fixes selection bias;

  2. estimate a causal effect from a real experiment using OLS in R.

The research question of the day

The integration paradox: immigrants and their descendants who are better integrated (more educated, longer resident) often report more discrimination, not less (Schaeffer and Kas, 2024).

One proposed mechanism: the better integrated consume more news, become more aware of discrimination, and so report it more often.

Research question: does consuming news increase how often immigrant minorities report discrimination?

280 estimates from 42 studies. Source: (Schaeffer and Kas, 2024)

Recap: why naïve comparisons mislead

Preparation

pacman::p_load( # Load several R packages using the pacman package manager
  tidyverse,  # A collection of packages for data manipulation and visualization
  ggplot2,    # Powerful package for creating static, animated and interactive visualizations
  estimatr,   # Package for fast estimators for regression with weighted data
  modelr,     # Provides functions for modelling and prediction
  kableExtra, # Enhances table creation in R
  modelsummary) # Creates tables and plots to summarise statistical models
  • Schaeffer, Kas, and Hagedorn (2023)
  • 1093 immigrants and children of immigrants.
  • Berlin, Hamburg, Munich, Frankfurt, Cologne.
  • Interviewed August 2021.
  • Financed by the German Research Council (DFG)
  1. On a typical day, about how much time do you spend watching, reading, or listening to news about politics and current affairs? Please give your answer in hours and then minutes.

  2. How often were you personally discriminated against in the following situations here in Germany? Discrimination means a person is treated worse than others, with no factual justification — e.g. insult, ostracism, harassment. Disadvantaging rules and laws also count. … looking for work  ·  at work  ·  in education
    … looking for housing  ·  with officials  ·  in public

    1. Never (2) Rarely (3) Sometimes (4) Often (5) Very often
load("APAD.RData") # Load APAD dataset
APAD <- APAD %>% # Process the APAD data
  mutate(
    # News consumption in minutes (e.g. 2h 30m -> 150)
    news = news_hrs * 60 + news_mins,
    # Binary: reads >= 15 minutes of news per day?
    news_yn = case_when(
      news <  15 ~ 0,
      news >= 15 ~ 1,
      TRUE       ~ as.numeric(NA)),
    # Average perceived discrimination across 6 domains
    dis_index = rowMeans(
      select(., dis_trainee, dis_job, dis_school,
             dis_house, dis_gov, dis_public),
      na.rm = TRUE),
    # z-standardised version (mean 0, SD 1)
    z_dis_index = scale(dis_index) %>% as.numeric())


# # A tibble: 1,093 × 24
#    antidiscr_law dis_index news_hrs news_mins dis_trainee dis_job dis_school dis_house dis_gov dis_public gewFAKT
#            <dbl>     <dbl>    <dbl>     <dbl>       <dbl>   <dbl>      <dbl>     <dbl>   <dbl>      <dbl>   <dbl>
#  1             5      3.17        1         0           3       3          4         5       2          2   0.105
#  2             4      1.8         1         1           5      NA          1         1       1          1   0.103
#  3             3      2           0        30           1       2          1         3       3          2   0.802
#  4             5      2.83        0        10           3       3          2         5       1          3   0.104
#  5             4      3.2         0        30           2       3         NA         5       3          3   0.639
#  6             5      2.75        2         0          NA       2         NA         4       3          2   0.2  
#  7             2      1           3        58           1       1          1         1       1          1   2.42 
#  8             4      1.67        5         0           3       1          1         3       1          1   0.102
#  9             5      4           1        15           4       4          4         4       4          4   6.19 
# 10             4      1.33        0        30           1       2          1         1       1          2   2.08 
# # ℹ 1,083 more rows
# # ℹ 13 more variables: gender <fct>, age <dbl>, imor <fct>, german <fct>, nbh_exposed <dbl>, appearance <fct>,
# #   article <chr>, ment_happy <dbl>, leftright <dbl>, gen_trust <dbl>, news <dbl>, news_yn <dbl>,
# #   z_dis_index <dbl>

Naïve comparison

ols <- lm_robust( # Weighted OLS
  dis_index ~ news_yn,
  weights = gewFAKT,
  data = APAD)

modelsummary( # Regression table
  list("Reported Discr." = ols),    # Named list of models
  stars = TRUE,                     # Significance stars
  gof_map = c("nobs", "r.squared"), # Fit statistics to show
  output = "kableExtra")
Reported Discr.
(Intercept) 2.187***
(0.116)
news_yn −0.093
(0.129)
Num.Obs. 1085
R2 0.002
+ p < 0.1, * p < 0.05, ** p < 0.01, *** p < 0.001

The dream: two parallel worlds

To know the causal effect, we’d compare the same person reading news vs. not.

Person If reads news (\(Y_1\)) If no news (\(Y_0\)) Causal effect
Aisha 4 ? ?
Ben ? 2 ?
Chen 5 ? ?

Both worlds at once is impossible. The missing cell is the counterfactual. Not knowing it is the fundamental problem of causal inference.

Why the naïve comparison misleads

The raw gap between readers and non-readers mixes two things:

\[ \underbrace{Avg_{n}[Y_{1i}|D_{i}=1] - Avg_{n}[Y_{0i}|D_{i}=0]}_{\text{Difference we actually observe}} = \underbrace{\color{#901A1E}{\kappa}}_{\substack{\text{Causal}\\\text{effect}}} + \underbrace{Avg_{n}[Y_{0i}|D_{i}=1] - Avg_{n}[Y_{0i}|D_{i}=0]}_{\text{Selection bias}} \]

We want the causal effect \(\color{#901A1E}{\kappa}\). Selection bias is the contamination: the groups already differed in baseline \(Y_0\), before any news.

Do the groups actually differ?

Discuss: do news-readers and non-readers look the same on their background traits?

Who reads the news? Background of readers vs non-readers
0
1
Mean Std. Dev. Mean Std. Dev. Diff. in Means Std. Error
age 36.7 12.3 43.8 15.4 7.1 2.2
nbh_exposed 4.2 0.8 4.2 1.0 −0.0 0.1
N Pct. N Pct.
imor Child of immigrant 64 29.4 399 45.6
Immigrant 154 70.6 476 54.4
german Ja 89 40.8 561 64.1
Nein 129 59.2 314 35.9

No. Readers are older and more often German citizens. The comparison is confounded.

APAD %>% # Start with the APAD data, then pipe
  # Compare readers vs non-readers on background variables
  select(news_yn, age, nbh_exposed, imor, german, gewFAKT) %>%
  # datasummary_balance() treats a column called `weights` as survey weights
  rename(weights = gewFAKT) %>%
  datasummary_balance(
    formula = ~ news_yn, # Split the table by news_yn (0 vs 1)
    data = .,
    title = "Who reads the news? Background of readers vs non-readers",
    output = "kableExtra"
  )

A DAG

Discuss: the red link is a correlation. Why is it not a causal effect?

Citizenship drives both news use and discrimination → it opens a backdoor path. The link is spurious.

The solution: experiments & randomisation

Part 2 of 3

We intervene and let a coin decide who gets the treatment.

Experiment: we don’t watch, we intervene

  • We don’t passively observe, but actively intervene.

  • We decide who gets the treatment \(D\) and who doesn’t.

Discuss: how can our intervention \(\color{red}{I}\) cut the link between treatment \(D\) and confounders \(C\)?

Break

 

Break

Randomised Controlled Trial (RCT)

We flip a coin. We randomly decide who gets the treatment:

  • \(\text{News} = 0 \rightarrow\) Control group
  • \(\text{News} = 1 \rightarrow\) Treatment group

Because assignment is random, it cannot be driven by citizenship, age, or anything else.

Why randomisation works: a fair coin

The coin in R:

# 10 coin flips: 1 = treated, 0 = control
rbinom(n = 10, size = 1, prob = 0.5)
#  [1] 0 1 0 1 0 0 0 0 0 0
# One flip per respondent: how many?
nrow(APAD)
# [1] 1093

A fair coin gives everyone the same chance of treatment, regardless of who they are.

So the two groups look alike on average — on age, citizenship, and on things we can’t even measure:

And that includes \(Y_{0i}\)!

Equal baselines:
\(Avg_{n}[Y_{0i}|\text{News}=1] = Avg_{n}[Y_{0i}|\text{News}=0]\)

Why it works: selection bias vanishes

Randomly split subjects → both groups come from the same underlying population.

\(\rightarrow\) similar on average in every way, including their baseline \(Y_0\).

\(\rightarrow\) so \(E[Y_{0i}|D=1] = E[Y_{0i}|D=0]\), and selection bias = 0.

Beware: randomisation can still fail by chance, especially in small samples.

\[ \begin{aligned} & E[Y_{1i}|D=1] - E[Y_{0i}|D=0] \\[4pt] & = E[Y_{0i} + \color{#901A1E}{\kappa}\,|D=1] - E[Y_{0i}|D=0] \\[4pt] & = \color{#901A1E}{\kappa} + \underbrace{E[Y_{0i}|D=1] - E[Y_{0i}|D=0]}_{= \; 0 \;\text{ if randomised}} \\[4pt] & = \underbrace{\color{#901A1E}{\kappa}}_{\text{the average causal effect}} \end{aligned} \]

Why it works: no backdoor in the DAG

Randomising \(I\) means no arrow points into it.

\(\Rightarrow\) no backdoor path from \(I\) to \(Y\).

\(\Rightarrow\) no confounding, no selection bias.

A real experiment

Part 3 of 3

We ran an actual RCT inside the APAD survey.

 

The APAD survey experiment

We asked subjects to read a news article and randomly decided which one:

  • Venus \(\rightarrow\) Control group
  • Discrimination \(\rightarrow\) Treatment 1
  • Acculturation \(\rightarrow\) Treatment 2

 

Did randomisation balance the groups?

Who reads the news? Background of readers vs non-readers
0
1
Mean Std. Dev. Mean Std. Dev. Diff. in Means Std. Error
age 36.7 12.3 43.8 15.4 7.1 2.2
nbh_exposed 4.2 0.8 4.2 1.0 −0.0 0.1
N Pct. N Pct.
imor Child of immigrant 64 29.4 399 45.6
Immigrant 154 70.6 476 54.4
german Ja 89 40.8 561 64.1
Nein 129 59.2 314 35.9

Beware: the observational split is imbalanced — the groups differed before any treatment.

Background of the randomly assigned treatment groups
Contr (N=358)
Treat_1 (N=375)
Treat_2 (N=360)
Mean Std. Dev. Mean Std. Dev. Mean Std. Dev.
news_yn 0.8 0.4 0.8 0.4 0.8 0.4
age 41.8 15.7 41.2 12.4 43.8 16.9
nbh_exposed 4.2 0.9 4.2 1.1 4.2 1.0
N Pct. N Pct. N Pct.
imor Child of immigrant 156 43.6 157 41.9 150 41.7
Immigrant 202 56.4 218 58.1 210 58.3
german Ja 215 60.1 222 59.2 213 59.2
Nein 143 39.9 153 40.8 147 40.8

Randomised split: Did randomisation balance the groups?

APAD %>% # Start with the APAD data, then pipe
  # Keep the treatment indicator, background variables, and survey weights
  select(article, news_yn, age, nbh_exposed, imor, german, gewFAKT) %>%
  # datasummary_balance() treats a column called `weights` as survey weights
  rename(weights = gewFAKT) %>%
  datasummary_balance(
    formula = ~ article, # Split the table by the randomised treatment
    data = .,
    title = "Background of the randomly assigned treatment groups",
    output = "kableExtra")

The causal effect

# Weighted OLS on the survey experiment
ols  <- lm_robust(dis_index ~ article, weights = gewFAKT, data = APAD)

# Same, but on the z-standardised outcome (effect in SDs)
zols <- lm_robust(z_dis_index ~ article, weights = gewFAKT, data = APAD)

modelsummary( # Regression table with readable labels
  list("Discr." = ols, "Z-Discr." = zols),
  stars = TRUE,
  coef_map = c(
    "(Intercept)"    = "Intercept (Venus control)",
    "articleTreat_1" = "Article on discrimination",
    "articleTreat_2" = "Article on acculturation"),
  gof_map = c("nobs", "r.squared"),
  output = "kableExtra")
Discr. Z-Discr.
Intercept (Venus control) 1.979*** −0.171*
(0.067) (0.074)
Article on discrimination 0.239* 0.264*
(0.107) (0.118)
Article on acculturation 0.137 0.151
(0.118) (0.130)
Num.Obs. 1085 1085
R2 0.012 0.012
+ p < 0.1, * p < 0.05, ** p < 0.01, *** p < 0.001

Because assignment was random, this difference is the causal effect \(\kappa\) without confounding.

Visualise the effect

plotdata <- zols %>%
  tidy() %>%                        # Coefficients as a tibble
  filter(term != "(Intercept)") %>% # Keep only the treatment effects
  mutate(term = case_when(          # Readable labels for the plot
    term == "articleTreat_1" ~ "Read about\ndiscrimination",
    term == "articleTreat_2" ~ "Read about\nacculturation"))

ggplot(plotdata, aes(x = estimate, y = term)) +
  geom_vline(xintercept = 0, linewidth = 0.6, colour = "grey60") +
  geom_pointrange(aes(xmin = conf.low, xmax = conf.high),
                  colour = "#901A1E", linewidth = 1.1, size = 1) +
  geom_text(aes(label = sprintf("%+.2f", estimate)),
            vjust = -1.1, size = 5, colour = "#901A1E") +
  scale_x_continuous(limits = c(-0.15, 0.6)) +
  labs(x = "Effect of news article compared to control n \n in standard deviations of the outcome", y = NULL,
       title = "Reported discrimination") +
  theme_minimal(base_size = 16) +
  theme(panel.grid.major.y = element_blank(),
        plot.title = element_text(face = "bold", size = rel(1.05)))

It’s real research

Source: Schaeffer and Kas (2024)

Effects of experimentally-induced awareness of discrimination

Point estimates with 90% and 95% confidence intervals, post-stratification-weighted OLS with (cluster-)robust standard errors.

 

The logic in one line


  • Experiment: we assign the treatment, then watch the outcome.
  • RCT: treatment assigned by chance → groups alike on everything, including the unmeasured, even \(Y_0\)
  • Balance test: our check that randomisation worked.
  • OLS on an RCT: the coefficient is the causal effect.

But what if you can’t randomise?

Most of sociology can’t run experiments.

Over the next weeks you learn what we can do instead.

Check yourself: today’s goals

Look back at the goals from the start of the lecture. Can you tick all three?

  • Explain selection bias to a friend in one sentence. Why can a raw group comparison mislead?
  • Show, with the DAG or the formula, why randomisation removes it.
  • Run lm_robust() on an experiment in R and read the coefficient as a causal effect.

Anything feel shaky? That is what this week’s Absalon quiz and the Friday exercise class are for.

Today’s important functions

  • estimatr::lm_robust(y ~ d, weights = ...): the RCT effect — a bivariate OLS of the outcome on the randomly assigned treatment (survey-weighted here).
  • modelsummary::datasummary_balance(~ d, data = ...): the balance table — check treatment and control look alike on background traits (the payoff of randomisation).
  • modelsummary(list("Model" = ...), stars = TRUE): a clean regression table.
  • rowMeans(across(...)): average several survey items into one index.

References

Polack, F. P., S. J. Thomas, N. Kitchin, et al. (2020). “Safety and Efficacy of the BNT162b2 mRNA Covid-19 Vaccine”. In: New England Journal of Medicine, pp. 2603-2615.

Schaeffer, M. and J. Kas (2024). “The integration paradox: Does awareness of the extent of ethno-racial discrimination increase reports of discrimination?” In: Political Psychology.

Schaeffer, M., J. Kas, and P. Hagedorn (2023). “The Association between Actual and Perceived Discrimination (APAD): Technical Report”. In: SocArXiv.