---
title: "Conditional autoregressive (CAR) models"
description: "Build areal spatial dependence from local conditional distributions, fit a proper CAR by hand, contrast it with SAR, and use the intrinsic CAR as a smoother."
date: "2026-06-05 11:00"
date-modified: "2026-10-07"
categories: [spatial, regression, autocorrelation, areal data, ecology tutorial]
image: thumbnail.png
image-alt: "A lattice of square cells shaded in tones of green, darker towards the top and paler towards the bottom, with neighbouring cells taking similar shades. One cell is picked out by a darker outline and its four immediate neighbours above, below, left and right are ringed in gold, the local neighbourhood a CAR model conditions on."
---
*Corrected 7 October 2026: the right-hand map of the first figure was still titled a proper-CAR field after its caption was corrected; its title now calls it the simulated response, covariate plus proper-CAR error; the heading of the section that compares the two models also still said that a CAR is not a SAR in disguise, the general claim an earlier correction withdrew, and it now names what the section compares, a CAR and a SAR built on the same neighbours.*
*Corrected 1 October 2026: the caption of the first figure presented its right-hand map as the CAR field without saying that most of its pattern comes from the covariate, and the text description of the smoothing figure said the noise was removed when part of it remains; both now describe the maps as they are.*
*Corrected 30 September 2026: the post said, citing Ver Hoef and colleagues, that neither a CAR nor a SAR is a reparametrisation of the other except in special cases; either can in fact be rewritten as the other with different neighbour weights, and the post now says that the two differ when built on the same neighbourhood matrix.*
The [simultaneous autoregressive models](../spatial-autoregressive-models-sar/) build spatial dependence from the top down: one global equation, `y = rho W y + X beta + eps`, solved all at once. Conditional autoregressive (CAR) models take the opposite route. They specify, for each cell, only its distribution *given its neighbours*, and let a theorem stitch those local pieces into a coherent joint distribution. This bottom-up construction, due to Besag (1974), is the engine behind most Bayesian disease mapping and behind the spatial random effects in ecological hierarchical models. It is worth understanding directly.
This tutorial writes the conditional model down, assembles its precision matrix, fits a proper CAR by hand in base R, and then does three things the textbooks often skip: it shows why the CAR autocorrelation parameter is not comparable across software packages, it contrasts the covariance a CAR implies with the one a SAR implies, and it uses the intrinsic CAR as a practical smoother.
## The conditional specification
A CAR model says that, conditional on every other cell, cell `i` is normal with a mean pulled towards its neighbours:
$$ y_i \mid y_{-i} \sim \mathcal{N}\!\left(\mu_i + \rho \sum_j \frac{a_{ij}}{d_i}(y_j - \mu_j),\ \frac{\tau^2}{d_i}\right), $$
where `a_ij` marks neighbours, `d_i` is the number of neighbours of cell `i`, and `mu = X beta`. The Hammersley-Clifford theorem guarantees these conditionals correspond to a single joint Gaussian, and its precision matrix (the inverse covariance) is wonderfully sparse:
$$ Q = \frac{1}{\tau^2}(D - \rho A), $$
with `D` the diagonal matrix of neighbour counts and `A` the adjacency matrix. The model is *proper* (a valid distribution) when `D - rho A` is positive definite, which pins `rho` inside a range set by the eigenvalues.
## Setting up the lattice
```{r setup, include=FALSE}
library(ggplot2)
library(grid)
knitr::opts_chunk$set(echo = TRUE, message = FALSE, warning = FALSE,
dev = "png", dev.args = list(bg = "#f5f4ee"),
dpi = 150, fig.width = 8, fig.height = 3.5)
theme_te <- function() {
theme_minimal(base_size = 11) +
theme(plot.background = element_rect(fill = "#f5f4ee", colour = NA),
panel.background = element_rect(fill = "#f5f4ee", colour = NA),
panel.grid = element_blank(),
plot.title = element_text(face = "bold", size = 11),
plot.subtitle = element_text(size = 9, colour = "grey30"),
axis.text = element_blank(), axis.ticks = element_blank(),
legend.key.width = unit(0.35, "cm"))
}
tilemap <- function(df, value, title, subtitle = NULL) {
ggplot(df, aes(col, row, fill = value)) + geom_tile() + coord_equal() +
scale_fill_viridis_c(option = "mako", direction = -1) +
labs(title = title, subtitle = subtitle, x = NULL, y = NULL, fill = NULL) +
theme_te()
}
two_panel <- function(g1, g2) {
grid.newpage(); pushViewport(viewport(layout = grid.layout(1, 2)))
print(g1, vp = viewport(layout.pos.row = 1, layout.pos.col = 1))
print(g2, vp = viewport(layout.pos.row = 1, layout.pos.col = 2))
}
```
```{r lattice}
side <- 20; n <- side * side
coords <- expand.grid(col = 1:side, row = 1:side)
A <- matrix(0, n, n)
idx <- function(r, c) (c - 1) * side + r
for (r in 1:side) for (c in 1:side) {
i <- idx(r, c)
if (r > 1) A[i, idx(r - 1, c)] <- 1
if (r < side) A[i, idx(r + 1, c)] <- 1
if (c > 1) A[i, idx(r, c - 1)] <- 1
if (c < side) A[i, idx(r, c + 1)] <- 1
}
deg <- rowSums(A); D <- diag(deg)
# eigenvalues of the row-standardised operator fix the admissible rho range
ev <- eigen(diag(1 / sqrt(deg)) %*% A %*% diag(1 / sqrt(deg)), symmetric = TRUE)$values
rho_lo <- 1 / min(ev); rho_hi <- 1 / max(ev)
c(lower = rho_lo, upper = rho_hi)
```
## Simulating a proper CAR field
We draw one realisation with an intercept, a covariate, a strong conditional dependence of `rho = 0.9`, and a conditional scale of `tau = 0.4`. Sampling is direct from the joint covariance `tau^2 (D - rho A)^{-1}`.
```{r simulate}
set.seed(4231)
cc <- scale(coords$col)[, 1]
x <- as.numeric(scale(0.8 * cos(pi * coords$row / 7) + 0.6 * cc + rnorm(n, 0, 0.6)))
X <- cbind(1, x)
b0 <- 0.5; b1 <- 0.7; rho <- 0.9; tau <- 0.4
Q <- (D - rho * A) / tau^2
set.seed(11)
y <- as.numeric(X %*% c(b0, b1) + t(chol(solve(Q))) %*% rnorm(n))
```
## Fitting a CAR by hand
The log-likelihood needs the log-determinant of the precision. Factor it as `|D - rho A| = |D| * |I - rho D^{-1}A|`, so the determinant reduces to a sum over the same eigenvalues we already have. For each candidate `rho` we solve the generalised least squares problem for `beta`, read off `tau` from the quadratic form, and search over the single remaining parameter.
```{r car-fit}
nll_car <- function(r) {
if (r <= rho_lo || r >= rho_hi) return(1e10)
P <- D - r * A # precision up to 1/tau^2
be <- solve(t(X) %*% P %*% X, t(X) %*% P %*% y)
e <- y - X %*% be; t2 <- as.numeric(t(e) %*% P %*% e) / n
ldetP <- sum(log(deg)) + sum(log(1 - r * ev)) # log|D - rho A|
-(-n / 2 * log(2 * pi) - n / 2 - n / 2 * log(t2) + 0.5 * ldetP)
}
opt <- optimise(nll_car, c(rho_lo + 1e-4, rho_hi - 1e-4))
car_rho <- opt$minimum
P <- D - car_rho * A
be <- solve(t(X) %*% P %*% X, t(X) %*% P %*% y)
car_b0 <- be[1]; car_b1 <- be[2]
car_tau <- sqrt(as.numeric(t(y - X %*% be) %*% P %*% (y - X %*% be)) / n)
car_ll <- -opt$objective
round(c(rho = car_rho, tau = car_tau, b0 = car_b0, b1 = car_b1), 4)
```
The fit recovers the generating values well: a conditional dependence of `r round(car_rho, 2)` against the true `r rho`, a scale of `r round(car_tau, 2)` against `r tau`, and a slope of `r round(car_b1, 2)` against `r b1`. The likelihood machinery is the same log-determinant plus generalised least squares we used for the error model in the SAR tutorial, only the precision differs.
## A warning about conventions
Reach for a package and you may be surprised. The `spautolm` function in spatialreg (it lived in spdep before the model-fitting code was split out), with `family = "CAR"`, does not use the neighbour-count precision `D - rho A`. It uses a *symmetric-weights* convention, which with binary weights gives the precision `I - lambda A`. That is a different model, so its `lambda` is not on the same scale as our `rho` and the two numbers will never agree. To show this is a convention and not an error, we can fit the symmetric version by hand as well.
```{r car-symmetric}
evA <- eigen(A, symmetric = TRUE)$values
loA <- 1 / min(evA); hiA <- 1 / max(evA)
nll_sym <- function(lam) {
if (lam <= loA || lam >= hiA) return(1e10)
P <- diag(n) - lam * A
be <- solve(t(X) %*% P %*% X, t(X) %*% P %*% y); e <- y - X %*% be
s2 <- as.numeric(t(e) %*% P %*% e) / n
-(-n / 2 * log(2 * pi) - n / 2 - n / 2 * log(s2) + 0.5 * sum(log(1 - lam * evA)))
}
car_sym_lambda <- optimise(nll_sym, c(loA + 1e-6, hiA - 1e-6))$minimum
round(car_sym_lambda, 4)
```
The symmetric convention reports `r round(car_sym_lambda, 3)`, and this by-hand value matches spatialreg's `spautolm` to numerical precision. It is fitted to the same data as our `r round(car_rho, 2)`, but under a different model, with a parameter on a different scale. The lesson is practical: never compare a CAR autocorrelation parameter across papers or packages without first checking which weights convention each one uses.
## CAR and SAR on the same neighbours
Both models encode neighbourhood dependence, but built on the same neighbourhood matrix they imply different covariances (Ver Hoef et al. 2018 compare the two families); rewriting one as the other means changing the neighbour weights. A useful way to see the difference is to trace how the correlation between a central cell and the rest decays with graph distance, the number of steps through the lattice.
```{r reach}
centre <- idx(10, 10)
gdist <- rep(NA_integer_, n); gdist[centre] <- 0L; step <- 0L; front <- centre
repeat {
nb <- setdiff(unique(unlist(lapply(front, function(i) which(A[i, ] > 0)))),
which(!is.na(gdist)))
if (!length(nb)) break
step <- step + 1L; gdist[nb] <- step; front <- nb
}
Ccar <- cov2cor(solve(D - 0.9 * A))[centre, ] # CAR reach
Wrs <- A / deg
Csar <- cov2cor(solve(crossprod(diag(n) - 0.6 * Wrs)))[centre, ] # SAR reach
reach <- aggregate(data.frame(CAR = Ccar, SAR = Csar), list(dist = gdist), mean)
round(head(reach, 5), 3)
```
At the first step the two are tuned to match, near `r round(reach$CAR[reach$dist == 1], 2)`. Further out they part company: at three steps the CAR retains a correlation of `r round(reach$CAR[reach$dist == 3], 3)` against the SAR's `r round(reach$SAR[reach$dist == 3], 3)`. The conditional model here carries dependence further across the map than the simultaneous one at matched short range. Which shape suits your system is an empirical question, and one more reason the two model families are worth keeping distinct.
```{r fig1-reach}
#| echo: false
#| fig-cap: "Correlation with the centre cell against graph distance for a CAR and a SAR tuned to match at one step (left), and one realisation of the simulated response on the lattice, covariate plus proper-CAR error, most of whose visible pattern comes from the covariate (right)."
#| fig-alt: "Two panels. The left panel is a line chart of correlation against graph distance for CAR and SAR, both starting at one and decaying, with the CAR line staying above the SAR line beyond the first step. The right panel is a grid map of that simulated response, speckled from cell to cell and darker towards the right-hand side, a pattern that comes mostly from the covariate."
long <- data.frame(dist = rep(reach$dist, 2),
corr = c(reach$CAR, reach$SAR),
model = rep(c("CAR", "SAR"), each = nrow(reach)))
long <- long[long$dist <= 8, ]
g1 <- ggplot(long, aes(dist, corr, colour = model)) +
geom_line(linewidth = 0.9) + geom_point(size = 1.6) +
scale_colour_manual(values = c(CAR = "#275139", SAR = "#c98a1a")) +
labs(title = "Correlation with the centre cell",
subtitle = "by number of steps through the lattice",
x = "graph distance", y = "correlation", colour = NULL) +
theme_te() + theme(axis.text = element_text(size = 8),
panel.grid.major.y = element_line(colour = "grey92"),
legend.position = "inside", legend.position.inside = c(0.8, 0.8))
g2 <- tilemap(coords, y, "Simulated response", "covariate plus proper-CAR error, rho = 0.9")
two_panel(g1, g2)
```
## The intrinsic CAR as a smoother
Push `rho` all the way to its boundary and the precision becomes `D - A`, the graph Laplacian. This is the *intrinsic* CAR (ICAR): an improper distribution (it has no overall level, hence the usual sum-to-zero constraint) that penalises differences between neighbours. It is the most widely used spatial random effect in ecology and epidemiology, because as a smoother it borrows strength across the map. To see it work, we take a noisy signal, one observation per cell, and recover the underlying smooth field.
```{r icar}
set.seed(4232)
truef <- as.numeric(scale(1.4 * sin(pi * coords$col / 8) * cos(pi * coords$row / 9)))
yn <- truef + rnorm(n, 0, 1.0) # noisy observation per cell
R_icar <- D - A # intrinsic precision (graph Laplacian)
# GMRF smoother S = (I + kappa R)^{-1}; choose kappa by generalised cross-validation
gcv <- function(kappa) {
S <- solve(diag(n) + kappa * R_icar); fit <- S %*% yn
n * sum((yn - fit)^2) / (n - sum(diag(S)))^2
}
ks <- 10^seq(-2, 2, length = 40); k_opt <- ks[which.min(sapply(ks, gcv))]
Sopt <- solve(diag(n) + k_opt * R_icar)
fit_ic <- as.numeric(Sopt %*% yn)
icar_kappa <- k_opt
icar_edf <- sum(diag(Sopt))
icar_rmse_raw <- sqrt(mean((yn - truef)^2))
icar_rmse_sm <- sqrt(mean((fit_ic - truef)^2))
round(c(kappa = icar_kappa, edf = icar_edf,
rmse_raw = icar_rmse_raw, rmse_smooth = icar_rmse_sm), 3)
```
Cross-validation settles on a smoother with about `r round(icar_edf, 0)` effective degrees of freedom out of `r n` cells. It cuts the root mean squared error against the truth from `r round(icar_rmse_raw, 2)` for the raw observations to `r round(icar_rmse_sm, 2)`, roughly halving it, simply by letting each cell learn from its neighbours. This borrowing of strength is exactly what an ICAR random effect does inside a larger model.
```{r fig2-smooth}
#| echo: false
#| fig-cap: "Noisy per-cell observations (left) and the same cells after the intrinsic CAR smoother (right), drawn on a common colour scale."
#| fig-alt: "Two grid maps side by side. The left panel shows noisy per-cell observations with a speckled appearance. The right panel shows the ICAR-smoothed field, in which a broad smooth pattern stands out, with part of the cell-to-cell noise still visible."
lim <- range(c(yn, fit_ic))
p1 <- tilemap(coords, yn, "Noisy observations", "one draw per cell") +
scale_fill_viridis_c(option = "mako", direction = -1, limits = lim)
p2 <- tilemap(coords, fit_ic, "ICAR smoother", paste0("edf = ", round(icar_edf, 0))) +
scale_fill_viridis_c(option = "mako", direction = -1, limits = lim)
two_panel(p1, p2)
```
## What to carry forward
A CAR model is built from local conditional rules and summarised by a sparse precision matrix `D - rho A`. It is the natural spatial term for hierarchical and generalised linear mixed models, where the conditional form slots neatly into the wider structure. Three points are worth remembering. The autocorrelation parameter depends on the weights convention, so it is not portable between software. A CAR and a SAR on the same neighbourhood imply genuinely different covariances, and the choice between them is substantive, not cosmetic. And the intrinsic CAR, the boundary case, is less a model of correlation than a smoother, penalising neighbour differences to pool information.
That smoothing power comes with a hidden cost. When the spatial random effect is flexible enough to mimic a spatially structured covariate, it can quietly absorb that covariate's effect. This is spatial confounding, and [checking a spatial regression](../checking-a-spatial-regression/) takes it apart in detail.
## References
Besag J 1974 Journal of the Royal Statistical Society: Series B 36(2):192-225 (10.1111/j.2517-6161.1974.tb00999.x)
Dormann CF, McPherson JM, Araujo MB, Bivand R, Bolliger J, Carl G, et al. 2007 Ecography 30(5):609-628 (10.1111/j.2007.0906-7590.05171.x)
Ver Hoef JM, Peterson EE, Hooten MB, Hanks EM, Fortin M-J 2018 Ecological Monographs 88(1):36-59 (10.1002/ecm.1283)
## Related tutorials
- [Spatial lag and spatial error models (SAR)](../spatial-autoregressive-models-sar/) build areal dependence from a single simultaneous equation.
- [Moran eigenvector spatial filtering](../moran-eigenvector-spatial-filtering/) offers a third route that fits inside a generalised linear model.
- [Checking a spatial regression](../checking-a-spatial-regression/) compares CAR, SAR and filtering and exposes spatial confounding.
- [Generalised least squares for spatial correlation](../gls-spatial-correlation/) covers the continuous-distance analogue.