Part V: Discovery Through Simulation and Optimization
Chapter 44: World Models for Discovery

44.2 Counterfactual Reasoning with World Models

"I asked myself: what would have happened if I had heated the reactor to 350K instead of 300K? Then I realized I could just roll forward the latent state and find out. Being a world model has its perks."

A Prior Distribution That Imagines Alternative Histories

Prerequisites

This section builds directly on the Recurrent State-Space Model (RSSM) architecture and Evidence Lower Bound (ELBO) objective from Section 44.1. Familiarity with causal inference concepts (interventions, do-operator, counterfactuals) from Chapter 31: Causal Discovery and Causal Inference is helpful but not strictly required; the necessary concepts are introduced as they arise. The code examples use the RSSM implementation from Listing 44.1.

The Big Picture

Prediction answers "what will happen?" Counterfactual reasoning answers a harder question: "what would have happened if I had done something differently?" This distinction is central to scientific discovery. A chemist who observes a failed reaction does not just want to predict the next failure; she wants to know whether changing the catalyst, the temperature, or the solvent would have produced a different outcome. World models make this reasoning tractable by providing a differentiable simulator where we can intervene on past actions and propagate the consequences forward. The machinery below implements such interventions, connects them to Pearl's causal hierarchy, and produces a confidence-calibrated counterfactual analysis system.

1. From Prediction to Counterfactuals

In 2016, a pharmaceutical company recalled a blood-pressure drug after post-market analysis revealed that a narrow dosing window, missed during trials, caused dangerous rebounds in a subset of patients. The trial data contained enough signal to flag the problem, but no one could rewind individual patient trajectories and ask "what would have happened at a lower dose?" Counterfactual reasoning with world models makes exactly that question answerable, turning historical data into a laboratory for interventions that were never tried.

Imagine a chemist watches a reaction fail at 300K and wonders: would 350K have saved it? She cannot rewind the experiment and change a single variable while holding everything else constant, but a trained world model can. Given the current state \(z_t = (h_t, s_t)\) and a future action sequence \(a_{t:t+H}\), the imagine() method produces predicted observations \(\hat{o}_{t+1:t+H}\), rolling forward from the present. Counterfactual reasoning demands something different: returning to a past moment, changing one thing, and playing the tape forward again. Figure 44.3 below illustrates this branching structure, showing how the factual and counterfactual trajectories share a common encoded history up to the intervention point and then diverge.

z_0 z_1 z_2 z_3 Intervention point t_0 Factual (original a_t) Counterfactual (alternative a'_t) Posterior encoding (observed history) Prior rollout (no observations) Time
Figure 44.3: Factual vs. counterfactual trajectory branching. The posterior encoder maps the observed history into latent states (blue dots, left). At the intervention point (orange), the trajectory forks: the factual branch (blue, dashed) continues with original actions, while the counterfactual branch (red, solid) substitutes an alternative action and rolls forward via the prior. Growing uncertainty ellipses on the counterfactual branch reflect compounding prediction error over the rollout horizon.

Counterfactual reasoning asks: "what would the outcome have been if a specific variable had taken a different value, while everything else remained the same?" This question matters because it is the only way to evaluate interventions that no one actually tried, the core task of experimental science and evidence-based decision making. The mechanism encodes the observed history into a latent state, preserving all context up to the intervention point. It then substitutes the hypothetical action and propagates dynamics forward under the learned model to produce the alternative trajectory. Use counterfactual analysis rather than simple what-if simulation when you need to reason about a specific observed case rather than a generic starting condition. For generic exploration of action effects from arbitrary states, standard forward rollouts (the imagine() method from Section 44.1) are sufficient and cheaper.

This practical distinction rests on a formal theoretical hierarchy that clarifies exactly what each type of reasoning can and cannot do.

Pearl (2009) formalized three levels of causal reasoning, each strictly more powerful than the last:

  1. Association (seeing): \(P(Y | X)\). "Patients who take the drug have lower blood pressure." This is correlation, observable from passive data.
  2. Intervention (doing): \(P(Y | \text{do}(X))\), where the do-operator \(\text{do}(X)\) denotes forcibly setting a variable to a value rather than passively observing it. "If we administer the drug, blood pressure will decrease." This requires a causal model that distinguishes action from observation.
  3. Counterfactual (imagining): \(P(Y_{X=x'} | X=x, Y=y)\). "Given that this specific patient took the drug and their blood pressure dropped, what would have happened if they had not taken it?" This requires a structural model of the individual case.

A standard neural network trained on observational data operates at Level 1: it captures correlations but cannot distinguish causation from confounding. A world model trained on interventional data (trajectories where actions are applied to the environment) operates at Level 2: its imagine() method implements the do-operator by replacing the action in the dynamics and rolling forward. With additional machinery, we can push it to Level 3: counterfactual reasoning about specific observed trajectories.

Key Insight: The RSSM's Posterior Enables Counterfactuals

The critical ingredient for counterfactual reasoning is the RSSM's posterior encoder. Given an observed trajectory, the posterior maps each observation into a latent state \(z_t = (h_t, s_t)\) that captures all the specific details of that particular trajectory (the actual temperature, the actual catalyst concentration, the actual mixing rate). To ask "what if?", we rewind to a specific time step \(t_0\), keep the posterior state \(z_{t_0}\) (which encodes the actual conditions at that moment), substitute a different action \(a'_{t_0}\), and then roll forward using the prior (since we no longer have real observations for the counterfactual future). The posterior anchors us to the specific case; the prior propagates the consequences of the intervention.

Mental Model

Think of counterfactual reasoning like replaying a chess game from a recorded transcript. You follow the exact moves both players made up to move 14 (encoding the observed trajectory through the posterior). Then you ask: "What if White had moved the bishop instead of the knight on move 14?" You keep the entire board position as it actually was at move 14 (the posterior state), substitute the bishop move (the counterfactual action), and then play out the rest of the game using your best judgment about how both players would respond (the prior). The board position at move 14 anchors you to the specific game that was played; your judgment about subsequent moves is your learned model of chess dynamics. The further you play past move 14, the less confident you should be, because each subsequent move compounds your uncertainty about what would have happened.

2. Implementing Counterfactual Rollouts

The counterfactual procedure has three steps: (1) encode the observed trajectory through the posterior to recover latent states, (2) select an intervention point and substitute a different action, (3) roll forward from the intervention point using the prior. Let us implement this. In short: the posterior anchors the counterfactual to a real history; the prior propagates the road not taken. Figure 44.2.1 illustrates counterfactual rollout procedure with posterior encoding and prior propagation.

Counterfactual rollout procedure with posterior encoding and prior propagation
Figure 44.2.1: The three-phase counterfactual rollout procedure. The observed trajectory (top) is encoded through the posterior to recover latent states. At the intervention point, the counterfactual branch (bottom) substitutes an alternative action and propagates forward using the prior, with multiple stochastic samples producing confidence intervals.
import torch
import numpy as np
from typing import Optional


class CounterfactualAnalyzer:
    """Counterfactual reasoning engine using a trained RSSM.

    Given an observed trajectory and a hypothetical intervention,
    produces counterfactual predictions with confidence estimates.
    """

    def __init__(self, rssm, n_samples: int = 50):
        """
        Args:
            rssm: trained RSSM model (from Listing 44.1)
            n_samples: number of stochastic samples for confidence intervals
        """
        self.rssm = rssm
        self.n_samples = n_samples
        self.rssm.eval()

    @torch.no_grad()
    def encode_trajectory(self, observations, actions):
        """Encode an observed trajectory through the posterior.

        Args:
            observations: (1, T, obs_dim) observed trajectory
            actions: (1, T, act_dim) actions taken

        Returns:
            Lists of deterministic states h_t and stochastic states s_t.
        """
        device = next(self.rssm.parameters()).device
        obs = observations.to(device)
        act = actions.to(device)

        h, s = self.rssm.initial_state(1, device)
        h_states = [h]
        s_states = [s]

        T = obs.shape[1]
        for t in range(T):
            h = self.rssm.sequence_step(h, s, act[:, t])
            s, _, _ = self.rssm.posterior(h, obs[:, t])
            h_states.append(h)
            s_states.append(s)

        return h_states, s_states

    @torch.no_grad()
    def counterfactual_rollout(
        self,
        observations: torch.Tensor,
        actions: torch.Tensor,
        intervention_step: int,
        counterfactual_actions: torch.Tensor,
        horizon: Optional[int] = None,
    ):
        """Compute counterfactual predictions.

        Args:
            observations: (1, T, obs_dim) the observed trajectory
            actions: (1, T, act_dim) the original actions
            intervention_step: time step at which to intervene
            counterfactual_actions: (1, H, act_dim) alternative actions
                starting at intervention_step
            horizon: how many steps to roll out (default: len of
                counterfactual_actions)

        Returns:
            Dictionary with factual predictions, counterfactual
            predictions, and confidence intervals.
        """
        if horizon is None:
            horizon = counterfactual_actions.shape[1]

        device = next(self.rssm.parameters()).device
        counterfactual_actions = counterfactual_actions.to(device)

        # Step 1: encode the observed trajectory
        h_states, s_states = self.encode_trajectory(observations, actions)

        # Step 2: get the latent state at the intervention point
        # (index is intervention_step because h_states[0] is initial)
        h_intervene = h_states[intervention_step]
        s_intervene = s_states[intervention_step]

        # Step 3: factual rollout (continue with original actions)
        factual_obs = []
        factual_rewards = []
        h, s = h_intervene.clone(), s_intervene.clone()
        for t in range(horizon):
            act_idx = intervention_step + t
            if act_idx < actions.shape[1]:
                a = actions[:, act_idx].to(device)
            else:
                a = torch.zeros(1, actions.shape[-1], device=device)
            h = self.rssm.sequence_step(h, s, a)
            s, _, _ = self.rssm.prior(h)
            obs_pred, rew_pred = self.rssm.decode(h, s)
            factual_obs.append(obs_pred)
            factual_rewards.append(rew_pred)

        # Step 4: counterfactual rollout with multiple samples
        cf_obs_samples = []
        cf_rew_samples = []

        for _ in range(self.n_samples):
            h, s = h_intervene.clone(), s_intervene.clone()
            sample_obs = []
            sample_rew = []
            for t in range(horizon):
                a = counterfactual_actions[:, t]
                h = self.rssm.sequence_step(h, s, a)
                s, _, _ = self.rssm.prior(h)
                obs_pred, rew_pred = self.rssm.decode(h, s)
                sample_obs.append(obs_pred)
                sample_rew.append(rew_pred)
            cf_obs_samples.append(torch.stack(sample_obs, dim=1))
            cf_rew_samples.append(torch.stack(sample_rew, dim=1))

        # Aggregate counterfactual samples
        cf_obs_all = torch.stack(cf_obs_samples, dim=0)  # (n_samples, 1, H, obs)
        cf_rew_all = torch.stack(cf_rew_samples, dim=0)

        return {
            "factual_obs": torch.stack(factual_obs, dim=1),
            "factual_rewards": torch.stack(factual_rewards, dim=1),
            "counterfactual_obs_mean": cf_obs_all.mean(dim=0),
            "counterfactual_obs_std": cf_obs_all.std(dim=0),
            "counterfactual_rewards_mean": cf_rew_all.mean(dim=0),
            "counterfactual_rewards_std": cf_rew_all.std(dim=0),
            "counterfactual_obs_samples": cf_obs_all,
            "intervention_step": intervention_step,
            "horizon": horizon,
        }
Listing 44.5: Counterfactual analysis engine. The encoder maps the observed trajectory into posterior latent states. The counterfactual branch departs from the intervention point, using the prior (not the posterior) because no real observations exist for the hypothetical future. Multiple stochastic samples produce confidence intervals.

The distinction between the factual and counterfactual branches is subtle but important. Both start from the same latent state \(z_{t_0}\), which encodes the actual conditions at the intervention point. The factual branch continues with the original actions; the counterfactual branch substitutes different actions. Both use the prior for forward prediction because we want a fair comparison: the factual branch should not have an unfair advantage from seeing the actual observations.

Common Misconception

A frequent mistake is believing that a counterfactual rollout is the same as simply re-running a simulation from different initial conditions. It is not. A counterfactual preserves the exact observed state at the intervention point (encoded by the posterior from real data), then changes only the action while keeping all latent context intact. Re-running from different initial conditions discards the specific history that led to the current state, and therefore answers a fundamentally different question: "what happens from state X?" rather than "what would have happened to this particular trajectory if the action had been different?"

Practical Example: Counterfactual Drug Dosing

A clinical researcher observes a patient's 10-day treatment trajectory: daily blood pressure readings (observations), drug doses (actions), and symptom scores (rewards). On day 3, the clinician increased the dose from 10mg to 20mg. The patient's blood pressure dropped sharply on days 4-5 but rebounded on days 6-7. The counterfactual question: "What would have happened if we had increased to only 15mg on day 3?" The analyzer encodes the full 10-day observed trajectory, rewinds to day 3, substitutes the 15mg action, and rolls forward. The counterfactual prediction (with confidence intervals from 50 stochastic samples) suggests that the 15mg dose would have produced a slower but more sustained blood pressure reduction, without the rebound. This counterfactual insight, which cannot be obtained from the observational data alone, guides the design of future dosing protocols.

3. Confidence Calibration for Counterfactuals

The counterfactual rollouts of the previous section produced predictions with multiple stochastic samples, but the drug dosing example already raised the crucial follow-up: how much should we trust those predictions?

Counterfactual predictions grow less reliable as the hypothetical action diverges from the observed action and as the rollout horizon lengthens. A responsible analysis system must quantify this uncertainty and flag untrustworthy predictions.

We use three complementary measures of counterfactual confidence:

Three Signals of Counterfactual Reliability

Stochastic spread. The standard deviation across multiple stochastic samples from the prior captures aleatoric uncertainty, where aleatoric uncertainty is the irreducible randomness inherent in the system's dynamics (as opposed to epistemic uncertainty, which shrinks with more data). Wide spread means the outcome is highly sensitive to stochastic factors that the model cannot predict deterministically.

Action distance. The distance between the counterfactual action and the observed action, \(\| a'_t - a_t \|\), serves as a proxy for how far we are extrapolating from the training distribution. World models tend to be more reliable when interpolating within the training distribution than when extrapolating beyond it; large action distances warrant skepticism.

Horizon decay. Prediction error compounds over time (we quantify this precisely in Section 44.3). A counterfactual prediction at horizon \(H\) carries roughly \(\sqrt{H}\) times the single-step uncertainty under independent-error assumptions (as implemented in Listing 44.6), though nonlinear error interactions can push the actual growth rate higher. We apply an exponential discount to confidence as the horizon grows.

Checkpoint

So far: counterfactual confidence rests on three independent signals: stochastic spread (how much the model's own samples disagree), action distance (how far the hypothetical action is from what was actually done), and horizon decay (how far into the future we are predicting), and Listing 44.6 below combines them multiplicatively into a single per-step score.

def compute_counterfactual_confidence(
    cf_result: dict,
    original_actions: torch.Tensor,
    counterfactual_actions: torch.Tensor,
    single_step_rmse: float,
    decay_rate: float = 0.15,
) -> dict:
    """Compute confidence scores for counterfactual predictions.

    Args:
        cf_result: output from CounterfactualAnalyzer.counterfactual_rollout
        original_actions: (1, T, act_dim) original action sequence
        counterfactual_actions: (1, H, act_dim) alternative actions
        single_step_rmse: one-step prediction root mean square error (RMSE)
            (from validation)
        decay_rate: exponential decay rate for horizon confidence

    Returns:
        Dictionary with per-step confidence scores and overall rating.
    """
    horizon = cf_result["horizon"]
    t0 = cf_result["intervention_step"]

    # 1. Stochastic spread (normalized by observation magnitude)
    obs_std = cf_result["counterfactual_obs_std"].squeeze(0)  # (H, obs_dim)
    obs_mean = cf_result["counterfactual_obs_mean"].squeeze(0).abs() + 1e-6
    normalized_spread = (obs_std / obs_mean).mean(dim=-1)  # (H,)

    # 2. Action distance at each step
    orig_slice = original_actions[
        :, t0:t0 + horizon
    ].squeeze(0)  # (H, act_dim)
    cf_slice = counterfactual_actions.squeeze(0)[:horizon]
    action_dist = torch.norm(
        cf_slice - orig_slice, dim=-1
    )  # (H,)
    max_action_dist = action_dist.max() + 1e-6
    normalized_action_dist = action_dist / max_action_dist

    # 3. Horizon decay
    steps = torch.arange(horizon, dtype=torch.float32)
    horizon_confidence = torch.exp(-decay_rate * steps)

    # Compound error estimate: RMSE grows roughly as sqrt(H)
    error_estimate = single_step_rmse * torch.sqrt(steps + 1)

    # Combined confidence: product of three factors
    spread_confidence = torch.exp(-2.0 * normalized_spread)
    action_confidence = torch.exp(-1.0 * normalized_action_dist)
    per_step_confidence = (
        spread_confidence * action_confidence * horizon_confidence
    )

    # Overall confidence: geometric mean across steps
    overall_confidence = per_step_confidence.prod().pow(1.0 / horizon).item()

    # Qualitative rating
    if overall_confidence > 0.7:
        rating = "HIGH"
    elif overall_confidence > 0.4:
        rating = "MODERATE"
    elif overall_confidence > 0.2:
        rating = "LOW"
    else:
        rating = "UNRELIABLE"

    return {
        "per_step_confidence": per_step_confidence.numpy(),
        "overall_confidence": overall_confidence,
        "rating": rating,
        "error_estimate_per_step": error_estimate.numpy(),
        "stochastic_spread": normalized_spread.numpy(),
        "action_distance": normalized_action_dist.numpy(),
    }
Listing 44.6: Counterfactual confidence scoring. Three independent uncertainty signals (stochastic spread, action distance, horizon decay) are combined multiplicatively. The qualitative rating (HIGH/MODERATE/LOW/UNRELIABLE) provides a human-readable summary for use in Discovery Workbench dashboards.
Key Insight: Counterfactual Confidence Decays Faster Than Predictive Confidence

A subtle but critical point: counterfactual predictions are less reliable than ordinary predictions at the same horizon, even when both use the same world model. The reason is distributional shift (a mismatch between the states the model encounters during counterfactual rollout and the states it was trained on). Ordinary predictions extrapolate the current trajectory forward; the model has been trained on similar trajectories. A counterfactual applies an action the model may never have seen from this particular state. The further the counterfactual action departs from the behavioral policy (the policy that generated the training data)'s distribution, the more the model must extrapolate, and the less trustworthy its predictions become. This is why the action distance term in the confidence score is essential.

4. Causal Structure Discovery Through Interventions

Beyond answering individual "what-if" questions, systematic counterfactual analysis reveals the causal structure of the environment. By intervening on each action dimension independently and measuring the effect on each observation dimension, we can construct a causal influence matrix that shows which actions affect which outcomes.

def discover_causal_structure(
    analyzer: "CounterfactualAnalyzer",
    observations: torch.Tensor,
    actions: torch.Tensor,
    intervention_step: int,
    perturbation_scale: float = 0.5,
    horizon: int = 5,
) -> np.ndarray:
    """Discover causal structure by systematic intervention.

    For each action dimension, perturb it independently and measure
    the effect on each observation dimension. The resulting matrix
    reveals which actions causally influence which observations.

    Args:
        analyzer: trained CounterfactualAnalyzer
        observations: (1, T, obs_dim) reference trajectory
        actions: (1, T, act_dim) reference actions
        intervention_step: when to intervene
        perturbation_scale: magnitude of perturbation
        horizon: rollout length after intervention

    Returns:
        Causal influence matrix (act_dim, obs_dim) where entry (i, j)
        measures how much action dimension i affects observation dim j.
    """
    act_dim = actions.shape[-1]
    obs_dim = observations.shape[-1]
    influence_matrix = np.zeros((act_dim, obs_dim))

    # Baseline: counterfactual with original actions (should match factual)
    cf_actions_base = actions[:, intervention_step:intervention_step + horizon]

    for act_idx in range(act_dim):
        # Perturb only action dimension act_idx
        cf_actions_plus = cf_actions_base.clone()
        cf_actions_plus[:, 0, act_idx] += perturbation_scale

        cf_actions_minus = cf_actions_base.clone()
        cf_actions_minus[:, 0, act_idx] -= perturbation_scale

        # Run counterfactual rollouts
        result_plus = analyzer.counterfactual_rollout(
            observations, actions, intervention_step,
            cf_actions_plus, horizon
        )
        result_minus = analyzer.counterfactual_rollout(
            observations, actions, intervention_step,
            cf_actions_minus, horizon
        )

        # Measure effect: difference in predicted observations
        delta_obs = (
            result_plus["counterfactual_obs_mean"]
            - result_minus["counterfactual_obs_mean"]
        )
        # Average absolute effect over horizon
        influence = delta_obs.abs().mean(dim=1).squeeze(0).numpy()
        influence_matrix[act_idx] = influence

    # Normalize each row to [0, 1]
    row_maxes = influence_matrix.max(axis=1, keepdims=True)
    row_maxes[row_maxes == 0] = 1.0
    influence_matrix = influence_matrix / row_maxes

    return influence_matrix
Listing 44.7: Causal structure discovery through systematic perturbation. Each action dimension is perturbed independently while all others are held constant. The resulting influence matrix reveals which actions have causal effects on which observations, complementing the correlation-based methods of Chapter 31.

This perturbation-based approach connects directly to the causal discovery methods of Chapter 31. The influence matrix approximates the Jacobian (the matrix of partial derivatives \(\partial f_j / \partial x_i\), measuring how each output dimension responds to an infinitesimal change in each input dimension) of the causal mechanism: \(\frac{\partial \mathbb{E}[o_{t+1}]}{\partial \text{do}(a_t)}\). Unlike the purely observational methods of Chapter 31 (PC algorithm, NOTEARS), this approach uses interventional data by construction: the world model trained on trajectories where actions were applied. It can therefore distinguish genuine causal effects from spurious correlations due to confounders.

Practical Example: Identifying Active Controls in a Bioreactor

A bioprocess engineer controls a bioreactor with five action dimensions: feed rate, temperature setpoint, pH setpoint, dissolved oxygen target, and agitation speed. The observations include cell density, product concentration, nutrient levels, and waste metabolite concentration. Running discover_causal_structure() reveals that feed rate strongly influences cell density and nutrient levels (as expected) but that dissolved oxygen has an unexpectedly strong influence on product concentration (potentially through a metabolic switch). The temperature setpoint shows a strong delayed effect on waste metabolite concentration but minimal effect on cell density at the current operating point. These causal insights, extracted automatically from the world model, suggest that optimizing product yield should focus on dissolved oxygen control rather than the traditionally emphasized temperature profile.

Research Frontier

TD-MPC2 (Hansen et al., 2024, "TD-MPC2: Scalable, Robust World Models for Continuous Control," ICLR 2024) demonstrated that a single world model architecture, trained once across 104 continuous control tasks spanning multiple domains, can support both planning and counterfactual reasoning without per-task fine-tuning. Its implicit latent dynamics (learned via temporal difference objectives rather than explicit reconstruction) may produce tighter counterfactual confidence bounds than reconstruction-based models like the RSSM, because the latent space is optimized directly for action-conditional prediction rather than observation fidelity. This suggests that the next generation of counterfactual analysis tools may not need the explicit posterior/prior separation described in this section; instead, a single learned value-aware dynamics model can serve both forward planning and backward counterfactual queries, with calibration emerging from the TD loss itself.

5. Counterfactual Explanation of Anomalies

Causal structure discovery tells us which actions influence which outcomes in general; the natural next step is using counterfactual reasoning to diagnose why a specific outcome went wrong.

One powerful application of counterfactual reasoning is explaining anomalies detected by the methods of Chapter 30. When an observation deviates from the world model's prediction (a large reconstruction error or a high Kullback-Leibler (KL) divergence between prior and posterior), we can ask: "What action would have been needed to prevent this anomaly?" The answer identifies the controllable cause of the anomalous event.

Real-World Application: DeepMind's Counterfactual Policy Evaluation in Data Centers
Real-World Application: DeepMind's Counterfactual Policy Evaluation in Data Centers
def explain_anomaly(
    analyzer: "CounterfactualAnalyzer",
    observations: torch.Tensor,
    actions: torch.Tensor,
    anomaly_step: int,
    target_obs: torch.Tensor,
    n_optimization_steps: int = 100,
    lr: float = 0.01,
) -> dict:
    """Find the counterfactual action that would have prevented an anomaly.

    Uses gradient-based optimization to find the action at anomaly_step-1
    that would have produced the target (normal) observation at anomaly_step.

    Args:
        analyzer: trained CounterfactualAnalyzer
        observations: (1, T, obs_dim) trajectory containing the anomaly
        actions: (1, T, act_dim) original actions
        anomaly_step: the time step where the anomaly occurred
        target_obs: (obs_dim,) the "normal" observation we wish we had seen
        n_optimization_steps: gradient descent iterations
        lr: learning rate for action optimization

    Returns:
        Dictionary with optimal counterfactual action and explanation.
    """
    rssm = analyzer.rssm
    device = next(rssm.parameters()).device

    # Encode trajectory up to anomaly
    h_states, s_states = analyzer.encode_trajectory(observations, actions)

    # Get state just before the anomaly
    h_pre = h_states[anomaly_step - 1].detach()
    s_pre = s_states[anomaly_step - 1].detach()

    # Initialize counterfactual action from the original
    original_action = actions[:, anomaly_step - 1].to(device).detach()
    cf_action = original_action.clone().requires_grad_(True)
    target = target_obs.to(device)

    optimizer = torch.optim.Adam([cf_action], lr=lr)
    best_action = cf_action.clone().detach()
    best_loss = float("inf")

    # Enable gradients temporarily for optimization
    rssm.train()
    for step in range(n_optimization_steps):
        optimizer.zero_grad()
        h = rssm.sequence_step(h_pre, s_pre, cf_action)
        s, _, _ = rssm.prior(h)
        obs_pred, _ = rssm.decode(h, s)

        loss = torch.nn.functional.mse_loss(obs_pred.squeeze(), target)

        # Regularize: prefer small deviations from original action
        action_reg = 0.1 * torch.norm(cf_action - original_action)
        total_loss = loss + action_reg

        total_loss.backward()
        optimizer.step()

        if total_loss.item() < best_loss:
            best_loss = total_loss.item()
            best_action = cf_action.clone().detach()

    rssm.eval()

    action_delta = (best_action - original_action).squeeze()

    return {
        "original_action": original_action.squeeze().cpu().numpy(),
        "counterfactual_action": best_action.squeeze().cpu().numpy(),
        "action_delta": action_delta.cpu().numpy(),
        "reconstruction_loss": best_loss,
        "explanation": _format_explanation(action_delta.cpu().numpy()),
    }


def _format_explanation(delta: np.ndarray, threshold: float = 0.05) -> str:
    """Generate human-readable explanation from action delta."""
    significant = np.where(np.abs(delta) > threshold)[0]
    if len(significant) == 0:
        return (
            "The anomaly cannot be explained by action changes alone. "
            "It may be due to unobserved external factors."
        )
    parts = []
    for idx in significant:
        direction = "increasing" if delta[idx] > 0 else "decreasing"
        parts.append(
            f"action dimension {idx} by {abs(delta[idx]):.3f} ({direction})"
        )
    return "The anomaly could have been prevented by changing: " + "; ".join(parts)
Listing 44.8: Counterfactual anomaly explanation via gradient-based action optimization. Given an anomalous observation, the optimizer searches for the minimal action change at the previous step that would have produced a normal observation. The L2 regularization term penalizes large deviations from the original action, ensuring the explanation identifies the most plausible single-step intervention.
Common Pitfall: Confusing Counterfactual Explanation with Root Cause

The counterfactual action that "would have prevented" an anomaly is not necessarily the root cause of the anomaly. It is the controllable lever that, if moved, would have changed the outcome. The actual root cause may be an unobserved disturbance (equipment malfunction, contamination, external perturbation) that the world model cannot represent because it was not in the training data. Always present counterfactual explanations as "the action change that would have compensated for the anomaly," not as "the cause of the anomaly." This distinction matters for scientific integrity and for the responsible discovery practices discussed in Chapter 57.

6. Batch Counterfactual Analysis for Experiment Design

The single-trajectory counterfactual analysis above answers one question at a time. For experiment design (Chapter 46), we often want to explore an entire space of counterfactual actions to identify the most informative experiment to run next. This connects counterfactual reasoning to active learning: the best next experiment is the one whose counterfactual prediction has the highest uncertainty (because that is where the world model needs the most data).

def counterfactual_experiment_candidates(
    analyzer: "CounterfactualAnalyzer",
    observations: torch.Tensor,
    actions: torch.Tensor,
    intervention_step: int,
    candidate_actions: torch.Tensor,
    horizon: int = 5,
) -> dict:
    """Rank candidate experiments by counterfactual informativeness.

    For each candidate action, computes the counterfactual prediction
    and its uncertainty. Actions that produce high-uncertainty
    counterfactual predictions are the most informative to test.

    Args:
        analyzer: trained CounterfactualAnalyzer
        observations: (1, T, obs_dim) reference trajectory
        actions: (1, T, act_dim) reference actions
        intervention_step: when to intervene
        candidate_actions: (N, H, act_dim) candidate action sequences
        horizon: rollout horizon

    Returns:
        Dictionary with ranked candidates, uncertainties, and predictions.
    """
    n_candidates = candidate_actions.shape[0]
    uncertainties = []
    predictions = []

    for i in range(n_candidates):
        cf_actions = candidate_actions[i:i+1]
        result = analyzer.counterfactual_rollout(
            observations, actions, intervention_step,
            cf_actions, horizon
        )
        # Uncertainty: mean std across observation dims and horizon
        uncertainty = result["counterfactual_obs_std"].mean().item()
        prediction = result["counterfactual_rewards_mean"].sum().item()
        uncertainties.append(uncertainty)
        predictions.append(prediction)

    uncertainties = np.array(uncertainties)
    predictions = np.array(predictions)

    # Rank by uncertainty (most informative first)
    info_ranking = np.argsort(-uncertainties)
    # Rank by predicted reward (most promising first)
    reward_ranking = np.argsort(-predictions)

    return {
        "uncertainties": uncertainties,
        "predicted_rewards": predictions,
        "informativeness_ranking": info_ranking,
        "reward_ranking": reward_ranking,
        "most_informative_idx": info_ranking[0],
        "most_promising_idx": reward_ranking[0],
    }
Listing 44.9: Ranking candidate experiments by counterfactual informativeness. Each candidate action sequence is rolled out from the same intervention point, and the mean observation standard deviation across stochastic samples measures how uncertain the model is about that candidate's outcome. This connects counterfactual reasoning to the Bayesian experiment design methods of Chapter 46.
Right Tool: Causal Counterfactuals with DoWhy

For counterfactual reasoning on tabular (non-sequential) data, the DoWhy library provides a complete causal inference pipeline including counterfactual estimation. What required 200+ lines of custom code above (for the sequential RSSM-based case) is a five-line call for the tabular case:

import dowhy

model = dowhy.CausalModel(data=df, treatment="temperature",
                          outcome="yield", graph=causal_graph)
identified = model.identify_effect()
estimate = model.estimate_effect(identified, method_name="backdoor.linear_regression")
counterfactual = model.do(x={"temperature": 350})  # do(T=350)
Listing 44.10: Tabular counterfactual estimation with DoWhy. The CausalModel wraps a causal graph and dataset, identify_effect() selects an estimand, and do() computes the interventional distribution for a specified treatment value.

DoWhy handles confounding adjustment, sensitivity analysis, and refutation tests internally. As of 2024, DoWhy is part of the broader PyWhy ecosystem and offers a newer Graphical Causal Model (GCM) API alongside the classic CausalModel interface shown above; the GCM API supports more flexible counterfactual estimation, including intrinsic causal influence and distributional counterfactuals. Use it when your data is tabular and your causal graph is known; use the RSSM-based approach when you have sequential decision data and need multi-step counterfactual rollouts.

7. Integrating Counterfactuals into the Discovery Workbench

In the Discovery Workbench, the CounterfactualAnalyzer extends the world model backend introduced in Section 44.1. When a scientist observes an unexpected experimental result, the Workbench offers three analysis modes:

  1. What-if analysis: the scientist specifies an alternative action and the Workbench shows the counterfactual trajectory with confidence intervals.
  2. Anomaly explanation: the Workbench automatically identifies the minimal action change that would have produced the expected result, using the gradient-based optimization from Listing 44.8.
  3. Next experiment recommendation: the Workbench evaluates a grid of candidate actions using Listing 44.9 and recommends the most informative or most promising experiment to run next.

All three modes feed their results back into the Workbench's experiment registry (Chapter 47), creating a full audit trail of real experiments, counterfactual analyses, and the reasoning that led to each experimental decision.

Try It: Counterfactual Temperature Sweep on a Toy Chemical Reactor

Build a minimal counterfactual analysis pipeline using only NumPy and Matplotlib (no GPU required). The steps:

  1. Simulate a toy exothermic reactor using the ODE ordinary differential equation (ODE) \(\frac{dC}{dt} = -k_0 e^{-E_a / T} C\) (first-order Arrhenius kinetics). Generate 50 trajectories with random temperature profiles \(T(t)\) drawn uniformly from [300K, 400K] over 100 time steps, recording concentration \(C(t)\) as the observation and \(T(t)\) as the action.
  2. Train a small multi-layer perceptron (MLP) dynamics model: given \((C_t, T_t)\), predict \(C_{t+1}\). Split your 50 trajectories into 40 for training and 10 for validation. Use torch.nn.Linear with two hidden layers of 64 units and ReLU activations. Train for 200 epochs with Adam (lr=1e-3).
  3. Pick one validation trajectory. Encode it by feeding the real observations through the model step by step. At step 30, substitute a range of counterfactual temperatures: 310K, 330K, 350K, 370K, 390K. Roll each forward for 50 steps using the trained model.
  4. For each counterfactual temperature, run the true ODE with that temperature from the same \(C_{30}\) to get ground-truth counterfactual trajectories. Compare the model's counterfactual predictions to the ODE ground truth.
  5. Plot all five counterfactual trajectories (model prediction vs. ground truth) on one figure with Matplotlib. Add shading for the model's prediction error. Observe how prediction error grows with both horizon length and deviation from the training distribution's mean temperature.

Exercise 44.2.1

Suppose you have a trained world model for a two-variable system (temperature \(T\) and pressure \(P\)) and an observed trajectory of 20 time steps. You intervene at step 10, changing the temperature action from 350K to 400K. After rolling out the counterfactual for 10 steps, you obtain a stochastic spread (normalized standard deviation) of 0.8 at step 5 of the rollout. Using the confidence formula from Listing 44.6 with decay_rate=0.15, and assuming the action distance term contributes exp(-1.0 * 0.3) = 0.741, compute the per-step confidence at rollout step 5. Is this prediction trustworthy enough to guide a real experiment?

Hint

The three multiplicative factors are: spread confidence = exp(-2.0 * 0.8), action confidence = 0.741, and horizon confidence = exp(-0.15 * 5). Multiply all three. Compare the result to the qualitative thresholds: above 0.7 is HIGH, above 0.4 is MODERATE, above 0.2 is LOW, below 0.2 is UNRELIABLE.

Step-Through: Counterfactual Rollout on a 3-Step Trajectory

Trace the counterfactual_rollout procedure with concrete numbers. Suppose our RSSM has 2-dimensional latent states and 1-dimensional observations and actions.

  1. Observed trajectory: observations = [1.0, 1.5, 2.3], actions = [0.2, 0.4, 0.6].
  2. Encode via posterior: feeding each (obs, action) pair yields posterior states \(z_0 = (h_0, s_0)\), \(z_1 = (h_1, s_1) = ([0.3, 0.7], [0.1, 0.4])\), \(z_2 = (h_2, s_2)\), \(z_3 = (h_3, s_3)\).
  3. Intervention at step 1: we keep \(z_1 = ([0.3, 0.7], [0.1, 0.4])\) and substitute action \(a'_1 = 0.9\) instead of \(a_1 = 0.4\).
  4. Prior rollout step 1: \(h'_2 = \text{GRU}([0.3, 0.7], [0.1, 0.4], 0.9) = [0.5, 0.2]\) (the Gated Recurrent Unit (GRU) now processes the counterfactual action). Sample \(s'_2 \sim \text{prior}(h'_2) = [0.3, 0.1]\).
  5. Decode: \(\hat{o}'_2 = \text{decode}([0.5, 0.2], [0.3, 0.1]) = 2.8\). Compare to the factual \(\hat{o}_2 = 2.3\). The higher counterfactual action (0.9 vs. 0.4) produced a higher predicted observation (2.8 vs. 2.3).
  6. Repeat for the next step using \(h'_2, s'_2\) and the next counterfactual action. Each step compounds uncertainty because \(s'\) is sampled from the prior (no real observation to anchor it).

Real-World Application: DeepMind's Counterfactual Policy Evaluation in Data Centers

Google DeepMind applied counterfactual reasoning with learned world models to evaluate alternative cooling strategies in production data centers without physically testing them. The system encodes the observed thermal trajectory of a data center hall (server loads, ambient temperature, coolant flow rates) into a latent state, then asks: "What would the Power Usage Effectiveness (PUE) have been if we had raised the chilled water setpoint by 2 degrees Fahrenheit?" By running thousands of such counterfactual queries against historical operating data, the team identified safe energy-saving interventions that reduced cooling energy by roughly 40% (circa 2016), all before committing to any physical change.

The Grandmother of All Counterfactuals

The philosophical roots of counterfactual reasoning predate AI by centuries. In 1748, David Hume argued that our very notion of causation is built on counterfactual thinking: we say fire causes heat because we believe that if the fire had not been lit, the room would have remained cold. Pearl's do-calculus, which underlies the machinery in this section, finally gave Hume's intuition a rigorous mathematical foundation 250 years later. The irony: it took a computer scientist to formalize what an 18th-century philosopher articulated over afternoon tea.

Lab: Counterfactual Sensitivity Map for CartPole

Goal: build a visual "sensitivity map" showing how counterfactual outcomes change as you vary both the intervention time and the magnitude of the alternative action in the CartPole environment.

Tools needed: Python 3.8+, PyTorch, Gymnasium (pip install gymnasium), Matplotlib, and the RSSM implementation from Section 44.1 (or a simple 2-layer MLP dynamics model as a stand-in).

Procedure (20 minutes): (1) Collect 200 CartPole episodes using a random policy and train a dynamics model on them. (2) Select one episode where the pole falls at step ~40. (3) For each intervention time \(t_0 \in \{5, 10, 15, 20, 25, 30, 35\}\) and each counterfactual force \(a' \in \{-1.0, -0.5, 0.0, 0.5, 1.0\}\), run a counterfactual rollout of 20 steps and record the predicted pole angle at the final step. (4) Plot the results as a heatmap (x-axis: intervention time, y-axis: counterfactual force, color: final pole angle).

What to vary: try different episodes (ones that fail early vs. late) and different training set sizes (50 vs. 200 episodes). What to observe: the heatmap should show a "corridor of controllability" where early interventions with moderate forces keep the pole upright, while late interventions require extreme forces and produce high-uncertainty predictions (visible as noisy regions in the map).

Exercises

  1. (Conceptual) Explain the difference between Pearl's three levels of causal reasoning (association, intervention, counterfactual). At which level does the RSSM's imagine() method operate? At which level does the CounterfactualAnalyzer operate? Justify your answers.
  2. (Coding) Implement a counterfactual analysis of a CartPole trajectory. Train an RSSM on CartPole trajectories (using the code from Section 44.1), then use the CounterfactualAnalyzer to answer: "At the moment the pole started falling, what force should I have applied to keep it balanced?" Visualize both the factual and counterfactual trajectories.
  3. (Analysis) Run discover_causal_structure() on a trained RSSM for the Pendulum environment. Compare the discovered causal influence matrix to the known physics of the pendulum (torque affects angular velocity, which affects angle). Does the world model recover the correct causal structure?
  4. (Research) The confidence scoring in Listing 44.6 uses three heuristic factors. Design and implement a calibration experiment: generate 1000 counterfactual predictions with known ground truth (by running the actual environment), then check whether the confidence scores are well-calibrated (i.e., 70% confidence predictions are correct 70% of the time). If not, propose and implement a recalibration method.

What's Next

We have used world models to reason about what happened and what might have happened. In Section 44.3: Building a World Model Planner, we turn to the forward-looking question: what should we do? We build a complete model-based planner that selects actions by imagining thousands of future trajectories, and we confront the fundamental challenge of compound error accumulation over long planning horizons.

Bibliography

Pearl, J. (2009). Causality: Models, Reasoning, and Inference, 2nd edition. Cambridge University Press.

The foundational text on causal inference, defining the three-level causal hierarchy (association, intervention, counterfactual) that structures this section.

Hafner, D., Pasukonis, J., Ba, J., & Lillicrap, T. (2023). Mastering diverse domains through world models. arXiv preprint arXiv:2301.04104.

DreamerV3, whose RSSM architecture provides the posterior encoding and prior rollout mechanisms we use for counterfactual reasoning.

Sharma, A. & Kiciman, E. (2020). DoWhy: An end-to-end library for causal inference. arXiv preprint arXiv:2011.04216.

The DoWhy library for causal inference on tabular data, providing a complementary approach to the sequential RSSM-based counterfactual analysis.

Ke, N. R., et al. (2022). Systematic evaluation of causal discovery in visual model-based reinforcement learning. NeurIPS 2022 Workshop on Causal Machine Learning.

Empirical evaluation of causal discovery methods applied to learned world models, showing when perturbation-based approaches succeed and fail.

Buesing, L., et al. (2019). Woulda, coulda, shoulda: Counterfactually-guided policy search. ICLR 2019.

Using counterfactual reasoning in model-based RL to improve policy learning from off-policy data.