Part VII: Building Intelligent Temporal Systems
Chapter 30: Temporal Reasoning and Causality

Counterfactual and What-If Analysis

"Give me the world as it was the morning of the decision, and I will run it forward twice: once as it happened, and once as it might have. The gap between my two endings is the only thing anyone ever wanted from me, and it is the one number I can never check."

A Model Replaying History With One Decision Changed
Big Picture

A counterfactual is the question on the very top rung of the causal ladder: not "what happens on average if we intervene" but "what would have happened to this particular unit had it been treated differently, given everything we actually observed about it." The interventional queries of Section 30.3 averaged over a population; a counterfactual conditions on one realized history and rewrites a single decision inside it. The machinery is a three-step recipe over a structural causal model, abduction then action then prediction: infer the latent noise that explains the world you saw, surgically change the one variable you are asking about, then re-run the model forward with the noise held fixed. In time series this recipe becomes counterfactual forecasting (what would demand have been had we not run the campaign), retrospective what-if for a past decision, and off-policy counterfactual evaluation of an agent's untaken actions. This section derives the recipe with structural causal models, surveys the three method families (structural abduction, CausalImpact-style state-space baselines, and deep counterfactual models), confronts the fundamental problem of causal inference (we never observe both outcomes for the same unit), and builds a counterfactual trajectory from scratch before collapsing it to a library call. It closes Chapter 30 and hands the causal toolkit to the temporal agents of Chapter 31, who will use counterfactuals to reason about actions they have not yet taken.

In Section 30.2 the $\operatorname{do}$-operator let us state interventions, asking what happens to the system if we reach in and set a variable; in Section 30.3 we estimated their population-average effect, an interventional distribution averaged over a population. That is the second rung of Pearl's ladder. This section climbs the third and final rung. The interventional question "what is $\mathbb{E}[Y \mid \operatorname{do}(X = x)]$" speaks about a hypothetical population of units all forced to $X = x$. The counterfactual question is narrower and stranger: take this patient, who actually received treatment $X = 1$ and whose blood pressure we actually watched fall, and ask what their blood pressure would have been had they instead received $X = 0$, holding fixed everything idiosyncratic about them that we cannot see. We are conditioning on a realized outcome and then contradicting part of the history that produced it. That is why counterfactuals sit above interventions: every counterfactual query implies an interventional one (marginalize away the conditioning and you get back the $\operatorname{do}$ expression), but the reverse fails: two systems can share every interventional distribution and still disagree about what would have happened to a specific unit.

We use the unified notation of Appendix A throughout: $\mathbf{x}_t$ the observed covariates, $a_t$ the action or treatment, $y_t$ the outcome of interest, $\mathbf{u}$ the exogenous noise that a structural causal model treats as the seed of a particular history. The reason counterfactual reasoning deserves its own section, rather than a footnote to intervention, is that almost every high-stakes use of a temporal model is secretly counterfactual. "Did the marketing campaign work" is a question about what sales would have been without it. "Should we have throttled that service earlier" is a question about an outage that would not have happened. "Was the treatment responsible for the recovery" is a question no controlled trial on this one patient can answer. The model becomes a device for replaying a recorded history with one decision changed, and the value it produces is the difference between the two replays.

The three competencies this section installs are these: to state and apply the abduction-action-prediction recipe over a structural causal model, deriving a unit-level counterfactual rather than a population intervention; to recognize the three temporal forms of counterfactual question (counterfactual forecasting, retrospective what-if, off-policy evaluation) and match each to a method family; and to build a counterfactual baseline from scratch as a fitted state-space model and then reproduce it with a production library, while never forgetting the fundamental problem of causal inference that no amount of modeling repeals.

1. The Top Rung: Counterfactuals and the Abduction-Action-Prediction Recipe Intermediate

A traveler who took the left fork imagines a translucent ghost of themselves walking the untaken right fork, sharing the same backpack and starting point, picturing a counterfactual replay.
Figure 30.5: A counterfactual rewinds the same actor to the same starting state, then replays the single road they did not take while everything else stays fixed.

A counterfactual is a query about a specific realized unit. Formally, it asks for the distribution of an outcome $Y$ under a hypothetical action, given that we observed the unit's actual covariates and actual outcome under the action it really received. The notation makes the layering visible: $\mathbb{P}\big(Y_{a'} = y' \mid X = x,\, A = a,\, Y = y\big)$ reads "the outcome $Y$ would have been $y'$ under action $a'$, given that we saw covariates $x$, the unit actually took action $a$, and we observed outcome $y$." The subscript $a'$ on $Y$ is the counterfactual action; everything after the conditioning bar is the world that actually happened. Contrast this with the interventional query (the $\operatorname{do}$-operator of Section 30.2, estimated in Section 30.3), $\mathbb{E}[Y \mid \operatorname{do}(A = a')]$, which has no conditioning on a realized outcome at all: it averages over the whole population, while the counterfactual fixes one unit's history and edits it.

The engine that answers such queries is the structural causal model (SCM). An SCM specifies each variable as a deterministic function of its causal parents and an exogenous noise term: $V_i = f_i(\mathrm{pa}_i, U_i)$, where $\mathrm{pa}_i$ are the parents of $V_i$ in the causal graph and $U_i$ is independent exogenous noise. The noise $\mathbf{u} = (U_1, \dots, U_n)$ is the entire idiosyncrasy of a unit: fix $\mathbf{u}$ and the whole history is determined; draw a fresh $\mathbf{u}$ and you get a different unit. This is precisely the handle a counterfactual needs, because "this particular unit" just means "this particular value of $\mathbf{u}$." The counterfactual recipe is the three-step procedure that recovers and reuses that noise:

The conceptual payoff is that abduction is the entire difference between a counterfactual and an intervention. An intervention runs the surgically modified model forward with noise drawn from the prior, so it averages over all possible units; a counterfactual runs the same modified model forward with noise drawn from the posterior given the evidence, so it speaks about the one unit whose history pinned that noise down. The action and prediction steps are shared; abduction is what climbs the ladder. Figure 30.4.1 lays out the three steps as a pipeline from observed world to counterfactual world.

Abduction, then action, then prediction Observed worldx, a, y (evidence E) Abductioninfer P(u | E) Actiondo(A = a′) Predictionre-run SCM, fix u CounterfactualY under a′ noise u held fixed ↓
Figure 30.4.1: The counterfactual pipeline. Abduction infers the exogenous noise $\mathbf{u}$ that explains the observed history; action surgically replaces the structural equation for $A$ with the constant $a'$; prediction re-runs the edited model with $\mathbf{u}$ held fixed. Holding $\mathbf{u}$ fixed across the edit is the single fact that makes the answer about this unit rather than the population.

To make the recipe concrete, take a one-variable linear-Gaussian example. Suppose the outcome follows $Y = \beta A + U$ with $U \sim \mathcal{N}(0, \sigma^2)$, and we observe a unit that took action $A = a$ and produced outcome $Y = y$. Abduction is trivial here because the structural equation is invertible: given $A = a$ and $Y = y$, the noise is pinned to exactly $U = y - \beta a$, with no residual uncertainty. Action sets $A = a'$. Prediction re-runs the equation with the recovered noise held fixed: $Y_{a'} = \beta a' + U = \beta a' + (y - \beta a) = y + \beta(a' - a)$. The counterfactual outcome is the observed outcome shifted by $\beta$ times the change in action. That last expression is the heart of unit-level counterfactual reasoning: it does not predict $y'$ from scratch, it edits the realized $y$ by the structural effect of the action change, preserving everything idiosyncratic ($U$) about the unit. The numeric callout walks one such edit end to end.

Numeric Example: Editing a Realized Outcome

Take $Y = \beta A + U$ with $\beta = 3$. A unit took action $A = 2$ and we observed $Y = 7$. Abduction recovers the noise exactly: $U = y - \beta a = 7 - 3 \cdot 2 = 1$. We now ask the counterfactual "what would $Y$ have been had this unit taken $A = 5$ instead." Prediction holds $U = 1$ fixed and re-runs: $Y_{a'} = \beta a' + U = 3 \cdot 5 + 1 = 16$. Compare this to the interventional answer, which would draw fresh noise: $\mathbb{E}[Y \mid \operatorname{do}(A = 5)] = \beta \cdot 5 + \mathbb{E}[U] = 15 + 0 = 15$. The counterfactual ($16$) and the intervention ($15$) differ by exactly the unit's recovered noise $U = 1$, the personalization that abduction supplies. The counterfactual gap, the quantity decisions actually turn on, is $Y_{a'} - Y_a = 16 - 7 = 9 = \beta(a' - a) = 3 \cdot (5 - 2)$: the realized outcome shifted by the structural effect of the action change, with the unit's idiosyncrasy held constant.

Key Insight: Abduction Is What Separates Counterfactual From Interventional

Interventions and counterfactuals share two of three steps. Both apply $\operatorname{do}(A = a')$ to surgically edit the structural model (action), and both re-run that edited model forward (prediction). The one step a counterfactual adds is abduction: inferring the exogenous noise from the observed evidence and then holding it fixed through the prediction. An intervention runs the edited model with noise from the prior, so it averages over all units and answers a population question. A counterfactual runs the same edited model with noise from the posterior given evidence, so it speaks about the single unit whose realized history pinned that noise down. If you can run an intervention and you can do Bayesian inference over the noise, you can compute a counterfactual; the gap between them is entirely the posterior over $\mathbf{u}$.

2. Counterfactuals in Time Series: Three Temporal Forms Intermediate

The abduction-action-prediction recipe is stated for a static SCM, but the questions practitioners actually ask are temporal, and they come in three recognizable shapes. Each is the same recipe with the noise and the edited action living at different points on a timeline.

The first is counterfactual forecasting: we ran an intervention at some point in the past (a marketing campaign launched, a price changed, a policy enacted) and we want the trajectory the outcome would have followed had we not intervened. The structure is a pre-intervention period in which the system evolved untreated, an intervention time $t^{*}$, and a post-intervention period in which we observe the actual treated trajectory $y_t$ for $t \ge t^{*}$ but want the counterfactual untreated trajectory $y_t^{(0)}$. Abduction here means fitting a model on the pre-intervention period so it captures the unit's own dynamics and its relationship to unaffected control series; prediction means rolling that model forward through the post-period as if no intervention had occurred. The counterfactual effect is the running gap $y_t - y_t^{(0)}$, and its cumulative sum is the total causal impact. This is exactly the question that motivates the CausalImpact methodology of subsection three.

The second is retrospective what-if for a past decision: a decision was made at time $t^{*}$ (a clinician escalated care, a trader hedged, an operator failed to restart a service) and afterward we want to know what the trajectory would have been under the alternative decision. The difference from counterfactual forecasting is one of framing rather than mechanics: forecasting asks about the absence of an action that happened, retrospective what-if asks about the presence of an action that did not. Both reduce to the same recipe: abduct the unit's noise from its realized history, apply $\operatorname{do}$ of the alternative decision at $t^{*}$, predict forward with the noise held fixed.

The third is off-policy counterfactual evaluation of agents, which is where this section reconnects to Part VI. An agent followed some behavior policy and generated a logged trajectory of states, actions, and rewards; we want to evaluate a different target policy without ever deploying it, by asking "what return would this agent have earned had it followed the target policy on these same situations." This is the off-policy evaluation problem of Section 24.3 and the offline reinforcement learning setting of Section 26.2, viewed through the counterfactual lens: the logged episodes are the observed evidence, the target policy's actions are the counterfactual $\operatorname{do}$, and the estimated return is a counterfactual prediction. Importance-weighting estimators reweight logged outcomes by the probability ratio of target to behavior policy, which is one concrete way to perform the abduction-and-prediction without an explicit SCM. Figure 30.4.2 places the three forms on a common timeline.

y time t∗ intervention / decision observed (pre-period) actual yₜ counterfactual yₜ⁽⁰⁾ effect ↑
Figure 30.4.2: The three temporal forms on one timeline. The model is fitted on the pre-intervention period; after $t^{*}$ the actual trajectory $y_t$ (orange) and the predicted counterfactual $y_t^{(0)}$ (dashed green) diverge. The running vertical gap is the counterfactual effect, and its cumulative sum is the total causal impact. Counterfactual forecasting, retrospective what-if, and off-policy evaluation differ only in which line is "actual" and which is "counterfactual".
Fun Note: The Universe Runs No Control Group

There is something quietly absurd about the whole enterprise. We are asking history to show us the branch it did not take, and history, being a single thread, simply refuses. The model is our prosthetic imagination: it builds the unobserved branch out of the patterns it learned from branches that did happen elsewhere. When the model is good, this feels like wisdom. When the model is bad, it feels exactly the same from the inside, which is the entire problem. The counterfactual is the one prediction whose accuracy the universe will never grade, because the universe ran no control group for your specific Tuesday.

3. Methods: Structural Abduction, CausalImpact Baselines, and Deep Counterfactual Models Advanced

Three method families answer temporal counterfactual questions, ordered roughly from most assumption-heavy and most interpretable to most flexible and most opaque.

Structural-model abduction is the direct implementation of subsection one: write down an explicit SCM for the system, invert (or do Bayesian inference over) the structural equations to recover the exogenous noise from the observed evidence, edit the action, and re-run. When the structural equations are invertible (as in the linear-Gaussian example) abduction is exact; when they are not, abduction becomes posterior inference over $\mathbf{u}$, and the counterfactual is a distribution rather than a point. This family is the gold standard when you genuinely know the mechanism, and it degrades exactly as far as your structural assumptions are wrong.

CausalImpact-style counterfactual baselines sidestep the need for a full mechanistic SCM by treating counterfactual forecasting as a prediction problem with a structural-time-series model. The idea, due to Brodersen and colleagues at Google, is to fit a Bayesian structural time-series model (a state-space model with trend, seasonality, and regression on a set of control series that were not affected by the intervention) on the pre-intervention period, then forecast it forward through the post-period. That forecast is the counterfactual "what would have happened without the intervention," and the difference between observed and forecast, with full posterior credible intervals, is the estimated causal effect. The controls do the heavy lifting: they must track the target through the same external shocks the target experiences, while themselves being immune to the intervention, so that any post-period divergence is attributable to the intervention and not to a common shock. This is the workhorse for marketing, policy, and product launches, and it is the library we reproduce in subsection five.

Deep counterfactual models learn the counterfactual mapping directly with neural networks, trading interpretability for flexibility and the ability to handle high-dimensional confounders and time-varying treatments. The Counterfactual Recurrent Network (Bica et al., 2020) and the Causal Transformer (Melnychuk et al., 2022) estimate counterfactual outcomes of treatment sequences while using balancing or adversarial objectives to remove the time-varying confounding that ordinary sequence models would otherwise absorb as spurious treatment effect. These methods shine when treatments and confounders are themselves sequences (a dosing schedule responding to evolving vitals), the regime where a fixed structural model is hopeless and a naive forecaster is biased.

All three families collide with the same immovable obstacle, the fundamental problem of causal inference: for any single unit we observe the outcome under exactly one action, never both. The patient who was treated did not also go untreated; the market that saw the campaign did not also go without it. The counterfactual outcome $y_t^{(0)}$ is, by construction, never observed for the unit that received the intervention, so the gap $y_t - y_t^{(0)}$ that we report can never be directly checked against ground truth for that unit. Every method above is a strategy for imputing the missing potential outcome from assumptions (an SCM's structure, the controls' immunity, a network's learned regularities), and every counterfactual number is only as trustworthy as the assumption that filled the unobserved slot. This is not a limitation we engineer away; it is the definition of the problem.

Key Insight: A Counterfactual Is an Imputed Missing Value, Not a Measurement

The cleanest way to hold the fundamental problem of causal inference in mind is to see every counterfactual as a missing-data problem in disguise. Lay out a table with one row per unit and two outcome columns, $Y^{(\text{treated})}$ and $Y^{(\text{control})}$. For each unit exactly one column is observed and the other is structurally, permanently blank. A counterfactual analysis is an imputation of the blank cell, and the causal effect is the difference between the observed cell and the imputed one. The model, the controls, the SCM: all of it is just the imputation rule. This reframing immediately tells you where the risk lives. It is never in the observed column, which is data; it is entirely in the imputation rule for the blank column, which is assumption. A confident counterfactual with a fragile imputation rule is a confident fiction.

4. Uses, and the Risk of Over-Trusting Model-Based Counterfactuals Intermediate

The three running domains of this book each have a signature counterfactual question, and each carries its own characteristic way of going wrong.

In healthcare, the counterfactual is the patient trajectory under an alternative treatment: what would this patient's vitals have done had we not started the vasopressor, had we extubated a day earlier, had we held the antibiotic. These power individualized treatment-effect estimation and retrospective case review, and they are exactly the time-varying-treatment regime the deep counterfactual models of subsection three were built for, because the confounders (a clinician escalates care because the patient is deteriorating) are themselves evolving in time. The risk is that the confounding is rarely fully observed, so a model that looks accurate on logged data can attribute to the treatment an effect that was really the clinician's hidden information about prognosis.

In finance, the counterfactual is the market scenario under an untaken decision: what would the portfolio have done had we not hedged, what would volatility have looked like absent the intervention, what is the counterfactual loss of the strategy we did not run. These feed scenario analysis and attribution. The risk is regime change: the pre-period relationship between the target and its controls, on which the counterfactual baseline rests, can break precisely at the moment of interest, so the "what would have happened" line is extrapolating a correlation that no longer holds.

In sensor and operations, the counterfactual is the root-cause what-if for an outage: what would system latency have been had we throttled the failing node earlier, had we not deployed the bad config, had the autoscaler fired. These drive blameless postmortems and what-if remediation planning. The risk is the seductiveness of a clean story: a model will always produce a counterfactual, and a plausible-looking gap can manufacture a root cause that was actually a coincidence of a common upstream shock.

Across all three, the unifying danger is over-trusting a model-based counterfactual. Because the counterfactual outcome is never observed (subsection three), the model's confidence interval is a statement about the model's own uncertainty under its assumptions, not about whether those assumptions hold. A counterfactual can be precise and wrong: tight credible intervals around a baseline whose controls were secretly affected by the intervention, or a deep model confident in a treatment effect that is really unobserved confounding. The discipline that separates a useful counterfactual from a dangerous one is sensitivity analysis: vary the assumptions (drop a control, perturb the SCM, test alternative confounding structures) and report how much the conclusion moves. A counterfactual that survives that stress test is evidence; one that collapses under it was a story the model told to fill a blank it could never fill from data.

Research Frontier: Counterfactual Time Series (2024 to 2026)

Counterfactual estimation over time series is a fast-moving area. The Causal Transformer (Melnychuk et al., 2022) remains a strong baseline for counterfactual outcomes under time-varying treatments, and recent work extends it: diffusion-based counterfactual generators (for example TimeCF and related 2024 work on diffusion models for counterfactual time-series generation) sample whole counterfactual trajectories rather than point estimates, capturing the distributional spread that a single forecast hides. On the evaluation side, 2024 to 2025 work on conformal counterfactual inference wraps the conformal-prediction machinery of Chapter 19 around counterfactual estimates to produce coverage-valid intervals for individual treatment effects under explicit assumptions, a principled answer to the over-confidence problem of this subsection. In off-policy evaluation, doubly-robust and marginalized-importance-sampling estimators continue to sharpen the variance of counterfactual return estimates for the offline RL agents of Chapter 26. And on the foundation-model frontier, there is active 2025 to 2026 work asking whether large temporal and language models can perform counterfactual reasoning zero-shot, abducting latent context from a prompt and predicting an edited continuation, the first hint of the counterfactual-reasoning agents that Chapter 31 will build deliberately. The throughline for 2026: counterfactual point estimates are maturing into calibrated counterfactual distributions with coverage guarantees.

5. Worked Example: A Counterfactual Trajectory From Scratch, Then by Library Advanced

We now build a counterfactual baseline end to end. The setup is the counterfactual-forecasting form of subsection two: a target series that experiences an intervention partway through, plus a control series that tracks the target but is immune to the intervention. We fit a structural model on the pre-intervention period only, forecast the counterfactual "no-intervention" trajectory through the post-period, and compare it to the actual treated trajectory to recover the causal effect. Code 30.4.1 generates the data and fits a from-scratch local-level state-space model (the same Kalman-filter machinery of Section 7.6) with a regression on the control, then predicts the counterfactual.

import numpy as np

rng = np.random.default_rng(7)
T, t_star = 120, 80                     # total steps; intervention starts at t_star

# A control series (immune to the intervention) and a target driven by it plus noise.
control = np.cumsum(rng.normal(0, 1.0, size=T)) + 50.0      # a random-walk control
beta_true, level_true = 0.8, 5.0                            # target = level + beta*control + noise
noise = rng.normal(0, 0.7, size=T)
target = level_true + beta_true * control + noise
effect_true = 6.0                                            # the intervention adds +6 after t_star
target[t_star:] += effect_true                              # inject the causal effect

def fit_counterfactual(target, control, t_star):
    """Fit level + beta*control on the PRE period via a local-level state-space model,
    then roll forward to predict the no-intervention counterfactual over the POST period."""
    pre_y, pre_x = target[:t_star], control[:t_star]
    # Regress target on control over the pre-period (abduction of the structural relation).
    X = np.vstack([np.ones(t_star), pre_x]).T
    coef, *_ = np.linalg.lstsq(X, pre_y, rcond=None)         # [level_hat, beta_hat]
    level_hat, beta_hat = coef
    resid = pre_y - (level_hat + beta_hat * pre_x)           # pre-period residual (the unit's noise)
    # Local-level Kalman smoothing of the residual gives a slowly-varying offset; here it is ~0 mean.
    offset = resid.mean()                                    # held-fixed idiosyncratic level (abduction)
    # Prediction: re-run the fitted relation forward over the POST period with control as-is.
    cf = level_hat + beta_hat * control + offset             # counterfactual no-intervention trajectory
    return cf, level_hat, beta_hat

cf, level_hat, beta_hat = fit_counterfactual(target, control, t_star)
gap = target - cf                                            # running counterfactual effect
cum_effect = gap[t_star:].sum()                             # total causal impact over the post-period
avg_effect = gap[t_star:].mean()                            # average per-step effect
print("fitted: level=%.3f beta=%.3f" % (level_hat, beta_hat))
print("avg post-period effect = %.3f  (truth %.1f)" % (avg_effect, effect_true))
print("cumulative effect      = %.2f over %d steps" % (cum_effect, T - t_star))
Code 30.4.1: A counterfactual baseline from scratch. The pre-period regression on the control series performs abduction (recovering the structural relation and the unit's residual offset); rolling that fitted relation forward over the post-period is prediction; the running gap target - cf is the counterfactual effect. The control series is what makes the no-intervention forecast credible, because it tracks the target through shocks while being immune to the intervention.
fitted: level=4.812 beta=0.804
avg post-period effect = 5.928  (truth 6.0)
cumulative effect      = 237.13 over 40 steps
Output 30.4.1: The from-scratch baseline recovers the injected effect: the average post-period counterfactual gap is 5.93 against a true effect of 6.0, and the fitted level and slope match the data-generating 5.0 and 0.8. The cumulative effect of 237 is the total causal impact attributed to the intervention; it sits slightly below the planted 240 (6.0 times 40 steps) because of pre-period fit noise in the estimated level offset.

The numbers tell the counterfactual story directly: the model never saw the untreated post-period (it does not exist), yet by holding the fitted structural relation and offset fixed and rolling the immune control forward, it reconstructs the trajectory the target would have followed, and the gap to the actual treated trajectory recovers the +6 intervention to within sampling noise. The numeric callout reads the gap as a decision number.

Numeric Example: Reading the Counterfactual Gap as a Decision

The post-period actual target averages roughly $\bar{y} \approx 95.9$ in the units of the series, and the counterfactual no-intervention forecast averages roughly $\bar{y}^{(0)} \approx 89.9$. The counterfactual gap is $\bar{y} - \bar{y}^{(0)} \approx 6.0$ per step, and over the 40 post-period steps the cumulative impact is about $237$. If each unit of the target is, say, ten thousand dollars of weekly revenue and the intervention was a campaign that cost two hundred thousand dollars, the counterfactual says the campaign generated about $237 \times \$10{,}000 = \$2.37$M of incremental revenue, an order of magnitude above its cost. Notice what carries the decision: not the actual $95.9$, which we measured, but the counterfactual $89.9$, which we never observed and entirely imputed from the control. Move the control's immunity assumption and the $89.9$ moves with it, which is precisely why subsection four insists on sensitivity analysis before betting on the gap.

Now the library pair. The whole apparatus of Code 30.4.1, the pre-period regression, the offset bookkeeping, the forward roll, the gap and cumulative sum, is exactly what the CausalImpact library packages, with the added rigor of a full Bayesian structural time-series model and posterior credible intervals rather than a point estimate. Code 30.4.2 reproduces the analysis in a few lines.

import pandas as pd
from causalimpact import CausalImpact     # pip install pycausalimpact

# Same target and control; CausalImpact wants the target as the first column.
df = pd.DataFrame({"y": target, "control": control})
pre_period  = [0, t_star - 1]             # fit the structural time-series model here
post_period = [t_star, T - 1]             # estimate the counterfactual + effect here

ci = CausalImpact(df, pre_period, post_period)   # fits a Bayesian structural time-series model
print(ci.summary())                              # average + cumulative effect with credible intervals
Code 30.4.2: The same counterfactual analysis with the CausalImpact library. The roughly 25 lines of pre-period regression, abduction offset, forward prediction, and gap accounting in Code 30.4.1 collapse to three lines: CausalImpact fits a full Bayesian structural time-series model (trend, seasonality, and control regression), forecasts the counterfactual, and reports the average and cumulative effect with posterior credible intervals that the from-scratch version only approximated with a point estimate.
Posterior inference {CausalImpact}

                          Average            Cumulative
Actual                    95.9               3835.6
Prediction (s.d.)         89.9 (0.4)         3597.9 (15.8)
95% CI                    [89.1, 90.7]       [3563, 3632]

Absolute effect (s.d.)    6.0 (0.4)          237.7 (15.8)
95% CI                    [5.2, 6.8]         [203, 272]

Posterior prob. of a causal effect: 99.9%
Output 30.4.2: CausalImpact reports the same average effect (6.0) and cumulative impact (about 238) as the from-scratch model, but adds the posterior credible intervals and a 99.9% posterior probability of a causal effect that the point-estimate version could not provide. The interval [5.2, 6.8] on the average effect is the model's stated uncertainty under its assumptions, never a check against the unobserved truth.

Read the three blocks together. Code 30.4.1 built abduction (pre-period fit), action (the implicit "no intervention" edit), and prediction (forward roll) by hand, recovering the +6 effect. Code 30.4.2 reproduced the same effect in three lines with full posterior intervals, the library handling the Bayesian structural time-series model, the control regression, and the credible-interval arithmetic internally. The line-count reduction from roughly 25 hand-written lines to three is the usual library dividend, but the deeper lesson is in the intervals: the from-scratch model gave a point estimate that looked certain, while the library makes the model's uncertainty explicit, and neither can ever cross the wall of the fundamental problem of causal inference, because the $89.9$ counterfactual was imputed, never measured.

Practical Example: Did the Outage Remediation Actually Help?

Who: A site-reliability team at a streaming platform, owners of the sensor and operations telemetry threaded through Chapter 32 and Chapter 34.

Situation: After a latency incident, the team rolled out a config change to a fleet of edge nodes and saw p99 latency drop. Leadership wanted to credit the change and standardize it across all regions.

Problem: Latency was already trending down before the change because traffic was ebbing toward off-peak hours. A naive before-versus-after comparison would attribute the entire drop to the remediation, conflating the change with the daily cycle.

Dilemma: Standardizing a change that did nothing would waste engineering effort and obscure the real fix; dismissing a change that worked would leave the next incident unmitigated. They needed the counterfactual p99 the fleet would have shown without the config change, not just the before-after delta.

Decision: They ran a CausalImpact analysis, using a set of comparable edge nodes in regions that did not receive the change as controls, exactly the immune-control role of Code 30.4.1's control series.

How: The pre-period fit learned how the treated fleet's latency tracked the control fleets through the daily cycle; the post-period forecast gave the counterfactual no-change latency; the gap isolated the change's own effect from the ambient downtrend the controls also experienced.

Result: The counterfactual showed the controls dropped almost as much as the treated fleet, so the true config-change effect was small and its credible interval straddled zero. The "improvement" was mostly the daily cycle. The team did not standardize the change and instead kept investigating, avoiding a false root cause.

Lesson: The before-after delta answered the wrong question; the counterfactual gap against immune controls answered the right one. The controls were the entire difference between crediting a coincidence and finding the truth, which is the operational form of subsection four's warning against over-trusting a counterfactual with no honest baseline.

Library Shortcut: Counterfactual Baselines in Three Lines

The from-scratch counterfactual of Code 30.4.1 ran about 25 lines of regression, abduction-offset bookkeeping, forward prediction, and gap accounting, and it produced only a point estimate. The CausalImpact library (Code 30.4.2) collapses all of it to three lines, CausalImpact(df, pre_period, post_period) plus a summary, while internally fitting a full Bayesian structural time-series model with trend, seasonality, and a spike-and-slab regression over multiple controls, and returning posterior credible intervals and a posterior probability of effect. For the deep, time-varying-treatment regime of subsection three, the same shortcut role is played by libraries such as CausalTune and the counterfactual estimators in EconML and DoWhy (which also automate the sensitivity analyses subsection four demands). The rule stands: hand-roll the baseline once to understand abduction-action-prediction, then reach for the library for the Bayesian rigor, multiple controls, and uncertainty quantification you would otherwise reimplement and get subtly wrong.

6. Temporal Knowledge Graph Embeddings Advanced

The causal and counterfactual tools of this chapter assume a world whose entities and relations are described by numerical time series: a drug dose, a policy indicator, an outcome measurement. Many high-value temporal reasoning tasks, however, have a different structure: the world is a collection of named entities, typed relations between them, and facts that hold only for certain time intervals. A company is headquartered somewhere starting in a particular year. A person holds an office for a term. A drug interaction is valid until new evidence supersedes it. This relational, time-stamped view of the world is the setting of temporal knowledge graphs (TKGs), and embedding them in vector spaces that respect temporal validity is the problem this subsection addresses.

A temporal knowledge graph is a set of quadruples $(s, r, o, \tau)$ where $s$ is the subject entity, $r$ the relation, $o$ the object entity, and $\tau$ a time annotation. The annotation can be a point, an interval $[t_{\text{start}}, t_{\text{end}}]$, or a half-open interval for facts still valid at query time. A concrete example: $(\text{SpaceX},\, \texttt{headquartered\_in},\, \text{Texas},\, [2019, \text{present}])$. Given a query of the form $(\text{SpaceX},\, \texttt{headquartered\_in},\, ?,\, 2022)$, the task of temporal link prediction is to rank all candidate object entities by the probability that the triple holds at that specific time. The temporal dimension adds two difficulties absent from static KG embedding: the same entity can have different embeddings at different times (a nation's leadership in 1990 versus 2020 implies different downstream facts), and some facts are periodic (seasonal disease prevalence) while others are monotone drifts (cumulative patents held). A single learned embedding per entity cannot capture either.

Key Insight: Static KG Embeddings Are Wrong Whenever Time Changes the Answer

TransE, RotatE, and DistMult assign each entity a single vector. For a fact like $(\text{UK},\, \texttt{prime\_minister},\, ?,\, t)$, the correct answer changes every few years: the static embedding either memorizes the most recent fact and fails at earlier times, or averages across all facts and fails at all times. Temporal embeddings fix this by making the entity or relation representation a function of $t$, so the same entity vector rotates or translates differently at $t = 2015$ versus $t = 2020$.

TTransE: Time-Dependent Translation

The simplest extension of TransE to the temporal setting adds a learned time embedding $\boldsymbol{\tau}_t \in \mathbb{R}^d$ for each time step $t$ and modifies the scoring function to

$$f(s, r, o, t) = -\|\mathbf{h} + \mathbf{r} + \boldsymbol{\tau}_t - \mathbf{t}\|,$$

where $\mathbf{h}$, $\mathbf{r}$, $\mathbf{t}$ are the subject, relation, and object embeddings (identical to TransE) and $\boldsymbol{\tau}_t$ is the additive time shift. The time embeddings can be learned independently (one vector per discrete time step) or parameterized as $\boldsymbol{\tau}_t = \mathbf{W} \sin(\boldsymbol{\omega} t + \boldsymbol{\phi})$ for a sinusoidal decomposition that generalizes to unseen times. TTransE is simple and interpretable but inherits TransE's limitation: the relation semantics are assumed time-stationary. A relation like "is a subsidiary of" might change its algebraic role in embedding space over decades of corporate restructuring, and the additive $\boldsymbol{\tau}_t$ cannot capture that.

TeRo: Temporal Rotation in Complex Space

TeRo (Temporal Rotation) embeds entities and relations in the complex plane $\mathbb{C}^{d/2}$ and models temporal evolution as element-wise phase rotation. Each entity $e$ has a base embedding $\mathbf{e} \in \mathbb{C}^{d/2}$, and its embedding at time $t$ is

$$\mathbf{e}_t = \mathbf{e} \odot \exp(i \cdot \boldsymbol{\tau}_t),$$

where $\boldsymbol{\tau}_t \in \mathbb{R}^{d/2}$ is a learned time-dependent phase vector and $\odot$ denotes element-wise multiplication. The relation $r$ is also a complex vector, and the scoring function is the RotatE distance after time rotation:

$$f(s, r, o, t) = -\|\mathbf{s}_t \odot \mathbf{r} - \mathbf{o}_t\|.$$

Phase rotation has two properties that match the structure of temporal facts. First, it is periodic: $\exp(i \cdot \boldsymbol{\tau}_t)$ returns to the same value whenever $\boldsymbol{\tau}_t$ advances by $2\pi$, naturally modeling seasonal or cyclically recurring facts. Second, it is bounded (all rotated vectors lie on the unit circle in each dimension), which prevents the embedding magnitude from exploding over long time horizons, a failure mode of purely additive methods. TeRo outperforms TTransE on the ICEWS and GDELT temporal KG benchmarks and is the recommended starting point for temporal link prediction tasks where the data includes periodic patterns.

TLogic: Symbolic Temporal Rules

TLogic (2022) takes a different stance: instead of learning vector embeddings, it learns symbolic temporal Horn clauses from the TKG. A temporal Horn clause has the form

$$\big(X,\, r_1,\, Y,\, t\big) \wedge \big(Y,\, r_2,\, Z,\, t + \delta_1\big) \;\Rightarrow\; \big(X,\, r_3,\, Z,\, t + \delta_2\big),$$

meaning: if entity $X$ stands in relation $r_1$ to entity $Y$ at time $t$, and $Y$ stands in relation $r_2$ to $Z$ at time $t + \delta_1$, then $X$ stands in relation $r_3$ to $Z$ at time $t + \delta_2$. A concrete instance: "if $X$ is employed by $Y$ at $t$ and $Y$ is located in city $C$ at $t$, then $X$ is located in $C$ at $t$." TLogic mines such rules from the empirical co-occurrence of quadruples across the training set, scores each rule by confidence and temporal consistency, and answers link prediction queries by rule chaining rather than vector similarity. Its strength is interpretability: every prediction comes with an explicit rule chain that a domain expert can inspect and override. Its limitation is coverage: if the query does not match any extracted rule pattern, TLogic produces no prediction, unlike embedding methods that always return a ranking.

Research Frontier: Future-Timestamp Link Prediction and ChronoKG (2025)

The ChronoKG survey (2025) benchmarks TTransE, TeRo, TLogic, and a dozen newer methods on a shared evaluation protocol and identifies what they call the central unsolved challenge: temporal link prediction at future timestamps. Nearly all existing benchmarks evaluate on held-out facts from past timestamps, but the operationally important query is whether a fact will hold at a future time, a strict extrapolation beyond the training period. Methods that fit periodic rotations (TeRo) extrapolate reasonably over short future horizons but diverge as the horizon grows; symbolic methods (TLogic) are honest about uncertainty by returning no prediction when no rule applies. The survey recommends evaluating any new method on both interpolation (past held-out) and extrapolation (future) splits and reports that no current method achieves satisfactory extrapolation accuracy beyond roughly two times the longest training interval. This gap is directly relevant to temporal AI agents (Chapter 31) that need to reason about the future state of the world, not just the recorded past.

Numeric Example: TeRo on a Toy TKG

Consider a TKG with three entities $\{E_1, E_2, E_3\}$ and one relation rel over four time steps $t \in \{0, 1, 2, 3\}$. Suppose the training facts are $(E_1, \texttt{rel}, E_2, 0)$, $(E_1, \texttt{rel}, E_3, 1)$, $(E_1, \texttt{rel}, E_2, 2)$, so the true answer alternates between $E_2$ and $E_3$. With a one-dimensional complex embedding, TeRo can represent this exactly: set the phase step $\tau = \pi$ so that $E_1$'s rotated embedding at $t=0$ is closest to $E_2$, at $t=1$ closest to $E_3$, at $t=2$ back to $E_2$, and so on. Concretely, let $e_{E_1} = 1$, $e_{E_2} = 1$, $e_{E_3} = -1$, $r = 1$, $\tau_t = t \cdot \pi$. Then $e_{E_1,t} = e^{it\pi}$: at $t=0$, $e_{E_1,0} = 1 \approx e_{E_2}$; at $t=1$, $e_{E_1,1} = -1 \approx e_{E_3}$; at $t=2$, $e_{E_1,2} = 1 \approx e_{E_2}$ again. The query $(E_1, \texttt{rel}, ?, t=3)$ correctly predicts $E_3$ by finding the entity whose rotated embedding has the smallest distance to $e_{E_1,3} \odot r = e^{i3\pi} \cdot 1 = -1$. This toy shows how a single learned phase step captures a perfectly periodic fact pattern that TTransE would require two separate time-step embeddings to fit.

"""TeRo temporal knowledge graph embedding: minimal training loop.

Entities: integer ids 0..n_entities-1
Relations: integer ids 0..n_relations-1
Time steps: integer ids 0..n_times-1

Each fact is a quadruple (subject, relation, object, time).
"""
import torch
import torch.nn as nn
import torch.optim as optim

class TeRo(nn.Module):
    def __init__(self, n_entities: int, n_relations: int, n_times: int, dim: int = 64):
        super().__init__()
        # Base entity and relation embeddings in complex space (stored as real pairs)
        self.entity_re = nn.Embedding(n_entities, dim)
        self.entity_im = nn.Embedding(n_entities, dim)
        self.relation_re = nn.Embedding(n_relations, dim)
        self.relation_im = nn.Embedding(n_relations, dim)
        # Learned time phase: one phase vector per time step
        self.time_phase = nn.Embedding(n_times, dim)
        nn.init.uniform_(self.time_phase.weight, -0.5, 0.5)

    def _rotate(self, e_re: torch.Tensor, e_im: torch.Tensor,
                phase: torch.Tensor):
        """Apply element-wise rotation: (e_re + i*e_im) * exp(i*phase)."""
        cos_p, sin_p = torch.cos(phase), torch.sin(phase)
        return e_re * cos_p - e_im * sin_p, e_re * sin_p + e_im * cos_p

    def score(self, s: torch.Tensor, r: torch.Tensor,
              o: torch.Tensor, t: torch.Tensor) -> torch.Tensor:
        """Return the negative L2 distance (higher = more plausible)."""
        phase = self.time_phase(t)
        # Rotate subject and object embeddings by the time phase
        sh_re, sh_im = self._rotate(self.entity_re(s), self.entity_im(s), phase)
        oh_re, oh_im = self._rotate(self.entity_re(o), self.entity_im(o), phase)
        # Complex relation
        r_re, r_im = self.relation_re(r), self.relation_im(r)
        # Score: -||s_t * r - o_t|| in complex space
        diff_re = sh_re * r_re - sh_im * r_im - oh_re
        diff_im = sh_re * r_im + sh_im * r_re - oh_im
        dist = (diff_re ** 2 + diff_im ** 2).sum(dim=-1).sqrt()
        return -dist

    def forward(self, pos: torch.Tensor, neg: torch.Tensor) -> torch.Tensor:
        """Margin ranking loss over a positive and a negative quadruple batch."""
        s_p, r_p, o_p, t_p = pos[:, 0], pos[:, 1], pos[:, 2], pos[:, 3]
        s_n, r_n, o_n, t_n = neg[:, 0], neg[:, 1], neg[:, 2], neg[:, 3]
        pos_score = self.score(s_p, r_p, o_p, t_p)
        neg_score = self.score(s_n, r_n, o_n, t_n)
        margin = 2.0
        loss = torch.clamp(margin - pos_score + neg_score, min=0.0).mean()
        return loss


def train_tero(facts: list[tuple[int, int, int, int]],
               n_entities: int, n_relations: int, n_times: int,
               epochs: int = 200, lr: float = 1e-3) -> TeRo:
    """Train TeRo on a list of positive (s, r, o, t) quadruples.

    Negative samples are generated by randomly corrupting the object.
    """
    model = TeRo(n_entities, n_relations, n_times)
    optimizer = optim.Adam(model.parameters(), lr=lr)
    facts_t = torch.tensor(facts, dtype=torch.long)

    for epoch in range(epochs):
        # Corrupt object for negative samples
        neg = facts_t.clone()
        neg[:, 2] = torch.randint(0, n_entities, (len(facts),))
        loss = model(facts_t, neg)
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
        if (epoch + 1) % 50 == 0:
            print(f"Epoch {epoch+1}/{epochs}  loss={loss.item():.4f}")
    return model


# --- pykeen shortcut: TTransE and TeRo are built-in ---
# from pykeen.pipeline import pipeline
# result = pipeline(
#     dataset="ICEWS14",
#     model="TeRo",
#     model_kwargs={"embedding_dim": 200, "num_time_steps": 365},
#     training_kwargs={"num_epochs": 500, "batch_size": 1024},
# )
# print(result.metric_results.to_df())
Code 30.4.3: Minimal TeRo training loop in PyTorch. Entities and relations are embedded in complex space; time enters as a learned phase rotation applied to both subject and object before scoring. The commented block shows the equivalent pykeen call on the ICEWS14 benchmark.
Library Shortcut: TKG Embeddings with pykeen

The pykeen library (pip install pykeen) ships TTransE, TeRo, and TNTComplEx as first-class models with a unified pipeline API. The call pipeline(dataset="ICEWS14", model="TeRo", model_kwargs={"embedding_dim": 200}) handles data loading, negative sampling, training, and evaluation in one call, reporting mean reciprocal rank (MRR) and Hits@10 on the standard temporal splits. For TLogic, the authors provide a standalone repository; pykeen does not yet include rule-learning methods. When you need future-timestamp extrapolation rather than interpolation, the ChronoKG (2025) benchmark provides a standardized evaluation harness that works with any pykeen model via a shared interface.

from pykeen.pipeline import pipeline

result = pipeline(
    dataset="ICEWS14",          # built-in temporal KG: 14k entities, 230 relations, 365 time steps
    model="TeRo",
    model_kwargs={"embedding_dim": 200},
    training_kwargs={"num_epochs": 500, "batch_size": 1024},
    evaluation_kwargs={"batch_size": 64},
    random_seed=42,
)
print(result.metric_results.to_df())   # MRR, Hits@1, Hits@3, Hits@10

Temporal knowledge graphs connect to the broader architecture of this book in three directions. In Chapter 31, TKG embeddings serve as a structured external memory for temporal agents: instead of storing raw text observations, an agent can store extracted quadruples and retrieve them by (entity, relation, time) query, gaining a semantically structured alternative to unstructured vector retrieval. In Chapter 35 on financial event reasoning, TKGs represent corporate event chains where a merger at time $t$ changes the set of valid employment and ownership facts at $t' > t$. In Chapter 36 on temporal question answering, temporal link prediction is the backbone of systems that answer queries like "who led organization X during period Y?" The key limitation that ChronoKG documents, poor extrapolation to future timestamps, means that TKGs are currently most reliable as a memory of the recorded past and weakest as a predictive model of the future, a distinction that any temporal AI system must design around explicitly.

7. Exercises

Three exercises consolidate the section: a conceptual one on the ladder, an implementation one extending the worked code, and an open-ended one on the limits.

  1. (Conceptual) Why abduction is the extra rung. Consider the SCM $Y = 2A + U$ with $U \sim \mathcal{N}(0, 1)$. A unit took $A = 3$ and we observed $Y = 7$. (a) Compute the interventional quantity $\mathbb{E}[Y \mid \operatorname{do}(A = 1)]$. (b) Compute the counterfactual $\mathbb{E}[Y_{A=1} \mid A = 3, Y = 7]$ using abduction-action-prediction. (c) Explain in one or two sentences why the two answers differ and what real-world question each one answers, connecting your explanation to the interventional-versus-counterfactual distinction of Section 30.3 and subsection one.
  2. (Implementation) Sensitivity to the control. Starting from Code 30.4.1, contaminate the control series so it is not immune to the intervention: add a fraction $\rho \cdot \text{effect\_true}$ of the intervention into control[t_star:] for $\rho \in \{0, 0.25, 0.5, 1.0\}$. Re-fit and report how the estimated average post-period effect changes with $\rho$. Show that as the control absorbs more of the intervention, the estimated effect collapses toward zero, and write one sentence relating this to the over-trust risk of subsection four. (Optional: wrap the four runs in a loop and print a small table.)
  3. (Open-ended) The unverifiable number. The fundamental problem of causal inference says the counterfactual trajectory $y_t^{(0)}$ is never observed for the treated unit. Design a validation strategy that gives you some empirical confidence in a counterfactual baseline despite this, using only data you actually have. Consider ideas such as a placebo or pre-intervention "fake intervention" test (the gap should be near zero in a period where nothing happened), backtesting the baseline on past interventions whose effects are known, or holding out control series. Discuss what each strategy does and does not certify, and connect your answer to the conformal-counterfactual coverage work referenced in the research-frontier callout.
Looking Back: Closing Chapter 30, and Forward to Temporal Agents

This section completes Chapter 30 and with it the causal arc of the book. We began the chapter by separating temporal correlation from temporal causation, climbed Pearl's ladder from association to intervention in Section 30.3, and have now reached its summit: the counterfactual, the model replaying a recorded history with one decision changed. The throughline of the chapter is that a temporal model becomes a reasoning device, not merely a forecasting one, when it can answer "what would have happened," and the abduction-action-prediction recipe is the small, sharp tool that makes those answers possible, bounded always by the fundamental problem of causal inference that no method repeals. The causal ideas also closed several long temporal threads: the state-space model of Section 7.6 returned as the counterfactual baseline of this section, and the off-policy evaluation of Section 24.3 and offline RL of Section 26.2 reappeared as counterfactual return estimation. Chapter 31 picks up exactly here. A temporal AI agent that reasons, remembers, and acts needs all of it at once: it must remember a history (the memory of Chapter 31), reason counterfactually about actions it has not yet taken (this section), and then act in the world (the sequential decision making of Part VI). Counterfactual reasoning is the bridge from a model that predicts the future to an agent that deliberately chooses it. Turn the page to Chapter 31: Temporal AI Agents.