26  Interventional Reasoning: Forecasting Under Interventions

Status: Draft

v0.4

26.1 Introduction

Records and identification assumptions support different kinds of forecasts under intervention. Conditional forecasting asks what follows when the observed history includes \(A_t=a\); interventional forecasting asks what follows when the mechanism for \(A_t\) is replaced (Robins 1986; Robins et al. 2000; Pearl 2009). This chapter treats that distinction as Doing from the Observable role.

26.2 Conditional vs Interventional Forecasting

26.2.1 Conditional Forecasting

Conditional forecasting asks: “What follows given the observed history and treatment value?”

\[ P(Y_{t+1} \mid Y_{1:t}, A_t = a) \]

Interpretation: Distribution of future outcomes given past observations and observed treatment value.

When to use:

  • Prediction: Forecast future observations
  • No intervention: We’re not changing anything, just observing
  • Association: Level 1 of causal hierarchy

26.2.2 Interventional Forecasting

Interventional forecasting asks: “What follows when the treatment mechanism is replaced?”

\[ P^{do(A_t = a)}(Y_{t+1} \mid Y_{1:t}) \]

Interpretation: Distribution of future outcomes under intervention \(do(A_t = a)\).

When to use:

  • Policy evaluation: What happens if we implement a policy?
  • Treatment effects: What happens if we give treatment?
  • Intervention: Level 2 of causal hierarchy

26.2.3 When Do They Differ?

If treatment assignment is confounded, conditional and interventional forecasts differ (Pearl 2009; Robins et al. 2000):

  • Conditional: \(P(Y \mid A = a)\) adjusts for confounders via conditioning
  • Interventional: \(P^{do(A = a)}(Y)\) sets treatment structurally, breaking confounder links

Key insight: Conditional forecasting observes what happens naturally; interventional forecasting predicts what happens when we change the system.

26.2.4 Implementation: Conditional vs Interventional Forecasting

The following example makes the difference visible when treatment is confounded:

# Bootstrap book utilities
include(joinpath(dirname(Base.active_project()), "scripts", "book_bootstrap.jl"))

@auto_using Random Distributions GLM DataFrames CairoMakie

Random.seed!(42)

# Simulate data with confounding
# L: Disease severity (confounder)
# A: Treatment (depends on L - confounding!)
# Y: Outcome (depends on L and A)

n = 1000
L = rand(Normal(0, 1), n)  # Confounder
# Treatment assignment: more severe (higher L) → more likely to treat
p_treat = 1 ./ (1 .+ exp.(-(L .- 0.5)))
A = rand.(Bernoulli.(p_treat))
# Outcome: treatment helps, but severity also affects outcome
Y = 0.5 .* A .- 0.8 .* L .+ rand(Normal(0, 0.2), n)

df = DataFrame(L = L, A = A, Y = Y)

# Conditional forecast: P(Y | A = 1)
# This adjusts for confounders via conditioning
conditional_model = lm(@formula(Y ~ A + L), df)
Y_cond_A1 = GLM.predict(conditional_model, DataFrame(A = ones(n), L = L))
conditional_forecast = mean(Y_cond_A1)

# Interventional forecast: P^{do(A = 1)}(Y)
# This sets A = 1 structurally, breaking the L → A link
# We need to marginalise over L: E[Y | do(A=1)] = E_L[E[Y | A=1, L]]
# Use marginal distribution of L (not conditional on A=1)
# Sample L from its marginal distribution
L_marginal = rand(Normal(mean(L), std(L)), n)  # Marginal distribution of L
Y_interv_A1 = GLM.predict(conditional_model, DataFrame(A = ones(n), L = L_marginal))
interventional_forecast = mean(Y_interv_A1)

# The forecasts differ because:
# - Conditional: P(Y | A=1) conditions on observed A=1, which is correlated with L
# - Interventional: P^{do(A=1)}(Y) sets A=1, breaking the correlation

println("Forecast comparison:")
println("  Conditional: E[Y | A=1] = ", round(conditional_forecast, digits=3))
println("  Interventional: E[Y | do(A=1)] = ", round(interventional_forecast, digits=3))
println("\nNote: When treatment is confounded, these differ!")
println("  Conditional adjusts for confounders via conditioning")
println("  Interventional breaks the confounder link structurally")
Forecast comparison:
  Conditional: E[Y | A=1] = 0.539
  Interventional: E[Y | do(A=1)] = 0.547

Note: When treatment is confounded, these differ!
  Conditional adjusts for confounders via conditioning
  Interventional breaks the confounder link structurally
Figure 26.1: Conditional vs interventional forecasting: the difference when treatment is confounded

26.3 Interventional Forecasting in Dynamical Systems

26.3.1 Structural Changes Propagate Through Time

When we intervene \(do(A_t = a)\), we modify the structural equation for \(A_t\):

\[ A_t \coloneqq a \quad \text{(instead of } \pi(H_t, U^A_t) \text{)} \]

This structural change propagates forward through time:

  1. Immediate effect: \(A_t\) is set to \(a\)
  2. Direct effect: \(A_t\) affects \(Y_t\) and future states
  3. Indirect effects: Changes propagate through feedback loops
  4. Long-term effects: System evolves toward new attractor

26.3.2 Computing Interventional Forecasts

Steps:

  1. Set intervention: \(do(A_t = a)\) for all \(t\) in forecast period
  2. Simulate forward: Use state transition \(f\) to propagate effects
  3. Account for feedback: Past outcomes affect future states
  4. Aggregate: Compute expected outcomes or distributions

26.3.3 Example: Treatment Intervention

Forecast patient outcome under treatment intervention:

\[ P^{do(\text{Treatment}_t = \text{drug A})}(\text{Recovery}_{t+1} \mid \text{History}_{1:t}) \]

This requires:

  • Understanding how treatment affects recovery (causal mechanism)
  • Accounting for time-varying confounding
  • Propagating effects through time

26.3.4 Implementation: Interventional Forecasting in Dynamical Systems

Under an intervention, the system can be forecast by simulating it forward with the modified mechanism:

# Bootstrap book utilities
include(joinpath(dirname(Base.active_project()), "scripts", "book_bootstrap.jl"))

@auto_using OrdinaryDiffEq CairoMakie

# Example: Disease model with treatment intervention
# Forecast: What happens if we intervene with treatment at t=5?

β = 0.2  # Disease progression
α = 0.3  # Treatment effectiveness

# Original system (no intervention)
function disease_original!(du, u, p, t)
    """Disease model: original dynamics with confounding (treatment depends on severity)."""
    X = u[1]
    # Treatment assignment depends on severity (confounding)
    A = X > 0.5 ? 1.0 : 0.0
    du[1] = β * X - α * A * X
end

# System under intervention do(A = 1) for t >= 5
function disease_intervened!(du, u, p, t)
    """Disease model: intervened dynamics with do(A = 1) starting at t=5."""
    X = u[1]
    # Intervention: do(A = 1) starting at t=5
    A = t >= 5.0 ? 1.0 : (X > 0.5 ? 1.0 : 0.0)
    du[1] = β * X - α * A * X
end

u0 = [0.3]  # Initial severity
tspan = (0.0, 20.0)

# Solve both systems
prob_original = ODEProblem(disease_original!, u0, tspan)
sol_original = solve(prob_original, Tsit5())

prob_intervened = ODEProblem(disease_intervened!, u0, tspan)
sol_intervened = solve(prob_intervened, Tsit5())

# Extract final-time forecasts for reuse (for both plotting and summary text)
final_original = sol_original.u[end][1]
final_intervened = sol_intervened.u[end][1]

println("Interventional forecast:")
println("  Severity at t=20 (original): ", round(final_original, digits=3))
println("  Severity at t=20 (intervened): ", round(final_intervened, digits=3))
println("  Improvement: ", round((final_original - final_intervened) / final_original * 100, digits=1), "%")
Interventional forecast:
  Severity at t=20 (original): 0.502
  Severity at t=20 (intervened): 0.106
  Improvement: 78.9%
Figure 26.2: Interventional forecasting: propagating intervention effects through time

26.4 Uncertainty in Interventional Forecasts

When making interventional forecasts, we must account for uncertainty (Rackauckas 2026). This uncertainty comes from:

  1. Parameter uncertainty: Uncertainty about model parameters
  2. Model uncertainty: Uncertainty about model structure
  3. Measurement uncertainty: Error in observations used to fit the model
  4. Numerical uncertainty: ODE solver errors

Why it matters: Intervention effect estimates without uncertainty bounds are incomplete. We need to know not just the expected effect, but also how uncertain we are about it.

26.4.1 Prediction Intervals for Interventions

Prediction intervals quantify uncertainty in intervention effects:

\[ P^{do(A=a)}(Y_{t+1} \in [L, U] \mid Y_{1:t}) = 1 - \alpha \]

where \([L, U]\) is the \((1-\alpha)\) prediction interval.

Methods:

  • Monte Carlo: Sample parameters from posterior, run model, compute quantiles
  • Linearisation: Approximate using first-order Taylor expansion
  • Bootstrap: Resample data, refit model, compute intervals

Push-forward from a fitted ATE posterior (RxInfer) is the cheapest Bayesian interval when the forecast is a linear functional of \(\tau\):

include(joinpath(dirname(Base.active_project()), "scripts", "book_bootstrap.jl"))
@auto_using CausalDynamics Graphs DataFrames Random Statistics
using RxInfer

Random.seed!(9)
n = 600
Z = randn(n); X = Z .+ randn(n); Y = 1.2 .* X .+ 0.5 .* Z .+ 0.3 .* randn(n)
g = DiGraph(3); add_edge!(g, 1, 2); add_edge!(g, 1, 3); add_edge!(g, 2, 3)
fit = infer_backdoor_effect(g, DataFrame(Z = Z, X = X, Y = Y), 2, 3;
    node_names = Dict(1 => :Z, 2 => :X, 3 => :Y), iterations = 30)
# Gaussian VI marginal → predictive shift for ΔX = 1
τμ, τσ = fit.τ_mean, sqrt(1 / fit.τ_posterior.w)  # NormalWeightedMeanPrecision
Δ = 1.0
lo, hi = τμ * Δ - 1.96 * τσ * Δ, τμ * Δ + 1.96 * τσ * Δ
println("Forecast shift for ΔX=1: ", round(τμ; digits = 3),
    "  95% ≈ [", round(lo; digits = 3), ", ", round(hi; digits = 3), "]")
Forecast shift for ΔX=1: 1.208  95% ≈ [1.131, 1.285]

Implementation:

# Find project root and include ensure_packages.jl
project_root = let
    current = pwd()
    while !isfile(joinpath(current, "Project.toml")) && !isfile(joinpath(current, "_quarto.yml"))
        parent = dirname(current)
        parent == current && break
        current = parent
    end
    current
end
include(joinpath(dirname(Base.active_project()), "scripts", "book_bootstrap.jl"))

@auto_using OrdinaryDiffEq Distributions Random Statistics CairoMakie

# SIR model
function sir!(du, u, p, t)
    S, I, R = u
    β, γ = p
    du[1] = -β * S * I
    du[2] = β * S * I - γ * I
    du[3] = γ * I
end

# Parameter uncertainty: β ~ N(0.3, 0.05²), γ ~ N(0.1, 0.02²)
Random.seed!(42)
n_samples = 1000
β_samples = rand(Normal(0.3, 0.05), n_samples)
γ_samples = rand(Normal(0.1, 0.02), n_samples)

u0 = [0.99, 0.01, 0.0]
tspan = (0.0, 50.0)

# Intervention: reduce transmission rate (vaccination)
β_intervened = 0.15  # 50% reduction

# Monte Carlo: sample parameters, run model, collect outcomes
# Store pairs to ensure matching samples
outcome_pairs = Tuple{Float64, Float64}[]

for i in 1:n_samples
    # Original (no intervention)
    p_orig = (β_samples[i], γ_samples[i])
    prob_orig = ODEProblem(sir!, u0, tspan, p_orig)
    sol_orig = solve(prob_orig, Tsit5())
    
    # Intervened (vaccination)
    p_int = (β_intervened, γ_samples[i])  # β reduced, γ unchanged
    prob_int = ODEProblem(sir!, u0, tspan, p_int)
    sol_int = solve(prob_int, Tsit5())
    
    # Only keep pairs where both succeeded
    if SciMLBase.successful_retcode(sol_orig.retcode) && SciMLBase.successful_retcode(sol_int.retcode) && 
       length(sol_orig.u) > 0 && length(sol_int.u) > 0
        peak_I_orig = maximum([u[2] for u in sol_orig.u])
        peak_I_int = maximum([u[2] for u in sol_int.u])
        if isfinite(peak_I_orig) && isfinite(peak_I_int)
            push!(outcome_pairs, (peak_I_orig, peak_I_int))
        end
    end
end

# Extract separate arrays
outcomes_original = [p[1] for p in outcome_pairs]
outcomes_intervened = [p[2] for p in outcome_pairs]

# Check if we have enough samples
if length(outcomes_original) == 0
    error("No successful ODE solves. Check model parameters and solver settings.")
end

# Compute prediction intervals (95%)
quantiles_orig = quantile(outcomes_original, [0.025, 0.5, 0.975])
quantiles_int = quantile(outcomes_intervened, [0.025, 0.5, 0.975])

# Intervention effect with uncertainty (now guaranteed to have matching pairs)
effect_samples = outcomes_original .- outcomes_intervened
effect_quantiles = quantile(effect_samples, [0.025, 0.5, 0.975])

println("Uncertainty in interventional forecasts:")
println("  Original peak I: ", round(quantiles_orig[2], digits=3), 
        " (95% CI: [", round(quantiles_orig[1], digits=3), ", ", round(quantiles_orig[3], digits=3), "])")
println("  Intervened peak I: ", round(quantiles_int[2], digits=3), 
        " (95% CI: [", round(quantiles_int[1], digits=3), ", ", round(quantiles_int[3], digits=3), "])")
println("  Intervention effect: ", round(effect_quantiles[2], digits=3), 
        " (95% CI: [", round(effect_quantiles[1], digits=3), ", ", round(effect_quantiles[3], digits=3), "])")
println("  Relative reduction: ", round(effect_quantiles[2] / quantiles_orig[2] * 100, digits=1), "%")
Uncertainty in interventional forecasts:
  Original peak I: 0.298 (95% CI: [0.121, 0.496])
  Intervened peak I: 0.061 (95% CI: [0.011, 0.222])
  Intervention effect: 0.226 (95% CI: [0.087, 0.328])
  Relative reduction: 75.9%
Figure 26.3: Uncertainty quantification for interventional forecasts

26.4.2 Decision-Making Under Uncertainty

When making decisions based on interventional forecasts, we need to account for uncertainty:

  • Robust decisions: Choose interventions that work well across parameter uncertainty
  • Risk assessment: Consider worst-case scenarios
  • Value of information: How much would better parameter estimates improve decisions?

Key Points:

  • Uncertainty quantification is essential for reliable interventional forecasts
  • Prediction intervals provide decision-relevant information
  • Parameter uncertainty propagates through models to create forecast uncertainty
  • Connects to counterfactual reasoning (see Counterfactual Reasoning: Unit-Level Alternatives)

26.5 Stratum context

Interventional forecasting combines records with structural and dynamical assumptions to predict under a changed mechanism; it does not modify an observation event itself.

26.6 Key Takeaways

  1. Conditional vs interventional: Observing vs doing, fundamentally different
  2. Interventional forecasting: Predict what happens under interventions
  3. Structural changes propagate: Interventions affect system evolution through time
  4. Confounding matters: Conditional and interventional forecasts differ when confounded

26.7 Further Reading