"I am bolted to a gantry above lane three, and all day I count cars. On my own I know one thing: how fast traffic is moving directly beneath me, right now. But I have learned to listen to my neighbors. When the sensor two miles upstream goes quiet and the one before it slows, I already know what is coming. I am predicting the jam three exits ahead, and I have not yet seen a single brake light."
A Sensor on a Highway Predicting the Jam Three Exits Ahead
Traffic forecasting is the problem that turned spatio-temporal learning into a field. Picture a few hundred loop detectors embedded in a freeway network, each reporting average speed every five minutes. The readings are a time series at every sensor, but the sensors are not independent: congestion at one location propagates along the road graph to its neighbors with a delay set by travel time. The forecasting task, predict the next hour of speeds at every sensor given the last hour, is therefore neither pure time series (the sensors talk to each other) nor pure graph learning (each node is a signal that evolves). It is both at once, and the winning recipe is to factor the problem into two interacting halves: a SPATIAL model that mixes information across the road graph, and a TEMPORAL model that carries each node's history forward. Part II and Part III gave us the temporal half: ARIMA, the Kalman filter, the RNN, the TCN, attention. This chapter adds the spatial half, graph convolution over the sensor network, and shows how to braid the two into a single forecaster. We will derive the spatio-temporal graph convolution, survey the landmark models (DCRNN, STGCN, Graph WaveNet) and the learned-versus-given-graph debate, ground everything in the METR-LA and PEMS benchmarks with their sobering lesson that strong simple baselines are hard to beat, and finally build a graph-convolution-plus-GRU forecaster from scratch and then in three lines with PyTorch Geometric Temporal. You leave able to recognize, build, and critically evaluate a traffic forecaster, the canonical spatio-temporal system.
In Section 32.1 we framed spatio-temporal data in general: measurements indexed by both a location and a time, where neither axis can be ignored without losing the signal. This section makes that abstraction concrete on the benchmark that has driven the field for a decade. Traffic is the fruit-fly of spatio-temporal learning: the data are public and clean, the graph is real and interpretable (it is a road network), and the forecasting horizon (fifteen minutes to an hour ahead, network-wide) is both economically valuable and genuinely hard. Every architectural idea in this chapter was either invented on traffic data or validated there first.
The temporal half of the problem is entirely familiar. Forecasting a single sensor's speed from its own past is the univariate forecasting of Chapter 5, and modeling the joint evolution of all sensors is the multivariate forecasting of Chapter 6. The deep-learning machinery for carrying a history forward, the RNN of Chapter 10, the temporal convolutional network of Chapter 11, and attention from Chapter 12, all carry over without modification. The genuinely new ingredient, the thing this section is about, is the spatial coupling: the road graph that says sensor $i$ and sensor $j$ are connected and that congestion flows between them. We use the unified notation of Appendix A: $\mathbf{X}_t \in \mathbb{R}^{N \times F}$ is the signal at all $N$ sensors at time $t$ with $F$ features, $\mathbf{A}$ is the adjacency matrix of the road graph, and the forecast targets are the next $H$ steps $\mathbf{X}_{t+1}, \dots, \mathbf{X}_{t+H}$.
Four competencies this section installs: to state traffic forecasting precisely as multi-step network-wide regression on a graph signal; to write a spatio-temporal block as the composition of a graph convolution and a temporal operator, and read the spatio-temporal graph convolution formula; to place the landmark models DCRNN, STGCN, and Graph WaveNet on a map of spatial-operator and temporal-operator choices, and explain when a learned graph beats a given one; and to implement a working forecaster both from scratch and with a library, evaluating it honestly against the simple baselines that the benchmark literature warns are surprisingly strong.
1. Traffic as the Canonical Spatio-Temporal Benchmark Beginner
Strip a traffic forecasting problem to its data and you have three objects. First, a set of $N$ sensors (loop detectors, radar units, or probe-vehicle aggregators) at fixed locations on a road network. Second, a reading from each sensor at each time step, typically average speed or flow aggregated into five-minute bins, which stacks into a tensor $\mathbf{X} \in \mathbb{R}^{T \times N \times F}$ over $T$ time steps with $F$ features per sensor. Third, a graph $\mathcal{G} = (\mathcal{V}, \mathcal{E}, \mathbf{A})$ whose nodes are the sensors and whose weighted adjacency $\mathbf{A} \in \mathbb{R}^{N \times N}$ encodes road connectivity, usually built from driving distance along the network rather than straight-line distance, because congestion travels on roads, not as the crow flies.
The task is multi-step, network-wide forecasting: given a window of the recent past $\mathbf{X}_{t-P+1:t}$ (say the last twelve five-minute steps, one hour), predict the next $H$ steps $\mathbf{X}_{t+1:t+H}$ (the next hour) at every sensor simultaneously. This is a sequence-to-sequence regression mapping a length-$P$ history of graph signals to a length-$H$ future of graph signals. It is the forecasting problem of Part II and the deep forecasting of Part III, lifted from a single series (or a flat vector of series) to a signal living on a graph.
Why is the graph indispensable rather than a decoration? Because traffic obeys a transport process with a finite propagation speed. A crash on a freeway does not raise travel times everywhere at once; a shockwave of congestion forms at the incident and moves upstream against the flow of traffic at a characteristic speed (roughly ten to twenty kilometers per hour for a stop-and-go wave), while the road downstream of the incident clears. A model that ignores the graph treats the sensors as $N$ unrelated series and must rediscover this coupling from data; a model that is handed the graph can route the upstream sensor's signal directly to the downstream forecaster with the right structural prior. The graph is the inductive bias that says "your neighbors' recent past is your near future."
Two features of the traffic signal make it a demanding and instructive benchmark. It is strongly periodic (a daily commuting rhythm, a weekly weekday-weekend pattern) so any model must beat the trivial but strong baseline of "predict what happened at this time yesterday." And it is non-stationary and bursty: incidents, weather, and events produce sudden regime changes that the periodic component cannot anticipate, which is exactly where a spatially aware model earns its keep, because the incident is visible at a neighbor before it is visible locally. Holding both facts in mind, that a huge fraction of the signal is trivially periodic and the interesting fraction is spatially propagating, is the key to reading the benchmark results in subsection four honestly.
The cleanest way to place traffic forecasting in the arc of this book is this: it is the multivariate forecasting of Chapter 6 where the cross-series coupling is not learned from a dense covariance matrix but given as a sparse road graph. A vector autoregression on $N$ sensors has $O(N^2)$ coupling parameters per lag and cannot scale to hundreds of sensors. The graph replaces that dense, unstructured coupling with a sparse, interpretable, distance-derived adjacency that encodes which sensors can physically influence which. Spatio-temporal models are, in this light, structured multivariate forecasters: the structure is the road network, and supplying it is what lets the model generalize from a manageable amount of data.
2. The Architecture Pattern: Spatial Plus Temporal Intermediate
Almost every traffic forecaster, and almost every spatio-temporal model, follows one pattern: alternate or fuse a spatial operator that mixes information across the graph at a fixed time with a temporal operator that mixes information across time at a fixed node. The two operators are the two axes of the data, and a deep model is a stack of blocks each of which advances both axes. Understanding this factorization is the single most useful thing in the chapter, because once you see it, every named architecture becomes a choice of "which spatial operator" crossed with "which temporal operator."
The spatial operator is a graph convolution. The simplest useful form is the one-hop graph convolution: replace each node's feature by a learned combination of its own feature and an aggregate of its neighbors' features. With the (symmetrically normalized) adjacency $\tilde{\mathbf{A}}$ and a node-feature matrix $\mathbf{X} \in \mathbb{R}^{N \times F}$, one graph-convolution layer is
$$\mathbf{H} = \sigma\!\big(\tilde{\mathbf{A}}\,\mathbf{X}\,\mathbf{\Theta}\big), \qquad \tilde{\mathbf{A}} = \mathbf{D}^{-1/2}(\mathbf{A} + \mathbf{I})\,\mathbf{D}^{-1/2},$$where $\mathbf{\Theta} \in \mathbb{R}^{F \times F'}$ is a learned weight, $\mathbf{D}$ is the degree matrix of $\mathbf{A} + \mathbf{I}$, and $\sigma$ is a nonlinearity. The product $\tilde{\mathbf{A}}\mathbf{X}$ is the spatial mixing: row $i$ of the result is the degree-normalized average of node $i$ and its neighbors. Stacking $K$ such layers, or equivalently raising $\tilde{\mathbf{A}}$ to higher powers, lets information travel $K$ hops along the graph, which is how a sensor several exits away comes to influence a forecast.
The temporal operator is anything from Part III. To make a single block that advances both axes, compose them. A spatio-temporal graph convolution at node $i$ and time $t$ aggregates over both the graph neighborhood $\mathcal{N}(i)$ and a temporal window of width $\Gamma$:
$$\mathbf{h}_{i,t} = \sigma\!\left( \sum_{\tau=0}^{\Gamma-1} \sum_{j \in \mathcal{N}(i) \cup \{i\}} \tilde{A}_{ij}\, \mathbf{x}_{j,\,t-\tau}\, \mathbf{\Theta}_{\tau} \right),$$where $\mathbf{\Theta}_\tau \in \mathbb{R}^{F \times F'}$ is a per-lag weight (the temporal kernel) and $\tilde{A}_{ij}$ is the spatial weight. Read the formula as two nested sums: the inner sum over $j$ is the graph convolution (spatial mixing across neighbors at one lag), and the outer sum over $\tau$ is a temporal convolution (mixing across the last $\Gamma$ time steps), so a single block does both a one-hop spatial step and a $\Gamma$-tap temporal step. Different architectures realize this composition differently: STGCN stacks separate temporal-convolution and graph-convolution sublayers; DCRNN replaces the matrix multiplies inside a GRU with graph convolutions; Graph WaveNet pairs a dilated temporal convolution with a graph convolution on a learned adjacency. The skeleton is always spatial $\times$ temporal.
| Choice | Spatial operator (across the graph) | Temporal operator (across time) |
|---|---|---|
| simplest | one-hop graph conv $\tilde{\mathbf{A}}\mathbf{X}\mathbf{\Theta}$ | a GRU or LSTM over each node |
| DCRNN | diffusion conv (random-walk powers of $\mathbf{A}$) | GRU with graph-conv gates |
| STGCN | Chebyshev / 1st-order graph conv | gated 1-D temporal convolution |
| Graph WaveNet | graph conv on a learned adjacency | dilated causal TCN (WaveNet stack) |
| attention-based (GMAN, ASTGCN) | spatial self-attention over nodes | temporal self-attention over steps |
This factorization is the temporal-graph idea of Chapter 18.3 applied to a fixed graph with continuously valued node signals. There, the graph itself changed over time (edges appearing and vanishing as events occurred); here, the graph is static (roads do not move) and what evolves is the signal on the nodes. Both are instances of the same general object, a sequence of graph signals, and both are attacked by interleaving a graph operator with a temporal operator. The static-graph case is the gentler one and the right place to learn the pattern.
Take the chain of Figure 32.2.1 restricted to three sensors $S_1 - S_2 - S_3$ in a line, with current speeds (in km/h) $\mathbf{x} = [60, 30, 58]^\top$: $S_2$ is congested, its neighbors are free-flowing. Use the row-normalized adjacency that averages each node with its neighbors. $S_2$ has two neighbors plus itself, so its aggregated value is $\tfrac{1}{3}(60 + 30 + 58) = 49.3$ km/h. End node $S_1$ has one neighbor plus itself: $\tfrac{1}{2}(60 + 30) = 45$ km/h. End node $S_3$: $\tfrac{1}{2}(58 + 30) = 44$ km/h. Notice what the single hop did: the congestion at $S_2$ (30) has bled outward, pulling its free-flowing neighbors down toward 44 to 45 while $S_2$ itself was pulled up toward 49. After one graph-convolution step every node "knows" that a slowdown is adjacent, the precise signal a forecaster needs to predict that $S_1$ and $S_3$ are about to slow. A second hop would spread the influence two sensors out, which is why depth in the spatial operator equals reach along the road.
3. The Landmark Models: DCRNN, STGCN, Graph WaveNet Intermediate
Three models, all published between 2017 and 2019, defined the field and remain the baselines every new method must beat. Each is a specific point in the spatial-temporal grid of subsection two, and each contributed one durable idea.
DCRNN (Diffusion Convolutional Recurrent Neural Network, Li and colleagues, 2018) models the spatial axis as a diffusion process: it convolves the graph signal with random-walk transition matrices in both directions (downstream and upstream), capturing the directed, asymmetric way traffic propagates. Its temporal axis is a GRU (Chapter 10) in which every dense matrix multiply is replaced by this diffusion convolution, so the recurrence carries a graph-aware hidden state. The diffusion convolution of order $K$ is
$$\mathbf{Z} = \sum_{k=0}^{K-1}\Big( \theta_{k,1}\,(\mathbf{D}_O^{-1}\mathbf{A})^{k} + \theta_{k,2}\,(\mathbf{D}_I^{-1}\mathbf{A}^\top)^{k} \Big)\mathbf{X},$$with $\mathbf{D}_O$ and $\mathbf{D}_I$ the out- and in-degree matrices, so the two terms model forward and backward diffusion separately. DCRNN's lasting lesson: directionality matters, and a sequence-to-sequence GRU with graph-convolutional gates is a strong, principled design.
STGCN (Spatio-Temporal Graph Convolutional Network, Yu and colleagues, 2018) drops recurrence entirely. It stacks "sandwich" blocks, each a gated 1-D temporal convolution, then a graph convolution, then another temporal convolution, so the whole model is convolutional in both axes and therefore fully parallel over time (the TCN advantage of Chapter 11). It is faster to train than DCRNN and competitive in accuracy. Its lasting lesson: you do not need an RNN for the temporal axis; a temporal convolution is faster and often just as good.
Graph WaveNet (Wu and colleagues, 2019) introduced the most consequential idea of the three: the adaptive adjacency matrix. Rather than accept the distance-based graph as given, it learns a graph from data by parameterizing $\mathbf{A}_{\text{adp}} = \mathrm{softmax}(\mathrm{ReLU}(\mathbf{E}_1 \mathbf{E}_2^\top))$ from two learned node-embedding matrices $\mathbf{E}_1, \mathbf{E}_2$, and uses it alongside (or instead of) the given graph. Its temporal axis is a dilated causal convolution stack borrowed from WaveNet, giving a large receptive field cheaply. Its lasting lesson, and the central debate of the field, is that the physical road graph is not always the best graph for forecasting: a learned graph can discover couplings the distance metric misses (two sensors on parallel routes that substitute for each other, say) and frequently improves accuracy.
A given graph (built from road distance) encodes a strong, correct, interpretable prior and needs no data to estimate, which is invaluable when data are scarce. A learned graph (Graph WaveNet, AGCRN, MTGNN) is estimated jointly with the forecaster and can capture couplings the distance graph misses, but it adds $O(N^2)$ or $O(N \cdot d)$ parameters and risks overfitting on small networks. The empirically robust recipe is a hybrid: start from the given graph as a prior and let the model learn a residual adjacency on top, so the physics is respected and the data fills in what the physics omits. The lesson generalizes well beyond traffic: whenever you have a plausible structural prior, supplying it and learning a correction usually beats both "trust the prior blindly" and "learn everything from scratch."
Beyond these three, attention-based spatio-temporal Transformers (ASTGCN, GMAN, and the more recent STTN and PDFormer families) replace the fixed graph convolution with spatial self-attention, letting each node attend to every other node with data-dependent weights, and replace the temporal convolution with temporal self-attention. This is the Transformer of Chapter 12 applied along both axes. Attention buys flexibility (the "graph" is recomputed per input, effectively a fully learned, input-dependent adjacency) at the cost of $O(N^2)$ attention over nodes, which is why these models often inject the road graph as an attention bias to keep the prior. The arc from DCRNN's fixed diffusion to GMAN's learned attention is the same arc the whole field of sequence modeling walked: from fixed structure to learned, data-dependent structure.
One of the quietly funny results in the literature is that learned-adjacency models sometimes work best when their learned graph looks nothing like the road map. You hand the model a beautiful, GPS-accurate network topology, and it politely ignores most of it in favor of edges between sensors that are geographically far apart but statistically twinned (two on-ramps that always jam together because they feed the same stadium, for instance). The road graph is the truth about asphalt; the learned graph is the truth about traffic, and the two are related but not the same. It is a small humbling lesson that the obvious structure is not always the useful one.
4. Datasets, Benchmarks, and the Strong-Baseline Lesson Intermediate
Two dataset families anchor essentially all traffic-forecasting research. METR-LA is 207 loop detectors on the Los Angeles freeways, four months of five-minute speed readings; its sibling PEMS-BAY is 325 sensors in the San Francisco Bay Area. The PEMS family (PEMS03, PEMS04, PEMS07, PEMS08) comes from the California Performance Measurement System and reports flow as well as speed across hundreds of sensors. The standard protocol forecasts the next twelve steps (one hour) from the previous twelve, and reports mean absolute error (MAE), root mean squared error (RMSE), and mean absolute percentage error (MAPE) at the 15-, 30-, and 60-minute horizons. These benchmarks, their splits, and the broader evaluation discipline are catalogued in Appendix E; the running sensor/IoT dataset of this book (introduced in Chapter 2 and threaded through Chapter 8) is the same shape, a network of telemetry sensors over time.
The single most important practical lesson from a decade of these benchmarks is sobering: strong simple baselines are hard to beat, and many reported gains are small or fragile. Because traffic is so strongly periodic, the "historical average" baseline (predict this sensor's average speed for this time-of-day and day-of-week) and a per-sensor ARIMA or a plain per-node LSTM are all formidable. Careful re-evaluations have repeatedly found that the headline improvement of a fancy spatio-temporal model over a well-tuned univariate baseline is often in the low single-digit percentages of MAE, and sometimes vanishes under a fair hyperparameter budget. The graph helps most at the longer horizons (45 to 60 minutes) and during the non-periodic, incident-driven episodes where a neighbor's signal genuinely carries information the local history does not.
A typical METR-LA result table reports MAE at three horizons. Suppose the historical-average baseline scores MAE 4.16, a per-node LSTM scores 3.44, DCRNN scores 3.17, and Graph WaveNet scores 3.07 (all at the 60-minute horizon, in mph speed error, the standard METR-LA reporting unit). Read these numbers honestly. The jump from historical average (4.16) to any learned model (3.44) is large, about 17 percent, and that is mostly the temporal model doing its job. The further jump from the per-node LSTM (3.44) to the graph models (3.17, 3.07) is the contribution of the spatial graph: about 8 to 11 percent, real but far smaller than the temporal gain, and concentrated at the long horizon. The gap between the two graph models (3.17 vs 3.07, about 3 percent) is the kind of margin that careful re-evaluation sometimes erases. The takeaway: the temporal model is most of the win, the graph is a real but modest add-on, and tiny gaps between leaderboard entries deserve skepticism.
The honest framing, then, is not "graphs revolutionize traffic forecasting" but "graphs are a correct and useful inductive bias whose benefit is real, concentrated at long horizons and anomalous episodes, and frequently overstated by leaderboard culture." A practitioner should always implement the periodic and per-node baselines first, because they are cheap, strong, and the only fair yardstick against which a graph model's added complexity can be judged. This is the same discipline that Chapter 5 urged for univariate forecasting, now enforced on the spatio-temporal frontier.
Three currents define the recent frontier. First, the simple-model backlash: following the "are Transformers effective for time series?" critique, papers such as the STID family (2022) and subsequent linear and MLP-based spatio-temporal models have shown that a well-designed lightweight model with the right identity embeddings can match heavy graph Transformers on METR-LA and PEMS, sharpening the strong-baseline lesson of this subsection. Second, spatio-temporal foundation models: work in 2024 to 2025 (for example UniST, GPT-ST, and the pretrain-then-adapt line) trains a single model across many cities and sensor networks and transfers to unseen graphs with little or no fine-tuning, mirroring the temporal foundation models of Chapter 15. Third, continuous-time and physics-informed approaches: spatio-temporal neural ODE and graph-ODE models (and PDE-informed variants that bake in the conservation-of-vehicles transport equation) handle irregular sampling and extrapolate incidents better, connecting to the continuous-time models of Chapter 13. The live question for 2026: how much architecture does network-wide forecasting actually need, and where does a learned graph stop helping?
5. Worked Example: A Spatio-Temporal Forecaster From Scratch and With a Library Advanced
We now build a working traffic forecaster two ways. First from scratch: a small graph-convolution-plus-GRU cell that does exactly the spatial-then-temporal composition of subsection two, trained on a synthetic five-sensor chain whose congestion propagates upstream just like Figure 32.2.1. Then with a library: the same job in a few lines using PyTorch Geometric Temporal, whose DCRNN and A3TGCN layers package the graph convolution, the recurrence, and the multi-step readout for us. We begin by generating the synthetic data and building the from-scratch model. Code 32.2.1 sets up a propagating-congestion sensor chain and a one-hop spatial graph convolution.
import numpy as np
import torch
import torch.nn as nn
torch.manual_seed(0); np.random.seed(0)
N, P, H = 5, 12, 3 # 5 sensors, look back 12 steps, forecast 3 steps
# Build the chain graph S1-S2-S3-S4-S5 and its symmetrically normalized adjacency.
A = np.zeros((N, N))
for i in range(N - 1):
A[i, i + 1] = A[i + 1, i] = 1.0 # undirected chain edges
A_hat = A + np.eye(N) # add self-loops (A + I)
deg = A_hat.sum(1)
D_inv_sqrt = np.diag(1.0 / np.sqrt(deg))
A_norm = torch.tensor(D_inv_sqrt @ A_hat @ D_inv_sqrt, dtype=torch.float32) # D^-1/2 (A+I) D^-1/2
def make_traffic(T=2000):
"""Synthetic speeds: a daily sine plus congestion waves that travel upstream."""
t = np.arange(T)
base = 55 + 8 * np.sin(2 * np.pi * t / 96) # daily commuting rhythm
speed = np.tile(base, (N, 1)).T.astype(np.float32) # (T, N), identical baseline
for start in range(50, T - 30, 130): # inject incidents at S4 ...
for k in range(5): # ... propagating upstream S4->S0
node = 3 - k
if 0 <= node < N:
speed[start + k: start + k + 8, node] -= 22 # an 8-step slowdown
return np.clip(speed, 10, None)
series = make_traffic()
mu, sd = series.mean(), series.std()
series_n = (series - mu) / sd # standardize for training
class GraphConv(nn.Module):
"""One-hop spatial mixing: H = A_norm X Theta, the spatial operator of subsection 2."""
def __init__(self, f_in, f_out):
super().__init__()
self.theta = nn.Linear(f_in, f_out)
def forward(self, x): # x: (batch, N, f_in)
return torch.relu(self.theta(A_norm @ x)) # A_norm @ x mixes each node with neighbors
make_traffic builds a five-sensor chain with a daily sine baseline and incidents that start at $S_4$ and crawl upstream one sensor per step, the controllable analogue of Figure 32.2.1. GraphConv implements the one-hop convolution $\tilde{\mathbf{A}}\mathbf{X}\mathbf{\Theta}$, the exact spatial mixing whose effect we computed by hand in the subsection-two numeric example.With the spatial operator in hand, we braid it with a GRU to form the spatio-temporal cell, then train it to forecast three steps ahead. Code 32.2.2 defines the from-scratch graph-conv-plus-GRU forecaster and runs the training loop.
class GConvGRU(nn.Module):
"""Spatial-then-temporal: graph-convolve each step, then a GRU carries node history."""
def __init__(self, hid=16):
super().__init__()
self.gc = GraphConv(1, hid) # spatial operator (one feature per sensor)
self.gru = nn.GRU(hid, hid, batch_first=True) # temporal operator (Chapter 10)
self.head = nn.Linear(hid, H) # multi-step readout: H future speeds
def forward(self, x): # x: (batch, P, N)
b, P_, N_ = x.shape
feats = []
for p in range(P_): # apply the graph conv at every time step
xp = x[:, p, :].unsqueeze(-1) # (batch, N, 1)
feats.append(self.gc(xp)) # (batch, N, hid) spatially mixed
seq = torch.stack(feats, 1) # (batch, P, N, hid)
seq = seq.permute(0, 2, 1, 3).reshape(b * N_, P_, -1) # one GRU sequence per sensor
out, _ = self.gru(seq) # temporal pass over each node's history
return self.head(out[:, -1, :]).reshape(b, N_, H) # (batch, N, H) forecasts
def make_windows(s):
X, Y = [], []
for i in range(len(s) - P - H):
X.append(s[i:i + P]); Y.append(s[i + P:i + P + H])
return torch.tensor(np.array(X)), torch.tensor(np.array(Y)).permute(0, 2, 1)
X, Y = make_windows(series_n) # X:(M,P,N) Y:(M,N,H)
ntr = int(0.8 * len(X)); model = GConvGRU()
opt = torch.optim.Adam(model.parameters(), lr=1e-2)
lossf = nn.L1Loss() # MAE, the standard traffic metric
for epoch in range(15):
model.train(); opt.zero_grad()
pred = model(X[:ntr])
loss = lossf(pred, Y[:ntr]); loss.backward(); opt.step()
model.eval()
with torch.no_grad():
test_mae = lossf(model(X[ntr:]), Y[ntr:]).item() * sd # de-standardize to km/h
print("from-scratch GConvGRU test MAE = %.3f km/h" % test_mae)
from-scratch GConvGRU test MAE = 2.418 km/h
The from-scratch model in Code 32.2.1 and 32.2.2 ran roughly 55 lines: the adjacency construction, the graph-convolution module, the GRU braiding, the windowing, and the training loop. A purpose-built library collapses all of it. PyTorch Geometric Temporal ships the landmark architectures of subsection three as single layers: its DCRNN is the diffusion-convolution GRU, and its A3TGCN is an attention-augmented graph-conv GRU that emits a multi-step forecast directly. Code 32.2.3 rebuilds the forecaster with A3TGCN.
from torch_geometric_temporal.nn.recurrent import A3TGCN
from torch_geometric.utils import dense_to_sparse
edge_index, edge_weight = dense_to_sparse(torch.tensor(A, dtype=torch.float32))
class LibForecaster(nn.Module):
"""The whole spatial-plus-temporal stack in one layer: A3TGCN does graph conv + GRU + attn."""
def __init__(self, hid=16):
super().__init__()
self.recurrent = A3TGCN(in_channels=1, out_channels=hid, periods=P) # spatio-temporal block
self.head = nn.Linear(hid, H) # multi-step readout
def forward(self, x, ei, ew): # x: (N, 1, P) node features over the window
h = torch.relu(self.recurrent(x, ei, ew)) # (N, hid): graph-aware temporal embedding
return self.head(h) # (N, H): H-step forecast per sensor
lib = LibForecaster()
opt = torch.optim.Adam(lib.parameters(), lr=1e-2)
Xl = X.permute(0, 2, 1).unsqueeze(2) # (M, N, 1, P): PyG-Temporal node-feature layout
for epoch in range(15):
lib.train(); opt.zero_grad()
preds = torch.stack([lib(Xl[i], edge_index, edge_weight) for i in range(ntr)])
loss = nn.functional.l1_loss(preds, Y[:ntr]); loss.backward(); opt.step()
lib.eval()
with torch.no_grad():
preds = torch.stack([lib(Xl[i], edge_index, edge_weight) for i in range(ntr, len(Xl))])
print("PyG-Temporal A3TGCN test MAE = %.3f km/h" % (nn.functional.l1_loss(preds, Y[ntr:]).item() * sd))
A3TGCN layer plus a linear head, roughly 12 lines, a better-than-four-fold reduction. The library handles the graph convolution, the gated recurrence, and the attention over the window, and the multi-step output internally (A3TGCN is an attention-based GCN-GRU, not a diffusion model), and swapping A3TGCN for DCRNN changes the spatial operator to the diffusion convolution with no other edits.PyG-Temporal A3TGCN test MAE = 2.301 km/h
Read the pair together as the chapter's central demonstration. Code 32.2.2 built the spatial-then-temporal pattern by hand, exposing every moving part: the graph convolution that mixes neighbors, the GRU that carries history, the readout that emits a multi-step forecast. Code 32.2.3 replaced all of it with one A3TGCN layer and got a comparable, marginally better result in a quarter of the code. The from-scratch version is for understanding; the library version is for shipping. Both confirm the substantive point of the section: a spatio-temporal forecaster is a graph operator and a temporal operator braided together, and once you see the braid you can build it either way.
Who: The operations team at a metropolitan traffic management center responsible for variable-message signs and ramp metering across a freeway network of about 300 loop detectors, working from the PEMS-style sensor feed that mirrors the running sensor/IoT dataset of Chapter 2.
Situation: They needed 15-, 30-, and 60-minute network-wide speed forecasts to trigger ramp metering before congestion formed, rather than reacting after queues had already built.
Problem: Their incumbent system ran 300 independent per-sensor ARIMA models, which forecast the periodic rhythm well but were always late on incident-driven congestion, because a per-sensor model cannot see a slowdown forming two exits upstream until it arrives locally.
Dilemma: Adopt a graph spatio-temporal model (DCRNN or Graph WaveNet) for the upstream-propagation signal, at the cost of a heavier training pipeline and a GPU, or keep the simple, robust, interpretable per-sensor baselines that operators already trusted.
Decision: They kept the per-sensor historical-average and ARIMA baselines as a fallback and a fairness yardstick, and deployed a Graph WaveNet on top, using the road distance graph as the given prior plus a learned residual adjacency, exactly the hybrid recipe of subsection three.
How: They trained on a year of five-minute data with the standard twelve-in, twelve-out protocol, evaluated against the baselines at all three horizons, and shipped the graph model only for the horizons (45 and 60 minutes) where it measurably beat the baselines, routing the 15-minute forecasts, where the graph helped little, to the cheaper per-sensor model.
Result: The graph model cut 60-minute MAE by about 9 percent over the per-sensor ARIMA and, more importantly, flagged incident-driven slowdowns several minutes earlier because it read the upstream sensors, which let ramp metering engage before the queue formed. The 15-minute forecasts stayed on the cheap baseline with no loss.
Lesson: Deploy the graph where it pays (long horizons, incident episodes) and keep the strong simple baseline everywhere else; measure the graph's benefit per horizon rather than assuming it helps uniformly, the discipline subsection four insists on.
The from-scratch graph-conv-plus-GRU of Code 32.2.1 and 32.2.2 ran about 55 lines. PyTorch Geometric Temporal packages every landmark architecture of this section as a single recurrent or convolutional layer: DCRNN, A3TGCN, STConv (the STGCN block), GConvGRU, and MTGNN among others, each accepting an edge index and an optional edge weight and handling the graph convolution, the recurrence or temporal convolution, and the gating internally. Swapping the spatial-temporal architecture becomes a one-line import change, and the same data layout feeds them all, so comparing DCRNN against Graph WaveNet against an attention model is a loop over layer classes rather than three separate codebases. For the road graph itself, build the adjacency once from sensor coordinates with a Gaussian kernel on driving distance and reuse it across every model.
6. Summary and What Carries Forward Beginner
Traffic forecasting is the canonical spatio-temporal problem because it has all the ingredients in their clearest form: a real, interpretable graph of road sensors; signals that evolve in time at every node; and a propagation physics (congestion travelling upstream) that makes the spatial coupling genuinely informative rather than incidental. The winning architecture pattern is a factorization, a spatial operator (graph convolution over the road network) braided with a temporal operator (an RNN, a TCN, or attention, straight from Part III), and the landmark models DCRNN, STGCN, and Graph WaveNet are three points in that design space, differing mainly in their spatial operator and in whether the graph is given or learned. The benchmark literature (METR-LA, PEMS) teaches a hard-won discipline: the temporal model is most of the win, the graph is a real but modest add-on concentrated at long horizons and anomalous episodes, and strong simple baselines must always be the yardstick.
What carries forward: the spatial-temporal factorization recurs in the next section, Section 32.3 on video understanding, where the "graph" becomes a grid of pixels and the spatial operator a 2-D convolution, but the braid with a temporal operator is the same. The learned-graph idea connects forward to the structure-learning themes of trustworthy modeling in Chapter 33, and the deployment discipline of the practical example is the subject of Chapter 34. The single idea to keep is the braid: a spatio-temporal model is a graph operator and a temporal operator interleaved, and almost everything else is a choice of which operator to use on each axis.
Exercises
A congestion shockwave travels upstream (against the direction of traffic flow), but a clearing wave (when the incident is removed) also travels in a characteristic direction. Explain why a directed, asymmetric spatial operator such as DCRNN's two-term diffusion convolution can represent both, whereas a symmetric one-hop graph convolution on an undirected graph cannot distinguish upstream from downstream neighbors. Relate your answer to the in-degree and out-degree normalizations $\mathbf{D}_I^{-1}\mathbf{A}^\top$ and $\mathbf{D}_O^{-1}\mathbf{A}$ in the DCRNN formula of subsection three.
Starting from the from-scratch GraphConv of Code 32.2.1, (a) extend it to a two-hop convolution by applying A_norm twice (or by using $\tilde{\mathbf{A}}^2$) and measure whether the two-hop model forecasts the upstream-propagating incidents better than the one-hop model on the synthetic chain. Then (b) replace the fixed A_norm with a learned adjacency $\mathrm{softmax}(\mathrm{ReLU}(\mathbf{E}_1\mathbf{E}_2^\top))$ from two learned node-embedding matrices, as in Graph WaveNet, and compare its test MAE and its learned edges against the true chain graph. Does the learned graph recover the chain?
Using the strong-baseline discipline of subsection four, design an experiment on a real benchmark (METR-LA or a PEMS dataset via PyTorch Geometric Temporal) that isolates the contribution of the graph. Train three models, a per-node temporal model with no graph, a graph model on the given road adjacency, and a graph model on a learned adjacency, under an identical hyperparameter budget, and report MAE at the 15-, 30-, and 60-minute horizons separately. At which horizon does the graph first earn its complexity, and how large is the gain relative to the temporal-only model? Discuss whether your finding supports or undercuts the leaderboard-culture critique of the research-frontier callout.