Part III: Discovery Through Data and Models
Chapter 31: Causal Discovery and Causal Inference

31.1 Structural Causal Models

"I encoded an entire clinical trial into four variables and three arrows. The reviewers said I was oversimplifying. I said the arrows were doing the heavy lifting."

A Structural Equation That Felt Underappreciated

Prerequisites

You should be comfortable with the causal reasoning concepts from Chapter 4, where we first introduced directed acyclic graphs (DAGs) and the intuition behind interventions versus observations. Basic probability (conditional distributions, Bayes' theorem, independence) from Appendix A is essential. Familiarity with Chapter 25: Exploratory Discovery will help you appreciate why moving beyond correlational analysis matters.

The Big Picture

A structural causal model (SCM) is a mathematical object that encodes not just what variables are associated, but how they generate each other. Where a joint probability distribution \(P(X_1, \ldots, X_n)\) tells you what to expect from passive observation, an SCM tells you what would happen if you intervened: forced a variable to take a specific value, breaking its natural generating mechanism. This distinction between seeing and doing is the heart of causal reasoning, and the SCM formalism makes it mathematically precise. The tools developed here let you translate a scientific hypothesis into a formal causal model, determine whether a causal effect is identifiable from observational data, and compute that effect using the do-calculus.

1. The Four-Tuple Formalism

When a pharmaceutical company asks whether its new drug truly lowers blood pressure or merely correlates with patients who were already healthier, the answer depends on four mathematical objects that, together, separate genuine causes from statistical mirages. A structural causal model is a four-tuple \(\mathcal{M} = (V, U, F, P_U)\), and each component captures a distinct aspect of that causal story:

The four-tuple formalism provides the minimal mathematical specification needed to answer "what if?" questions about a system. \(V\) names what you measure. \(U\) captures everything you do not measure. \(F\) encodes the directional mechanisms by which causes produce effects. \(P_U\) injects the randomness that makes outcomes probabilistic rather than deterministic. Without all four components, you cannot distinguish genuine causal effects from confounded associations. The formalism forces you to commit to a specific generative story before computing any effects. The mechanism is straightforward: given a draw from \(P_U\), each structural equation in \(F\) fires in topological order (where every parent node is evaluated before its children) along the DAG, deterministically producing every endogenous variable from its direct causes and its noise term. Use an SCM when your scientific question involves interventions or counterfactuals; use a standard probabilistic graphical model (such as a Bayesian network) when prediction under passive observation is sufficient and causal commitments are unnecessary.

S Soil Quality T Treatment Y Yield confounds confounds causal effect
Figure 31.1: The fertilizer SCM as a directed acyclic graph. Soil quality (\(S\)) confounds the relationship between treatment (\(T\)) and yield (\(Y\)) by causing both. The direct causal path \(T \to Y\) is the effect of interest; the backdoor path \(T \leftarrow S \to Y\) introduces confounding bias.

The structural equations are not regression equations, and this distinction is crucial. A regression \(Y = \beta X + \epsilon\) is a statistical statement about association: it describes how to predict \(Y\) from \(X\) in the observed data. A structural equation \(Y := f(X, U_Y)\) is a causal statement about mechanism: it asserts that the value of \(Y\) is generated by applying \(f\) to the values of its causes \(X\) and noise \(U_Y\). The asymmetric notation $:=$ (or \(\leftarrow\)) emphasizes that the left-hand variable is produced by the right-hand side, not merely correlated with it. In short: an SCM is a bet on the wiring diagram of reality; get the arrows right, and the math tells you what every intervention will do before you touch a single variable.

Key Insight: Structural Equations Encode Mechanisms, Not Associations

The regression equation \(Y = 0.3X + \epsilon\) and the structural equation \(Y := 0.3X + U_Y\) look identical on paper, but they answer fundamentally different questions. The regression tells you: "among units with \(X = x\), the expected value of \(Y\) is \(0.3x\)." The structural equation tells you: "if you set \(X\) to \(x\) by intervention (regardless of what \(X\) would have been naturally), \(Y\) will be \(0.3x + U_Y\)." The regression coefficient may absorb confounding; the structural coefficient isolates the direct mechanism. This is why Pearl insists that "behind every causal conclusion there must lie some causal assumption that is not testable in observational studies."

Let us build a concrete SCM for a scientific scenario. Suppose we study the effect of a new fertilizer (\(T\)) on crop yield (\(Y\)), where soil quality (\(S\)) affects both the farmer's decision to use the fertilizer and the yield itself:

$$S := U_S$$ $$T := f_T(S, U_T) = \mathbb{1}[0.5 \cdot S + U_T > 0]$$ $$Y := f_Y(T, S, U_Y) = 2T + 0.8S + U_Y$$

Here \(S\) is a confounder: it causes both treatment (\(T\)) and outcome (\(Y\)). If we simply regress \(Y\) on \(T\), the coefficient will absorb part of the effect of \(S\), inflating the apparent benefit of the fertilizer. The SCM makes this confounding structure explicit through the DAG implied by the structural equations, as shown in Figure 31.1.

2. Directed Acyclic Graphs and d-Separation

Every SCM \(\mathcal{M}\) induces a DAG \(\mathcal{G}\) where each node represents an endogenous variable and each directed edge \(V_i \to V_j\) indicates that \(V_i\) appears as an argument in the structural equation for \(V_j\). The DAG encodes the qualitative causal structure: which variables directly cause which others. In our fertilizer example, the DAG has edges \(S \to T\), \(S \to Y\), and \(T \to Y\).

The graph's power comes from d-separation, a graphical criterion that reads conditional independence relations directly from the DAG structure. Two variables \(X\) and \(Y\) are d-separated by a set \(Z\) (written \(X \perp\!\!\!\perp_{\mathcal{G}} Y \mid Z\)) if every path between \(X\) and \(Y\) is "blocked" by \(Z\). A path is blocked by \(Z\) if it contains:

Common Misconception

A widespread misconception is that conditioning on more variables always reduces bias and moves you closer to the true causal effect. In fact, conditioning on a collider (a variable caused by both \(X\) and \(Y\), or their descendants) opens a previously blocked path and introduces spurious association where none existed. For example, if talent and luck both cause success, then among successful people (conditioning on the collider "success"), talent and luck appear negatively correlated even if they are independent in the population. The correct rule is not "control for everything you can measure" but rather "control for exactly the variables that block all backdoor paths without opening collider paths," which requires knowing the causal graph.

The causal Markov condition (the assumption that each variable is independent of its non-descendants given its direct causes) connects the graph to the probability distribution. If the DAG \(\mathcal{G}\) is the causal graph for the data-generating process, every d-separation in \(\mathcal{G}\) implies a conditional independence in the observed distribution \(P(V)\). The converse, the faithfulness assumption (the assumption that the only conditional independencies in \(P(V)\) are those implied by d-separation in the graph, ruling out coincidental cancellations among parameters), asserts that d-separation accounts for all conditional independencies in \(P(V)\). Together, these two assumptions let you read testable predictions from the graph and, running in reverse, learn graph structure from conditional independence tests (as covered in Section 31.2).

import networkx as nx
import matplotlib.pyplot as plt

# Build the fertilizer DAG
dag = nx.DiGraph()
dag.add_edges_from([
    ("SoilQuality", "Treatment"),
    ("SoilQuality", "Yield"),
    ("Treatment", "Yield"),
])

# Check d-separation: is Treatment independent of Yield given SoilQuality?
# In this DAG, conditioning on SoilQuality blocks the backdoor path
# but also blocks the direct causal path T -> Y (it doesn't, actually,
# because SoilQuality is not on the direct path T -> Y).
# Let's verify with networkx's d-separation check:
is_dsep = nx.d_separated(dag, {"Treatment"}, {"Yield"}, {"SoilQuality"})
print(f"T _||_ Y | S? {is_dsep}")  # False: direct path T -> Y remains open

# What about T _||_ Y | {} (unconditionally)?
is_dsep_empty = nx.d_separated(dag, {"Treatment"}, {"Yield"}, set())
print(f"T _||_ Y | {{}}? {is_dsep_empty}")  # False: both paths open
Listing 31.1: Building a causal DAG with NetworkX and testing d-separation. The direct path \(T \to Y\) keeps Treatment and Yield associated even after conditioning on SoilQuality.
Practical Example: From Domain Knowledge to DAG

Consider a gene expression study where we measure a transcription factor (\(TF\)), a target gene (\(G\)), cell proliferation (\(P\)), and a batch effect (\(B\)). Domain knowledge tells us: the transcription factor regulates the target gene (\(TF \to G\)); the target gene drives proliferation (\(G \to P\)); and the batch effect introduces systematic variation in both measured expression levels (\(B \to TF\), \(B \to G\), \(B \to P\)). This gives us the DAG with edges \(\{B \to TF, B \to G, B \to P, TF \to G, G \to P\}\). From this graph, we can immediately see that the total effect of \(TF\) on \(P\) is mediated entirely through \(G\) (the only directed path is \(TF \to G \to P\)), and that \(B\) confounds every pairwise association. Without conditioning on \(B\), a naive correlation between \(TF\) and \(P\) would mix causal signal with batch artifact.

3. The do-Operator and Interventions

Knowing which variables are d-separated in a graph tells us what the data alone can and cannot reveal, but the real payoff of encoding causal structure is the ability to predict what happens when we actively change a variable rather than passively observe it.

The central innovation of Pearl's framework is the do-operator, which formalizes the difference between observing that \(X = x\) and intervening to set \(X = x\). The interventional distribution \(P(Y \mid \text{do}(X = x))\) describes the distribution of \(Y\) that would result from physically forcing \(X\) to equal \(x\), overriding whatever natural process normally generates \(X\).

Mental Model

Think of the do-operator as the difference between reading a thermometer and setting a thermostat. When you read the thermometer and see 30 degrees Celsius, you can predict that ice cream sales are high, because hot weather causes both high thermometer readings and high ice cream demand. But if you set the thermostat to 30 degrees (an intervention), you have broken the thermometer's connection to the weather; the reading of 30 is now produced by your action, not by the sun. Ice cream sales will not change, because you have overridden the mechanism that normally generates the temperature reading without touching the mechanism that drives ice cream purchases. The do-operator performs exactly this "thermostat move" in the math: it replaces the natural equation for a variable with a fixed value, severing all incoming arrows in the graph while leaving every other mechanism intact.

Mechanically, the do-operator works by graph surgery (we walk through a concrete numeric example in the step-through box below): to compute \(P(Y \mid \text{do}(X = x))\), we take the original SCM, delete all edges pointing into \(X\) (removing the influence of \(X\)'s natural causes), set \(X = x\), and propagate forward through the remaining structural equations. This yields the truncated factorization (the joint distribution rewritten with the intervened variable's factor removed, so that \(X\) no longer depends on its original causes): Figure 31.1.1 illustrates graph surgery and the do-operator.

Graph surgery and the do-operator
Figure 31.1.1: Graph surgery for the do-operator. The original DAG (left) includes the backdoor path through confounders; the interventional graph (right) severs all incoming edges to the treatment variable, isolating the causal effect from confounding.
$$P(v_1, \ldots, v_n \mid \text{do}(X = x)) = \prod_{i: V_i \neq X} P(v_i \mid \text{pa}(v_i)) \bigg|_{X=x}$$

Compare this with the ordinary conditional distribution \(P(Y \mid X = x)\), which conditions on observing \(X = x\) without changing the data-generating process. When \(X\) has causes that also affect \(Y\) (confounders), \(P(Y \mid X = x)\) and \(P(Y \mid \text{do}(X = x))\) diverge. The do-operator captures the causal effect; the conditional captures the associational relationship.

import numpy as np
import pandas as pd

np.random.seed(42)
n = 10_000

# Generate data from the fertilizer SCM
U_S = np.random.normal(0, 1, n)
U_T = np.random.normal(0, 1, n)
U_Y = np.random.normal(0, 1, n)

S = U_S  # Soil quality
T = (0.5 * S + U_T > 0).astype(float)  # Treatment (binary)
Y = 2 * T + 0.8 * S + U_Y  # Yield

data = pd.DataFrame({"S": S, "T": T, "Y": Y})

# Naive (confounded) estimate: E[Y|T=1] - E[Y|T=0]
naive_ate = data[data.T == 1]["Y"].mean() - data[data.T == 0]["Y"].mean()
print(f"Naive ATE (confounded): {naive_ate:.3f}")  # ~2.4, biased upward

# True ATE from the structural equation: coefficient of T in Y := 2T + 0.8S + U_Y
print(f"True ATE: 2.000")

# Adjustment formula: E[Y|do(T=t)] = sum_s E[Y|T=t, S=s] P(S=s)
# With continuous S, we use regression adjustment
from sklearn.linear_model import LinearRegression

model = LinearRegression()
model.fit(data[["T", "S"]], data["Y"])
print(f"Adjusted ATE (regression): {model.coef_[0]:.3f}")  # ~2.0, correct
Listing 31.2: Demonstrating confounding bias and the adjustment formula. The naive estimate inflates the treatment effect because soil quality confounds the treatment-outcome relationship. Adjusting for \(S\) recovers the true causal effect.

Step-Through: Graph Surgery for the do-Operator

Trace through the do-operator on the fertilizer SCM with concrete numbers. Start with the original DAG: \(S \to T\), \(S \to Y\), \(T \to Y\) and the structural equations \(S := U_S\), \(T := \mathbb{1}[0.5S + U_T > 0]\), \(Y := 2T + 0.8S + U_Y\).

Step 1 (Graph surgery): To compute \(P(Y \mid \text{do}(T = 1))\), delete the edge \(S \to T\). The modified DAG has only \(S \to Y\) and \(T \to Y\). Replace the equation for \(T\) with $T := 1$.

Step 2 (Propagate with a specific draw): Suppose \(U_S = 0.6\), \(U_Y = -0.3\). Then \(S = 0.6\) (unchanged; surgery only touched \(T\)). \(T = 1\) (forced). $Y = 2(1) + 0.8(0.6) + (-0.3) = 2 + 0.48 - 0.3 = 2.18$.

Step 3 (Contrast with do(\(T = 0\))): Same noise draw, but set $T := 0$. $Y = 2(0) + 0.8(0.6) + (-0.3) = 0.48 - 0.3 = 0.18$. The individual causal effect for this draw is \(2.18 - 0.18 = 2.00\), matching the structural coefficient of \(T\) in the yield equation.

Step 4 (Compare with conditioning): Without surgery, if we simply observe \(T = 1\), the equation for \(T\) tells us \(0.5S + U_T > 0\), so the distribution of \(S\) among treated units is shifted upward. This selection effect inflates \(E[Y \mid T = 1]\) relative to \(E[Y \mid \text{do}(T = 1)]\).

4. The Backdoor Criterion

The adjustment formula in Listing 31.2 worked because soil quality \(S\) satisfied the backdoor criterion. A set of variables \(Z\) satisfies the backdoor criterion relative to an ordered pair \((X, Y)\) in a DAG \(\mathcal{G}\) if:

  1. No node in \(Z\) is a descendant of \(X\).
  2. \(Z\) blocks every path between \(X\) and \(Y\) that contains an arrow into \(X\) (a "backdoor path").

When a set \(Z\) satisfies the backdoor criterion, the causal effect of \(X\) on \(Y\) is identified by the backdoor adjustment formula:

$$P(Y = y \mid \text{do}(X = x)) = \sum_{z} P(Y = y \mid X = x, Z = z) \, P(Z = z)$$

This formula converts the interventional query (left side) into a purely observational computation (right side): condition on \(X\) and \(Z\), then marginalize over \(Z\). The key insight is that conditioning on a valid adjustment set \(Z\) blocks all non-causal (backdoor) paths between \(X\) and \(Y\) while leaving all causal (directed) paths open.

Key Insight: Not Everything Should Be Conditioned On

A common mistake is to "control for everything" by throwing all available covariates into the adjustment set. This can introduce bias rather than remove it. Conditioning on a mediator (a variable on the causal path from \(X\) to \(Y\)) blocks part of the causal effect you are trying to estimate. Conditioning on a collider (a common effect of \(X\) and \(Y\), or their descendants) opens a spurious path that was previously blocked. The backdoor criterion tells you exactly which variables to include: enough to block all backdoor paths, but not so many that you block causal paths or open collider paths. This is why causal graphs are indispensable: without a graph, there is no principled way to choose an adjustment set.

5. The Front-Door Criterion

The backdoor criterion requires that we can measure and adjust for all confounders. When some confounders are unmeasured, the backdoor criterion fails. The front-door criterion provides an alternative identification strategy when there is a fully observed mediator \(M\) on the causal path from \(X\) to \(Y\).

A set of variables \(M\) satisfies the front-door criterion relative to \((X, Y)\) if:

  1. \(M\) intercepts all directed paths from \(X\) to \(Y\).
  2. There is no unblocked backdoor path from \(X\) to \(M\).
  3. All backdoor paths from \(M\) to \(Y\) are blocked by \(X\).

When the front-door criterion holds, the causal effect is identified by:

$$P(Y \mid \text{do}(X = x)) = \sum_{m} P(M = m \mid X = x) \sum_{x'} P(Y \mid M = m, X = x') \, P(X = x')$$

The classic example is the smoking-cancer-tar deposits triangle. Suppose an unmeasured genetic factor \(U\) affects both smoking behavior and lung cancer risk. We cannot use the backdoor criterion because \(U\) is unobserved. But if all of smoking's effect on cancer goes through tar deposits in the lungs, and if there is no direct path from genetics to tar deposits except through smoking, then tar deposits satisfy the front-door criterion, and we can identify the causal effect of smoking on cancer.

import dowhy
from dowhy import CausalModel

# Simulate front-door scenario
np.random.seed(42)
n = 5_000

U = np.random.normal(0, 1, n)  # Unmeasured confounder
X = 0.6 * U + np.random.normal(0, 1, n)  # Treatment (affected by confounder)
M = 0.7 * X + np.random.normal(0, 0.5, n)  # Mediator (only caused by X)
Y = 0.9 * M + 0.5 * U + np.random.normal(0, 0.5, n)  # Outcome (M + confounder)

df = pd.DataFrame({"X": X, "M": M, "Y": Y})

# True effect of do(X) on Y through M: 0.7 * 0.9 = 0.63
print(f"True causal effect (X -> M -> Y): {0.7 * 0.9:.2f}")

# DoWhy with front-door specification
model = CausalModel(
    data=df,
    treatment="X",
    outcome="Y",
    graph="""digraph {
        U [unobserved];
        U -> X; U -> Y;
        X -> M; M -> Y;
    }""",
)

# Identify the estimand (should find front-door)
identified = model.identify_effect(proceed_when_unidentifiable=True)
print(identified)

# Estimate using the front-door formula
estimate = model.estimate_effect(
    identified,
    method_name="frontdoor.two_stage_regression",
)
print(f"Front-door estimate: {estimate.value:.3f}")
Listing 31.3: Front-door identification with DoWhy. Despite the unmeasured confounder \(U\) that blocks backdoor adjustment, the mediator \(M\) satisfies the front-door criterion, enabling causal identification. The true effect \(0.7 \times 0.9 = 0.63\) is recovered.

Real-World Application: Uber's Surge Pricing

Uber's marketplace team has reportedly used structural causal models to estimate how surge pricing affects rider demand. The challenge is that surge prices are triggered by high demand, creating a feedback loop (confounder) between price and ride requests. By encoding the time-of-day, weather, and local event schedule as exogenous causes of both surge activation and rider intent, the team applies backdoor adjustment to isolate the causal elasticity of price on completed rides. This SCM-based approach replaced earlier A/B tests that were operationally expensive and ethically fraught (randomly charging different prices in the same area), enabling continuous causal monitoring from observational logs alone.

Real-World Application: Uber's Surge Pricing
Real-World Application: Uber's Surge Pricing
Library Shortcut: DoWhy's Four-Step Workflow

The code above demonstrates DoWhy's signature four-step workflow: (1) Model: specify the causal graph; (2) Identify: determine which estimand applies (backdoor, front-door, instrumental variable, meaning an exogenous source of variation that affects \(X\) but influences \(Y\) only through \(X\)); (3) Estimate: compute the effect using a chosen estimator; (4) Refute: stress-test the estimate with robustness checks. What takes dozens of lines to implement from scratch (graph surgery, adjustment formulas, variance estimation) reduces to four method calls. DoWhy handles the graph theory, identification logic, and estimator selection internally, letting you focus on the scientific question. As of 2024, DoWhy v0.11+ also offers a gcm (Graphical Causal Model) module that supports full SCM-level reasoning, including counterfactual inference, root-cause analysis via Shapley values, and anomaly attribution; the four-step API shown here remains supported alongside this newer interface. We will use the refutation step extensively in Section 31.4.

6. Beyond Backdoor and Front-Door: The do-Calculus

The backdoor and front-door criteria each handle a specific graph topology, but many real-world causal graphs do not fit neatly into either pattern, motivating a more general identification machinery.

The backdoor and front-door criteria are special cases of Pearl's do-calculus, a complete set of three inference rules that can determine whether any causal query is identifiable from any combination of observational and interventional data. The three rules govern when we can:

  1. Rule 1 (Insertion/deletion of observations): Add or remove a conditioning variable when it is d-separated from \(Y\) in a modified graph.
  2. Rule 2 (Action/observation exchange): Replace \(\text{do}(X = x)\) with observation \(X = x\) when \(X\) is d-separated from \(Y\) in the graph with incoming edges to \(X\) removed.
  3. Rule 3 (Insertion/deletion of actions): Remove \(\text{do}(X = x)\) when \(X\) has no causal effect on \(Y\) in a modified graph.

The do-calculus is provably complete (Huang and Valtorta, 2006; Shpitser and Pearl, 2006): if a causal effect is identifiable from the graph, the three rules can derive the identifying formula; if not, no formula exists. Automated algorithms in DoWhy, Ananke, and causaldag apply these rules, so you rarely derive formulas by hand. Understanding the framework still matters: it tells you why a particular effect is or is not identifiable, guiding study design and measurement strategy.

Research Frontier: Causal Reasoning in Foundation Models

Beyond using large language models (LLMs) as sources of causal prior knowledge (Kiciman et al., 2023), recent work pushes toward tighter integration of causal inference with foundation models. Several benchmark efforts, including CausalBench (Chevalley et al., 2023, focused on gene regulatory networks) and the CLADDER evaluation suite (Jin et al., 2023), provide ground-truth causal graphs and multi-rung reasoning tasks for systematically evaluating both classical algorithms and LLM-guided discovery pipelines. Early results suggest that hybrid pipelines, which use an LLM to propose candidate edge orientations and then validate them with conditional independence tests from data, can sometimes outperform either approach alone on moderately sized graphs. Concurrently, multiple groups have shown that GPT-4 and similar frontier models can serve as zero-shot structural priors, reducing the search space for score-based algorithms like Greedy Equivalence Search (GES) by an order of magnitude (circa 2024). These developments connect causal inference to the hypothesis generation methods of Chapter 39 and the AI scientist architectures of Chapter 53, where autonomous agents must reason about causal structure to design informative experiments.

7. Potential Outcomes and the Bridge Between Frameworks

The SCM framework and do-calculus provide one complete language for causal reasoning, but an alternative formulation, rooted in the statistics of randomized experiments, offers complementary intuitions that prove equally valuable in practice.

The SCM framework (Pearl's approach) coexists with the potential outcomes framework (Rubin's approach, also called the Rubin Causal Model). In the potential outcomes framework, each unit \(i\) has two potential outcomes: \(Y_i(1)\) (outcome under treatment) and \(Y_i(0)\) (outcome under control). The individual treatment effect is \(\tau_i = Y_i(1) - Y_i(0)\), but we can only ever observe one of these for any given unit. This is the fundamental problem of causal inference.

Equivalence of the Two Frameworks

The two frameworks are mathematically equivalent under standard assumptions (no interference between units, well-defined interventions): the structural equation \(Y := f(T, S, U_Y)\) generates potential outcomes \(Y(t) = f(t, S, U_Y)\), and the do-operator \(P(Y \mid \text{do}(T = t))\) equals the potential outcome distribution \(P(Y(t))\) under standard assumptions. The practical difference is one of emphasis: the SCM framework foregrounds the causal graph and identification theory; the potential outcomes framework foregrounds the assignment mechanism and estimation. We use both languages throughout this chapter, choosing whichever is more natural for the task at hand.

The key assumptions that connect the two frameworks are:

Checkpoint

So far: an SCM encodes causal mechanisms as structural equations over a DAG; the do-operator uses graph surgery to convert interventional queries into observational formulas; the backdoor and front-door criteria are specific conditions under which this conversion succeeds; and the potential outcomes framework restates the same ideas in the language of missing counterfactual data.

# Demonstrate the fundamental problem of causal inference
np.random.seed(42)
n = 1_000

# Simulate potential outcomes (in practice, we never see both)
S = np.random.normal(0, 1, n)  # Covariate (soil quality)
Y0 = 0.8 * S + np.random.normal(0, 1, n)  # Potential outcome under control
Y1 = 2 + 0.8 * S + np.random.normal(0, 1, n)  # Potential outcome under treatment

# True individual treatment effects (never observable in practice)
true_ite = Y1 - Y0
print(f"True ATE: {true_ite.mean():.3f}")  # ~2.0
print(f"True ATT: {true_ite[T.astype(bool)[:n]].mean():.3f}")

# In reality, we observe only one potential outcome per unit
T_obs = (0.5 * S + np.random.normal(0, 1, n) > 0).astype(float)
Y_obs = T_obs * Y1 + (1 - T_obs) * Y0  # Switching equation

# The "missing data" view of causal inference
observed = pd.DataFrame({
    "S": S, "T": T_obs, "Y": Y_obs,
    "Y0_true": Y0, "Y1_true": Y1,  # Oracle columns (never available)
})
print(f"\nObserved data has {observed.shape[0]} rows")
print(f"Treatment group: {int(T_obs.sum())} units")
print(f"Control group: {int((1 - T_obs).sum())} units")
print(f"Missing potential outcomes per unit: 1 (always)")
Listing 31.4: The fundamental problem of causal inference illustrated. We simulate both potential outcomes \(Y(0)\) and \(Y(1)\) but observe only one per unit. The true individual treatment effect \(\tau_i = Y_i(1) - Y_i(0)\) is never directly observable; all causal inference methods are strategies for estimating population summaries of these unobservable quantities.
Fun Note: The Philosopher's Stone of Statistics

The potential outcomes framework reveals that causal inference is, at its core, a missing data problem. For each unit, we observe \(Y(1)\) or \(Y(0)\), never both. If we could observe both, causal inference would be trivial: just compute \(Y(1) - Y(0)\). Entire fields (epidemiology, econometrics, political science, clinical medicine) are built around clever strategies for estimating the missing half. As Holland (1986) put it in his influential paper: "No causation without manipulation." Or as a reader once paraphrased: "No causation without imagination, because you have to imagine what would have happened."

8. Identification in Practice

For real scientific problems, the identification workflow proceeds as follows:

  1. Specify the DAG from domain knowledge. List all relevant variables, draw directed edges based on known or plausible mechanisms, and mark unmeasured variables as latent.
  2. State the target quantity: typically \(P(Y \mid \text{do}(X = x))\) for the total effect, or a conditional variant for heterogeneous effects.
  3. Search for a valid adjustment set using the backdoor criterion. Tools like DAGitty (Textor et al., 2016) enumerate all valid adjustment sets for a given DAG.
  4. If no adjustment set exists, check the front-door criterion or apply the do-calculus. If the effect remains non-identifiable, consider what additional measurements would enable identification (this is an experiment design problem, connecting to Chapter 46).
  5. Perform sensitivity analysis to quantify how robust your conclusions are to violations of the causal assumptions (covered in Section 31.3).
# Using DoWhy to automate identification
import dowhy
from dowhy import CausalModel

# A more complex DAG with multiple confounders
data_complex = pd.DataFrame({
    "age": np.random.normal(50, 10, n),
    "income": np.random.normal(50000, 15000, n),
})
data_complex["exercise"] = (
    0.02 * data_complex["age"]
    - 0.00001 * data_complex["income"]
    + np.random.normal(0, 1, n)
)
data_complex["health_score"] = (
    3 * data_complex["exercise"]
    - 0.05 * data_complex["age"]
    + 0.00002 * data_complex["income"]
    + np.random.normal(0, 2, n)
)

model = CausalModel(
    data=data_complex,
    treatment="exercise",
    outcome="health_score",
    common_causes=["age", "income"],  # Confounders
)

# Step 2: Identify
identified_estimand = model.identify_effect()
print("Identified estimand:")
print(identified_estimand)

# Step 3: Estimate
estimate = model.estimate_effect(
    identified_estimand,
    method_name="backdoor.linear_regression",
)
print(f"\nEstimated causal effect of exercise on health: {estimate.value:.3f}")
print(f"True causal effect: 3.000")
Listing 31.5: End-to-end causal identification and estimation with DoWhy. The library automatically finds the backdoor adjustment set (age, income), selects the appropriate estimand, and computes the causal effect using linear regression adjustment.

Try It: Build and Query Your Own SCM

Put structural causal models into practice with this mini-project using Python, NumPy, pandas, and NetworkX (all installable via pip install numpy pandas networkx dowhy).

  1. Design a DAG from a question you care about. Pick three to five variables from a domain you know (e.g., study hours, sleep, caffeine intake, and exam score). Draw the directed edges on paper, committing to which variable causes which. Encode the DAG with nx.DiGraph() and add_edges_from().
  2. Write structural equations. For each endogenous variable, write a Python function that computes it from its parents plus Gaussian noise: exam_score = 1.5 * study_hours - 0.4 * caffeine + U_exam. Simulate 5,000 samples.
  3. Compute the naive (confounded) estimate. Regress the outcome on the treatment without adjusting for confounders. Record the bias relative to the true coefficient you chose in step 2.
  4. Apply the backdoor adjustment. Use nx.d_separated() to verify your chosen adjustment set blocks all backdoor paths. Then re-run the regression including the adjustment variables and confirm the bias disappears.
  5. Stress-test with DoWhy. Feed your simulated data and DAG into dowhy.CausalModel, call identify_effect() and estimate_effect(), and compare DoWhy's automated estimate to your manual one. Run a placebo refutation (method_name="placebo_treatment_refuter") to verify that a randomly shuffled treatment produces a near-zero effect.

Exercises

  1. (Conceptual) Draw the DAG for the following scenario: a researcher studies whether coffee consumption (\(C\)) affects heart disease risk (\(H\)). Smoking (\(S\)) causes both coffee consumption and heart disease. Exercise (\(E\)) reduces heart disease risk and is negatively correlated with smoking. Genetic predisposition (\(G\)) affects both smoking behavior and heart disease risk but is unmeasured. Identify whether the causal effect of \(C\) on \(H\) is identifiable. If so, state a valid adjustment set; if not, explain what additional measurement would enable identification.
  2. (Coding) Using the fertilizer SCM from this section, simulate 10,000 samples and compare three estimators: (a) the naive difference in means \(E[Y \mid T=1] - E[Y \mid T=0]\), (b) regression adjustment controlling for \(S\), and (c) inverse propensity weighting (where each observation is reweighted by the inverse of its estimated probability of receiving the treatment it actually received) with propensity scores (the conditional probability of treatment given covariates, \(e(S) = P(T=1 \mid S)\)) estimated by logistic regression. Report the bias and standard error of each estimator across 100 simulation runs. Which estimator has the best bias-variance tradeoff?
  3. (Analysis) Use the DAGitty web tool (dagitty.net) to enumerate all valid adjustment sets for the coffee-heart disease DAG from Exercise 1. Then add a mediator \(M\) (arterial inflammation) on the path \(C \to M \to H\). How does the set of valid adjustment sets change? What happens to the estimated total effect if you accidentally condition on \(M\)?

Exercise 31.1.1

Consider an SCM with four variables: rain (\(R\)), sprinkler (\(K\)), wet sidewalk (\(W\)), and slippery sidewalk (\(L\)). Rain causes wet sidewalk directly and also causes the sprinkler to be turned off (\(R \to K\), \(R \to W\), \(K \to W\), \(W \to L\)). Suppose you want to estimate the causal effect of the sprinkler on slipperiness, \(P(L \mid \text{do}(K = 1))\). Write down the DAG, identify whether \(R\) satisfies the backdoor criterion for the pair \((K, L)\), and compute the adjustment formula. Then explain: what goes wrong if you condition on \(W\) instead of \(R\)?

Hint

Draw the DAG and trace all paths from \(K\) to \(L\). There is a directed (causal) path \(K \to W \to L\) and a backdoor path \(K \leftarrow R \to W \to L\). Check whether \(R\) blocks the backdoor path without being a descendant of \(K\). For the second part, note that \(W\) is a mediator on the causal path, so conditioning on it blocks part of the effect you are trying to measure.

Lab: Confounding Bias Under the Microscope

Goal: Build intuition for how confounding strength and sample size interact to produce biased causal estimates, and verify that backdoor adjustment eliminates the bias.

Tools: Python with NumPy, pandas, and statsmodels (or scikit-learn). Optionally, DoWhy for automated identification.

Procedure (20 minutes): Use the fertilizer SCM from this section (\(S := U_S\), \(T := \mathbb{1}[0.5S + U_T > 0]\), \(Y := 2T + 0.8S + U_Y\)). Vary two parameters systematically: (1) the confounding strength (change the 0.5 coefficient in the treatment equation from 0.0 to 2.0 in steps of 0.25) and (2) the sample size (100, 500, 2000, 10000). For each combination, run 200 simulation replicates and record the naive average treatment effect (ATE) estimate (difference in means) and the adjusted ATE estimate (regression controlling for \(S\)). Plot a heatmap of bias (estimated minus true ATE of 2.0) as a function of confounding strength and sample size.

What to observe: The naive estimator's bias should grow approximately linearly with confounding strength and remain roughly constant across sample sizes (bias is not a finite-sample problem). The adjusted estimator's bias should stay near zero across the entire grid. Increasing sample size shrinks the standard error of both estimators but only fixes variance, not bias.

What's Next

Structural causal models give us the language to express causal claims, and identification theory tells us when those claims are testable from data. But all of this assumes we know the causal graph. In practice, the graph itself is often uncertain or unknown. In Section 31.2: Causal Discovery Algorithms, we turn the problem around: given data, can we learn the causal graph? We explore constraint-based algorithms (Peter-Clark (PC), Fast Causal Inference (FCI)) that test conditional independencies, score-based algorithms (GES) that search over graph space, and functional methods (Linear Non-Gaussian Acyclic Model (LiNGAM)) that exploit non-Gaussianity to orient edges.