
Causal Data Science for Business Analytics
Hamburg University of Technology
Monday, 4. May 2026
Randomization is the only design that buys you independence from every covariate — observed and unobserved.
Under a randomized assignment protocol, each unit’s treatment value is drawn from a known distribution that does not depend on any of the unit’s other characteristics:
\[P(D_i = d) = \frac{1}{k} \quad\text{for } k \text{ equally-sized treatment groups.}\]
Definition: Covariate Balance
We have covariate balance if the distribution of covariates \(\mathbf{X}\) is the same across treatment groups:
\[P(\mathbf{X} \mid D = d) \stackrel{d}{=} P(\mathbf{X} \mid D = d') \quad \text{for all } d, d'.\]
Equivalently:
\[\mathbf{X} \perp\!\!\!\perp D.\]
Randomization implies covariate balance — across all covariates, including unobserved ones. This is the central reason RCTs dominate observational designs.
Let \(\mathbf{X}\) be a sufficient adjustment set including all (un-)observed confounders. Backdoor adjustment gives:
\[P(Y \mid do(D = d)) = \sum_x P(Y \mid D = d,\, \mathbf{X} = \mathbf{x}) P(\mathbf{X} = \mathbf{x}).\]
Multiply by \(\frac{P(D = d \mid \mathbf{X})}{P(D = d \mid \mathbf{X})}\) to recover the joint distribution in the numerator:
\[P(Y \mid do(D = d)) = \sum_x \frac{P(Y \mid D = d, \mathbf{X}) \, P(D = d \mid \mathbf{X}) \, P(\mathbf{X})}{P(D = d \mid \mathbf{X})} = \sum_x \frac{P(Y, D, \mathbf{X})}{P(D = d \mid \mathbf{X})}.\]
Apply randomization \(\mathbf{X} \perp\!\!\!\perp D\), so \(P(D \mid \mathbf{X}) = P(D)\):
\[P(Y \mid do(D = d)) = \sum_x \frac{P(Y, D, \mathbf{X})}{P(D)} = \sum_x P(Y, \mathbf{X} \mid D = d) = P(Y \mid D = d). \quad\blacksquare\]
Intervention distribution collapses to the conditional — association is causation under randomization.
Graphically: randomization replaces every incoming arrow into \(D\) with a single edge from a coin-flip node \(d\). All backdoor paths are mutilated, so the empty set \(\emptyset\) satisfies the backdoor criterion.
Pre-intervention world

\(X\) confounds \(D \to Y\): backdoor path \(D \leftarrow X \to Y\) is open.
Post-intervention world (do-mutilation)

The arrow \(X \to D\) is deleted: every backdoor path is blocked.
The causal ATE is defined on potential outcomes: \(ATE_{cs} = \mathbb{E}[Y_i(1) - Y_i(0)]\).
Let \(p = P(D_i = 1)\). Decompose by conditioning on \(D\):
\[ATE_{cs} = p \underbrace{\mathbb{E}[Y_i(1) - Y_i(0) \mid D_i = 1]}_{\text{ATT}} + (1-p) \underbrace{\mathbb{E}[Y_i(1) - Y_i(0) \mid D_i = 0]}_{\text{ATU}}\]
Expanding the conditional expectations and naming the four group means:
\[ \begin{aligned} ATE_{cs} &= p\, \mathbb{E}[Y_i(1) \mid D_i = 1] - p\, \mathbb{E}[Y_i(0) \mid D_i = 1] + (1-p)\, \mathbb{E}[Y_i(1) \mid D_i = 0] - (1-p)\, \mathbb{E}[Y_i(0) \mid D_i = 0] \\ &= p\, \mu_{11} - p\, \mu_{01} + (1-p)\, \mu_{10} - (1-p)\, \mu_{00}, \quad \text{where } \mu_{yd} = \mathbb{E}[Y_i(y) \mid D_i = d]. \end{aligned} \]
The associational ATE uses observed quantities only:
\[ATE_{as} = \mathbb{E}[Y_i \mid D_i = 1] - \mathbb{E}[Y_i \mid D_i = 0] = \mu_{11} - \mu_{00}.\]
The second equality uses consistency: when \(D_i = d\), the observed \(Y_i\) equals the potential outcome \(Y_i(d)\), so \(\mathbb{E}[Y_i \mid D_i = d] = \mathbb{E}[Y_i(d) \mid D_i = d]\).
Working through the algebra (see session notes for derivation), the associational ATE decomposes as:
\[ \begin{aligned} ATE_{as} \;=\;\; & \underbrace{\mathbb{E}[Y_i(1) - Y_i(0)]}_{ATE_{cs}} \\ \;+\;\; & \underbrace{\mathbb{E}[Y_i(0) \mid D_i = 1] - \mathbb{E}[Y_i(0) \mid D_i = 0]}_{\color{#00C1D4}{\text{Confounding Bias}}} \\ \;+\;\; & \underbrace{(1 - \mathbb{E}[D_i]) \big[\underbrace{\mathbb{E}[Y_i(1) \mid D_i = 1] - \mathbb{E}[Y_i(0) \mid D_i = 1]}_{\text{ATT}} - \underbrace{(\mathbb{E}[Y_i(1) \mid D_i = 0] - \mathbb{E}[Y_i(0) \mid D_i = 0])}_{\text{ATU}}\big]}_{\color{#FF7E15}{\text{Heterogeneity Bias}}} \end{aligned} \]
This is the gold equation of the session — we will use it again in Exercise 1 (Christmas / socks). Two distinct biases separate the observed mean difference from the true causal ATE:
Exchangeability under randomization: \(\{Y(1), Y(0)\} \perp\!\!\!\perp D\), which implies:
\[ \begin{aligned} ATE_{as} \;=\;\; & \underbrace{\mathbb{E}[Y_i(1) - Y_i(0)]}_{ATE_{cs}} \\ \;+\;\; & \underbrace{\mathbb{E}[Y_i(0) \mid D_i = 1] - \mathbb{E}[Y_i(0) \mid D_i = 0]}_{\color{#00C1D4}{\text{Confounding Bias} \;=\; 0}} \\ \;+\;\; & \underbrace{(1 - \mathbb{E}[D_i]) \big[\mathbb{E}[Y_i(1) \mid D_i = 1] - \mathbb{E}[Y_i(0) \mid D_i = 1] - (\mathbb{E}[Y_i(1) \mid D_i = 0] - \mathbb{E}[Y_i(0) \mid D_i = 0])\big]}_{\color{#FF7E15}{\text{Heterogeneity Bias} \;=\; 0}} \end{aligned} \]
\[\boxed{ATE_{as} \;=\; ATE_{cs} \quad \text{under randomization.}}\]
Simple randomization gives balance in expectation — but in any one realisation, treated and control groups can drift apart on important pre-treatment covariates (especially in small samples).
Block (stratified) randomization: partition units into strata \(S_1, \ldots, S_K\) on a pre-treatment covariate, then randomize within each stratum. Balance on \(S\) is now guaranteed exactly, not just in expectation.
Stratified ATE estimator
Within each block \(k\), compute the local mean difference \(\hat\tau_k = \bar Y_{1,k} - \bar Y_{0,k}\). Combine using stratum weights \(\pi_k = n_k / n\):
\[\hat\tau_{\text{strat}} \;=\; \sum_{k=1}^{K} \pi_k\, \hat\tau_k.\]
Variance is reduced when within-stratum outcome variance is smaller than the unconditional outcome variance — which is exactly the case when \(S\) is predictive of \(Y\).
When to block: prior-period outcome (\(Y_{\text{pre}}\)), demographic strata, geographic regions, or any pre-treatment variable known to be strongly predictive of \(Y\). Avoid blocking on too many strata (sparse cells, lost variance reduction).
Simulated experiment with \(n = 1{,}000\) users, pre-period spend \(X\) predicts outcome spend \(Y\), and we compare simple vs. blocked random assignment.
library(randomizr)
library(dplyr)
set.seed(42)
n <- 1000
X <- rnorm(n, mean = 50, sd = 15)
block_id <- cut(X, breaks = quantile(X, probs = 0:4/4),
labels = paste0("Q", 1:4), include.lowest = TRUE)
# Simple random assignment vs blocked random assignment
D_simple <- complete_ra(N = n, prob = 0.5)
D_blocked <- block_ra(blocks = block_id, prob = 0.5)
# True effect = 5, plus pre-period spend predicts Y strongly
Y <- 0.8 * X + 5 * D_blocked + rnorm(n, sd = 5)
# SE of the ATE under each design (HC1-robust, matches the rest of the deck)
library(estimatr)
se_simple <- lm_robust(Y ~ D_simple, se_type = "HC1")$std.error["D_simple"]
se_blocked <- lm_robust(Y ~ D_blocked + block_id, se_type = "HC1")$std.error["D_blocked"]
c(SE_simple = round(unname(se_simple), 3), SE_blocked = round(unname(se_blocked), 3)) SE_simple SE_blocked
0.834 0.434
import numpy as np
import pandas as pd
import statsmodels.formula.api as smf
rng = np.random.default_rng(42)
n = 1000
X = rng.normal(loc=50, scale=15, size=n)
block_id = pd.qcut(X, q=4, labels=["Q1", "Q2", "Q3", "Q4"])
# Simple random assignment
D_simple = rng.choice([0, 1], size=n)
# Blocked random assignment: shuffle within each quartile
D_blocked = np.zeros(n, dtype=int)
for q in block_id.unique():
idx = np.where(block_id == q)[0]
half = len(idx) // 2
perm = rng.permutation(idx)
D_blocked[perm[:half]] = 1
# True effect = 5
Y = 0.8 * X + 5 * D_blocked + rng.normal(scale=5, size=n)
df = pd.DataFrame({"Y": Y, "D_blocked": D_blocked,
"D_simple": D_simple, "block": block_id})
# SE of the ATE under each design (HC1-robust, matches the rest of the deck)
se_simple = smf.ols("Y ~ D_simple", data=df).fit(cov_type="HC1").bse["D_simple"]
se_blocked = smf.ols("Y ~ D_blocked + C(block)", data=df).fit(cov_type="HC1").bse["D_blocked"]
print({"SE_simple": round(se_simple, 3), "SE_blocked": round(se_blocked, 3)}){'SE_simple': np.float64(0.82), 'SE_blocked': np.float64(0.428)}
The blocked design typically yields a substantially smaller standard error when \(X\) is predictive of \(Y\) — variance reduction by design.
From a population identity to an estimator: mean comparison and linear regression are the same thing.
Identification is the step of expressing the inherently unobservable ATE in terms of observable population quantities — by appeal to randomization or further assumptions that remove confounding and heterogeneity bias.
Under randomization of \(D\), the mean comparison identifies the ATE:
\[\tau_{ATE} = \mathbb{E}[Y_i(1) - Y_i(0)] \;=\; \mathbb{E}[Y_i \mid D_i = 1] - \mathbb{E}[Y_i \mid D_i = 0].\]
The corresponding sample estimator is the difference of two sample means:
\[\hat\tau_{ATE} \;=\; \frac{1}{\sum_i D_i} \sum_i D_i Y_i \;-\; \frac{1}{\sum_i (1 - D_i)} \sum_i (1 - D_i) Y_i.\]
Job Corps is a US Department of Labor education program targeting disadvantaged 16–24 year olds. The RCT randomized program access between November 1994 and February 1996; outcome is weekly earnings four years post-assignment.
[1] 197.9258
[1] 213.981
[1] 16.05513
197.92583695570693
213.9809642758061
16.05512732009916
Sources: Schochet, Burghardt & Glazerman (2001); Schochet, Burghardt & McConnell (2008). Data: 1994–96 US, distributed via the causalweight R package.
A randomized experiment also delivers conditional independence for any pre-treatment covariate \(X\):
\[\{Y(1), Y(0)\} \perp\!\!\!\perp D \mid X.\]
Hence the Conditional Average Treatment Effect (CATE) is identified:
\[\tau_{CATE}(x) \;=\; \mathbb{E}[Y_i(1) - Y_i(0) \mid X_i = x] \;=\; \mathbb{E}[Y_i \mid D_i = 1, X_i = x] - \mathbb{E}[Y_i \mid D_i = 0, X_i = x].\]
X must be pre-treatment
The conditioning set \(X\) must be measured before treatment assignment — formally, we condition on \(X\), not \(X(D)\). Conditioning on a post-treatment variable can both block part of the causal effect and open collider paths through unobservables (back to Session 02’s collider lesson — and previewed again in the Pre- vs. post-treatment covariates slide).
Estimators of CATEs (causal forests, meta-learners) are the topic of Session 09: Treatment Effect Heterogeneity.
Start from the observed-outcome equation for each unit:
\[Y_i = Y_i(1) \cdot D_i + Y_i(0) \cdot (1 - D_i) = Y_i(0) + (Y_i(1) - Y_i(0))\, D_i.\]
Take the conditional expectation given \(D_i\) in the population:
\[\mathbb{E}[Y_i \mid D_i] = \mathbb{E}[Y_i(0) \mid D_i] + \big(\mathbb{E}[Y_i(1) \mid D_i] - \mathbb{E}[Y_i(0) \mid D_i]\big) \, D_i.\]
Apply exchangeability — under randomization, \(\{Y(1), Y(0)\} \perp\!\!\!\perp D\), so the conditional expectations do not depend on \(D\):
\[\mathbb{E}[Y_i(0) \mid D_i] = \mathbb{E}[Y_i \mid D_i = 0], \qquad \mathbb{E}[Y_i(1) \mid D_i] = \mathbb{E}[Y_i \mid D_i = 1].\]
Substituting:
\[\mathbb{E}[Y_i \mid D_i] \;=\; \underbrace{\mathbb{E}[Y_i \mid D_i = 0]}_{\beta_0} \;+\; \underbrace{\big(\mathbb{E}[Y_i \mid D_i = 1] - \mathbb{E}[Y_i \mid D_i = 0]\big)}_{\beta_1} \cdot D_i.\]
This is the population linear-regression model with \(\beta_1 = ATE\).
The residual is the gap between the observed outcome and the conditional mean: \(\epsilon_i \;=\; Y_i - \underbrace{(\beta_0 + \beta_1 D_i)}_{\mathbb{E}[Y_i \mid D_i]}\).
library(ggplot2)
library(dplyr)
set.seed(42)
n <- 50
df <- bind_rows(
data.frame(D = 0, Y = rnorm(n, mean = 2, sd = 1)),
data.frame(D = 1, Y = rnorm(n, mean = 4, sd = 1))
)
means <- df |> group_by(D) |> summarise(Ybar = mean(Y), .groups = "drop")
mu0 <- means$Ybar[means$D == 0]
mu1 <- means$Ybar[means$D == 1]
ggplot(df, aes(x = D, y = Y)) +
geom_point(shape = 1, alpha = 0.7) +
geom_segment(aes(x = 0, y = mu0, xend = 1, yend = mu1),
colour = "#005e73", linewidth = 0.6) +
geom_hline(yintercept = mu0, linetype = "dotted") +
geom_hline(yintercept = mu1, linetype = "dotted") +
geom_segment(aes(x = 0.99, y = mu1, xend = 0.99, yend = max(df$Y)),
colour = "#FF7E15", linewidth = 0.8,
arrow = arrow(length = unit(0.18, "cm"))) +
annotate("text", x = 0.94, y = 4.8,
label = "epsilon[i]", parse = TRUE,
colour = "#FF7E15", size = 5) +
geom_segment(aes(x = 0.5, y = mu0, xend = 0.5, yend = mu1),
colour = "#00C1D4", linewidth = 0.8,
arrow = arrow(length = unit(0.18, "cm"), ends = "both")) +
annotate("text", x = 0.55, y = 3,
label = "beta[1]", parse = TRUE,
colour = "#00C1D4", size = 5) +
geom_segment(aes(x = 0.02, y = 0, xend = 0.02, yend = mu0),
colour = "#00C1D4", linewidth = 0.8,
arrow = arrow(length = unit(0.18, "cm"))) +
annotate("text", x = 0.07, y = 1,
label = "beta[0]", parse = TRUE,
colour = "#00C1D4", size = 5) +
scale_x_continuous(breaks = c(0, 1), limits = c(-0.05, 1.05)) +
scale_y_continuous(breaks = 0:6, limits = c(0, 6)) +
labs(x = "D", y = "Y")
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
rng = np.random.default_rng(42)
n = 50
df = pd.DataFrame({
"D": np.r_[np.zeros(n), np.ones(n)],
"Y": np.r_[rng.normal(2, 1, n), rng.normal(4, 1, n)],
})
mu0 = df.loc[df.D == 0, "Y"].mean()
mu1 = df.loc[df.D == 1, "Y"].mean()
fig, ax = plt.subplots(figsize=(7, 4))
_ = ax.scatter(df["D"], df["Y"], facecolors="none", edgecolors="black", alpha=0.7)
_ = ax.plot([0, 1], [mu0, mu1], color="#005e73", linewidth=1.4)
_ = ax.axhline(mu0, linestyle=":", color="grey")
_ = ax.axhline(mu1, linestyle=":", color="grey")
_ = ax.annotate("", xy=(0.99, df["Y"].max()), xytext=(0.99, mu1),
arrowprops=dict(arrowstyle="->", color="#FF7E15", lw=1.5))
_ = ax.text(0.94, 4.8, r"$\epsilon_i$", color="#FF7E15", fontsize=14)
_ = ax.annotate("", xy=(0.5, mu1), xytext=(0.5, mu0),
arrowprops=dict(arrowstyle="<->", color="#00C1D4", lw=1.5))
_ = ax.text(0.55, 3, r"$\beta_1$", color="#00C1D4", fontsize=14)
_ = ax.annotate("", xy=(0.02, mu0), xytext=(0.02, 0),
arrowprops=dict(arrowstyle="->", color="#00C1D4", lw=1.5))
_ = ax.text(0.07, 1, r"$\beta_0$", color="#00C1D4", fontsize=14)
_ = ax.set(xticks=[0, 1], xlim=(-0.05, 1.05), ylim=(0, 6),
xlabel="D", ylabel="Y")
plt.tight_layout()
plt.show()
The sample estimator is the OLS minimiser:
\[\hat\beta_0,\, \hat\beta_1 \;=\; \arg\min_{\beta_0^*,\, \beta_1^*} \sum_{i=1}^n \underbrace{(Y_i - \beta_0^* - \beta_1^* D_i)^2}_{\hat\epsilon_i^2}.\]
Solving the first-order conditions yields the closed-form estimators:
\[\hat\beta_1 \;=\; \frac{\widehat{\text{Cov}}(Y_i, D_i)}{\widehat{\text{Var}}(D_i)},\]
\[\widehat{\text{Cov}}(Y_i, D_i) = \tfrac{1}{n-1} \sum_{i=1}^n \left(Y_i - \tfrac{1}{n}\sum_j Y_j\right)\left(D_i - \tfrac{1}{n}\sum_j D_j\right),\]
\[\widehat{\text{Var}}(D_i) = \tfrac{1}{n-1} \sum_{i=1}^n \left(D_i - \tfrac{1}{n}\sum_j D_j\right)^2, \qquad \hat\beta_0 \;=\; \bar{Y} - \hat\beta_1 \bar{D}.\]
A point estimate is not a finding — uncertainty quantification is what turns a number into a claim.
Under standard regularity conditions (Wooldridge 2010), the OLS estimators of \(\beta_0, \beta_1\) have three desirable properties:
Unbiased
On average across hypothetical samples, equal to the true parameter: \(\mathbb{E}[\hat\beta_1] = \beta_1, \;\; \mathbb{E}[\hat\beta_0] = \beta_0\).
Consistent
Converges in probability to the truth as \(n \to \infty\): \(\hat\beta_1 \xrightarrow{p} \beta_1, \;\; \hat\beta_0 \xrightarrow{p} \beta_0\).
Asymptotically normally distributed
Approximately normal in large samples — the basis for \(t\)-tests and confidence intervals:
\[\sqrt{n}(\hat\beta_1 - \beta_1) \xrightarrow{d} \mathcal{N}(0,\, \sigma^2_{\beta_1}), \qquad \sqrt{n}(\hat\beta_0 - \beta_0) \xrightarrow{d} \mathcal{N}(0,\, \sigma^2_{\beta_0}).\]
Proofs: Wooldridge (2010), Econometric Analysis of Cross Section and Panel Data, Ch. 4. Skipped in lecture.
In an RCT, who actually randomizes? The coin flip — not nature drawing from an infinite population.
Both fix the units in front of you and treat the assignment vector \(\mathbf{D}\) as the only randomness — they disagree on what to do with it.
Neyman (1923) — large-sample
Asks “how big is the average effect, and how precisely?” — tests the weak null \(\mathbb{E}[Y(1) - Y(0)] = 0\). Treats the potential outcomes as fixed; randomization is the source of variability.
\[\sqrt{n}(\hat\tau - \tau) \xrightarrow{d} \mathcal{N}(0,\, V_N).\]
Output: point estimate \(\hat\tau\) + asymptotic SE + CI. Variance \(V_N\) is conservatively estimated under treatment-effect heterogeneity (Neyman’s variance bound).
Pros: closed-form CIs, scales to large \(n\), dominates textbooks. Cons: only valid asymptotically; conservative in small samples.
Fisher (1925) — exact
Asks “could this difference be a coincidence under no effect for anyone?” — tests the sharp null \(H_0: Y_i(1) = Y_i(0)\) for every unit \(i\).
Under \(H_0\), observed outcomes are fixed; the test-statistic distribution comes from re-shuffling treatment labels across all \(\binom{n}{n_1}\) permutations.
Output: an exact finite-sample p-value \[p = \frac{1}{|\mathcal{P}|} \sum_{\pi \in \mathcal{P}} \mathbf{1}\{|\hat\tau_\pi| \geq |\hat\tau_{\text{obs}}|\}.\]
Pros: no asymptotic approximation; works in tiny samples. Cons: tests sharp null (not average null); compute-intensive.
From here on we ride with Neyman — but Fisher’s permutation logic returns next session as the engine behind A/B-test p-values.
A point estimate \(\hat\beta_1\) has zero information about its own precision. We need three primitives:
Standard errors — \(se(\hat\beta_1)\) measures the sample-to-sample variability of the estimator.
Hypothesis tests — assess whether the data are inconsistent with a specific null value (typically \(\beta_1 = 0\)).
Confidence intervals — report a range of plausible values for \(\beta_1\) at a chosen confidence level.
These three are not three separate calculations: they are three views on the same sampling-distribution object. Once you have the SE, you have everything else by simple arithmetic.
The asymptotic variance of \(\hat\beta_1\) in the population (heteroskedasticity-robust / sandwich form):
\[\text{Var}(\hat\beta_1) \;=\; \frac{\mathbb{E}\!\left[\epsilon^2 (D_i - \mathbb{E}[D_i])^2\right]}{n \cdot (\text{Var}(D_i))^2}.\]
Plug-in sample analog using residuals \(\hat\epsilon_i = Y_i - \hat\beta_0 - \hat\beta_1 D_i\):
\[\widehat{\text{Var}}(\hat\beta_1) \;=\; \frac{\sum_i \hat\epsilon_i^2 (D_i - \bar D)^2}{\big[\sum_i (D_i - \bar D)^2\big]^2}, \qquad se(\hat\beta_1) = \sqrt{\widehat{\text{Var}}(\hat\beta_1)}.\]
This is the HC0 sandwich estimator. R’s sandwich::vcovHC(fit, type = "HC1") and Python’s cov_type="HC1" apply a small-sample correction factor of \(n/(n-k)\).
HC vs. classical SE
The expression above is the heteroskedasticity-robust (HC0/HC1) sandwich form. The classical “homoskedastic” SE simplifies to \(\sigma_\epsilon^2 / (n \cdot \text{Var}(D))\) and assumes \(\text{Var}(\epsilon \mid D) = \sigma_\epsilon^2\) — a constraint we rarely want to impose. Default to HC.
The standardised statistic with known \(sd\) is normal; with the estimated \(se\) it is \(t\)-distributed with \(df = n - 2\):
\[z_1 = \frac{\hat\beta_1 - \beta_1}{sd(\hat\beta_1)} \xrightarrow{d} \mathcal{N}(0, 1) \quad \text{vs.} \quad t_1 = \frac{\hat\beta_1 - \beta_1}{se(\hat\beta_1)} \sim t_{n-2}.\]


Step 1. State the null \(H_0\) and alternative \(H_1\):
Step 2. Set the significance level \(\alpha\):
Step 3. Compute the critical value \(c\):
Step 4. Compute the test statistic and p-value:
Decision rules:
A \((1 - \alpha)\) confidence interval is the range of values \(\beta_1^*\) for which the corresponding null \(H_0: \beta_1 = \beta_1^*\) would not be rejected at level \(\alpha\).
Frequentist interpretation: across hypothetical repeated samples, \((1 - \alpha)\) of the constructed CIs would cover the true \(\beta_1\).
Two-sided CI:
\[CI \;=\; \hat\beta_1 \;\pm\; c \cdot se(\hat\beta_1).\]
Worked example: \(\hat\beta_1 = 0.30\), \(se = 0.05\), \(c = 1.96\) → \(CI = 0.30 \pm 1.96 \cdot 0.05 = [0.202,\; 0.398]\).
One-sided CI:
\[CI \;=\; \hat\beta_1 \;-\; c \cdot se(\hat\beta_1) \quad\text{(upper open)}.\]
Worked example with \(c = 1.65\): \(CI = [0.218,\; \infty)\).
The frequentist machinery is precise but not intuitive. Students (and reviewers, and journalists) routinely misread it.
What people want it to mean
These are all probability statements about hypotheses or parameters given data, \(P(H_0 \mid \text{data})\) or \(P(\beta_1 \in CI \mid \text{data})\).
What it actually means
All are statements about the procedure, not the parameter.
The intuitive readings are legitimate — under Bayesian inference. With a prior on \(\beta_1\), the posterior \(P(\beta_1 \mid \text{data})\) supports direct statements like “there’s a 95% chance \(\beta_1 \in [0.20,\, 0.40]\)”. That’s the credible interval — coming up in Session 5: online A/B testing.
General: percentage of variance in \(Y\) explained by all variables in the model:
\[R^2 \;=\; 1 - \frac{SSR}{SST}, \qquad SSR = \sum_i (Y_i - \hat Y_i)^2, \qquad SST = \sum_i (Y_i - \bar Y)^2.\]
Specific case — only a single binary treatment \(D\) in the model:
\[R^2 \;=\; r^2 \;=\; \left( \frac{\sum_i (Y_i - \bar Y)(D_i - \bar D)}{\sqrt{\sum_i (Y_i - \bar Y)^2} \cdot \sqrt{\sum_i (D_i - \bar D)^2}} \right)^{\!2}.\]
In a causal-inference context, \(R^2\) is largely diagnostic — high \(R^2\) does not mean the regression is causally identified, and low \(R^2\) does not preclude precise ATE estimation. We rarely report it as a primary result.
Estimate the ATE of Job Corps assignment on year-4 weekly earnings using OLS with heteroskedasticity-robust standard errors.
library(readr)
library(here)
library(sandwich)
library(modelsummary)
jc <- read_csv(here("materials/data/jc.csv"),
show_col_types = FALSE)
ols <- lm(earny4 ~ assignment, data = jc)
modelsummary(ols,
vcov = sandwich::vcovHC,
estimate = "{estimate} (se = {std.error}, t = {statistic}){stars}",
statistic = "p = {p.value}, CI = [{conf.low}, {conf.high}]",
stars = TRUE,
gof_map = c("r.squared", "nobs"),
output = "markdown")| (Intercept) | 197.926 (se = 3.073, t = 64.416)*** |
| p = <0.001, CI = [191.903, 203.949] | |
| assignment | 16.055 (se = 4.074, t = 3.941)*** |
| p = <0.001, CI = [8.069, 24.041] | |
| R2 | 0.002 |
| Num.Obs. | 9240 |
Significance: + p < 0.1, * p < 0.05, ** p < 0.01, *** p < 0.001.
The sample stands in for the population: resampling with replacement mimics fresh draws, and the resulting variability proxies the true sampling variability.
The procedure (Huber 2023):
\[se(\hat\beta_1) \;=\; \sqrt{\tfrac{1}{B - 1} \sum_{b=1}^{B} (\hat\beta_1^{(b)} - \bar{\hat\beta_1})^2}.\]
est.assignment boot_se
16.05513 3.95481
import pandas as pd, numpy as np, statsmodels.formula.api as smf
jc = pd.read_csv("../../materials/data/jc.csv")
rng = np.random.default_rng(1)
def bs(df, idx):
return smf.ols("earny4 ~ assignment",
data=df.iloc[idx]).fit().params["assignment"]
B = 1999
boots = np.array([
bs(jc, rng.integers(0, len(jc), len(jc)))
for _ in range(B)
])
print({"est": boots.mean().round(2), "boot_se": boots.std(ddof=1).round(3)})Bootstrap SE ≈ HC-robust SE in well-behaved samples.
When bootstrap > HC-robust: small \(n\), heavy tails, clustered/dependent data, or non-smooth statistics (medians, quantiles, ratios). Otherwise HC1 is faster and equally honest.
OLS extends naturally to multivalued and continuous treatments — and to covariate-adjusted variance reduction.
Treatment \(D\) can take discrete values, ordered or unordered:
The independence assumption generalises to all treatment levels:
\[\{Y_i(0), Y_i(1), Y_i(2), \ldots, Y_i(J)\} \perp\!\!\!\perp D_i.\]
Encode each non-baseline level as a binary dummy \(D_{ij} = \mathbf{1}\{D_i = j\}\) and run OLS:
\[\mathbb{E}[Y_i \mid D_i] \;=\; \underbrace{\beta_0}_{\mathbb{E}[Y \mid D = 0]} + \underbrace{\beta_1}_{\Delta \text{ vs } 0 \text{ at } 1} D_{i1} + \underbrace{\beta_2}_{\Delta \text{ vs } 0 \text{ at } 2} D_{i2} + \ldots + \underbrace{\beta_J}_{\Delta \text{ vs } 0 \text{ at } J} D_{iJ}.\]
Each \(\beta_j\) is the ATE of level \(j\) vs. the baseline level \(0\).
804 university students randomized into three mutually exclusive arms: control (default survey: professional-plans questions first, then personal-info questions), information (shown a graph of median monthly Swiss wages before being asked their own expectation), order (default sequence reversed — personal questions first, professional plans second — pure framing, no extra information). Outcome wexpect2 = own wage expectation 3 years after graduation, in 500 CHF brackets (Fernandes, Huber, and Vaccaro 2020).
library(readr); library(here)
library(sandwich); library(modelsummary)
wexpect <- read_csv(here("materials/data/wexpect.csv"),
show_col_types = FALSE)
ols <- lm(wexpect2 ~ treatmentinformation + treatmentorder,
data = wexpect)
modelsummary(ols,
vcov = sandwich::vcovHC,
estimate = "{estimate} (se = {std.error}, t = {statistic}){stars}",
statistic = "p = {p.value}, CI = [{conf.low}, {conf.high}]",
stars = TRUE,
gof_map = c("r.squared", "nobs"),
output = "markdown")| (Intercept) | 9.408 (se = 0.159, t = 59.268)*** |
| p = <0.001, CI = [9.096, 9.719] | |
| treatmentinformation | 0.345 (se = 0.243, t = 1.421) |
| p = 0.156, CI = [-0.132, 0.822] | |
| treatmentorder | -0.173 (se = 0.234, t = -0.741) |
| p = 0.459, CI = [-0.633, 0.286] | |
| R2 | 0.006 |
| Num.Obs. | 804 |
Significance: + p < 0.1, * p < 0.05, ** p < 0.01, *** p < 0.001.
Treatment \(D\) takes (continuum-) many values respecting cardinality: marketing spend in EUR, years of education, dose in mg.
Independence assumption for every value of the continuous treatment:
\[Y_i(d) \perp\!\!\!\perp D_i \qquad \text{for all } d.\]
Two strategies:
\[ATE \;=\; \frac{\partial \mathbb{E}[Y_i \mid D_i]}{\partial D_i} \;=\; \beta_1.\]
Linearity assumption: \(\partial \mathbb{E}[Y_i(D = t')] / \partial D_i \;=\; \partial \mathbb{E}[Y_i(D = t)] / \partial D_i\) for all \(t \neq t'\) in the support of \(D_i\) — i.e., the ATE is constant in \(D\).


Quadratic specification: \(\mathbb{E}[Y_i \mid D_i] = \beta_0 + \beta_1 D_i + \beta_2 D_i^2 \;\;\Rightarrow\;\; ATE(D_i) = \partial \mathbb{E}[Y_i \mid D_i] / \partial D_i = \beta_1 + 2 \beta_2 D_i\).
For maximum flexibility: higher-order polynomials, splines, or kernel regression.
Kernel regression estimates \(\widehat{\mathbb{E}}[Y \mid D = d]\) as a locally weighted average of nearby \(Y_i\) — observations close to \(d\) get high weight, distant ones get low weight; the bandwidth controls “close”.
200 brand-week observations, marketing-spend dataset (datarium::marketing). Outcome: sales; treatment: newspaper-ad spend (continuous, USD).
import pandas as pd, numpy as np, matplotlib.pyplot as plt
from statsmodels.nonparametric.kernel_regression import KernelReg
mkt = pd.read_csv("../../materials/data/marketing.csv")
D = mkt["newspaper"].values
Y = mkt["sales"].values
kr = KernelReg(endog=Y, exog=D, var_type="c")
grid = np.linspace(D.min(), D.max(), 100)
yhat, _ = kr.fit(grid)
fig, ax = plt.subplots(figsize=(6, 3.5))
ax.scatter(D, Y, alpha=0.4, color="#005e73")
ax.plot(grid, yhat, color="#FF7E15", linewidth=1.8)
ax.set_xlabel("newspaper"); ax.set_ylabel("sales")
plt.tight_layout(); plt.show()
Under randomization, \(D \perp X\), so adding pre-treatment covariates does not change the population value of \(\hat\beta_1\) — it cannot fix bias that does not exist.
What it does change is precision: the residual variance shrinks, and so does \(se(\hat\beta_1)\).
Augment the regression with covariates:
\[Y_i \;=\; \underbrace{\hat\beta_0 + \hat\beta_1 D_i + \hat\beta_{X_1} X_{i1} + \ldots + \hat\beta_{X_K} X_{iK}}_{\widehat{\mathbb{E}}[Y_i \mid D_i, X_i]} + \hat\epsilon_i.\]
The covariates explain part of the variation in \(Y\) that was previously residual:
\[R^2 \uparrow, \qquad \frac{\widehat{\text{Var}}(\hat\epsilon_i)}{\widehat{\text{Var}}(Y_i)} \downarrow, \qquad se(\hat\beta_1) \downarrow.\]
Under non-random assignment, covariates serve a different purpose
When \(D\) is not randomized, including covariates is for bias correction (de-confounding), not just variance reduction — Session 06 spends a full lecture on that. Don’t confuse the two roles.
Freedman (2008): naively adding covariates additively, \(Y \sim D + X\), can introduce small-sample bias when treatment-effect heterogeneity is present and \(X\) is unbalanced.
Why: \(Y \sim D + X\) assumes one effect \(\beta_1\) for everyone. With true heterogeneity \(\tau(X)\), OLS instead recovers a \(\widehat{\text{Var}}(D \mid X)\)-weighted average of conditional ATEs — and finite-sample imbalance in \(X\) tilts those weights off the marginal density that defines the ATE.
Lin (2013) proposes the canonical fix — interact \(D\) with mean-centred covariates:
\[Y_i \;=\; \beta_0 + \beta_1 D_i + \boldsymbol{\gamma}^\top (X_i - \bar X) + \boldsymbol{\delta}^\top D_i \cdot (X_i - \bar X) + \epsilon_i.\]
The coefficient \(\hat\beta_1\) is the Lin estimator of the ATE.
We’ll use the Lin estimator in the coding lab and compare its SE to the unadjusted mean comparison and to the additive lm(Y ~ D + X).
Pre-treatment \(X\) — safe

\(X\) is exogenous to \(D\). Adding \(X\) to the regression reduces variance without changing the expectation of \(\hat\beta_1\).
Post-treatment \(X\) — unsafe

Conditioning on \(X\) here opens a collider path through \(U\) — bridge to S03’s collider lesson.
Two harms from conditioning on post-treatment \(X\): (1) mediator-blocking — closes part of \(D \to X \to Y\); (2) collider-opening — opens \(D \to X \leftarrow U \to Y\), breaking randomization.
522 university students, randomly assigned to receive (or not) an environmental-impact leaflet about coffee. Outcome: awareness about waste production (awarewaste); covariates: mother’s education (mumedu) and student sex.
library(readr); library(here)
library(sandwich); library(modelsummary)
cl <- read_csv(here("materials/data/coffeeleaflet.csv"),
show_col_types = FALSE)
ols <- lm(awarewaste ~ treatment + mumedu + sex, data = cl)
modelsummary(ols,
vcov = sandwich::vcovHC,
estimate = "{estimate} (se = {std.error}, t = {statistic}){stars}",
statistic = "p = {p.value}, CI = [{conf.low}, {conf.high}]",
stars = TRUE,
gof_map = c("r.squared", "nobs"),
output = "markdown")| (Intercept) | 1.187 (se = 0.249, t = 4.770)*** |
| p = <0.001, CI = [0.698, 1.676] | |
| treatment | 0.332 (se = 0.096, t = 3.449)*** |
| p = <0.001, CI = [0.143, 0.521] | |
| mumedu | 0.272 (se = 0.090, t = 3.007)** |
| p = 0.003, CI = [0.094, 0.450] | |
| sex | 0.137 (se = 0.100, t = 1.370) |
| p = 0.171, CI = [-0.060, 0.333] | |
| R2 | 0.046 |
| Num.Obs. | 466 |
Significance: + p < 0.1, * p < 0.05, ** p < 0.01, *** p < 0.001.
The coefficient on treatment is the ATE of the leaflet on waste-awareness, with mumedu and sex absorbing residual variation in \(Y\) — variance reduction, not bias correction (since assignment was randomized).
| Thank you for your attention! | |
|
|
Causal Data Science for Business Analytics | 4. Experiments & Linear Regression