Spatiotemporal Modelling in R — CARBayesST¶
The space-time CAR with the package that defines it¶
The Python notebook built the space-time ANOVA from scratch and cross-checked it in PyMC. Here we fit the same model with CARBayesST — the R package purpose-built for spatiotemporal areal data — on the identical simulated Georgia panel (real 159-county geography, real population, a simulated 12-year process with known truth).
ST.CARanova fits exactly the Knorr-Held ANOVA decomposition of the Python notebook:
$$y_{it}\sim\text{Poisson}\!\big(E_{it}\,e^{\alpha+x_i'\beta+\phi_i+\delta_t\,(+\,\psi_{it})}\big),$$
a spatial main effect $\phi_i$ (a Leroux CAR field), a temporal main effect $\delta_t$ (also CAR-smoothed over time), and an optional space-time interaction $\psi_{it}$. We fit the additive form (interaction=FALSE) to match the from-scratch model, recover the truth, and map it. The sibling ST.CARar (spatial CAR × AR(1) in time) is the autoregressive alternative.
suppressWarnings(suppressMessages({
.libPaths(c("C:/Users/user/R/win-library/4.6", .libPaths()))
library(CARBayesST); library(sf); library(ggplot2)
}))
options(repr.plot.width=9.8, repr.plot.height=3.9)
w <- read.csv("ga_st_counts.csv"); N <- nrow(w); Tt <- 12
Wm <- as.matrix(read.csv("ga_adj.csv")); dimnames(Wm) <- NULL
phi_true <- read.csv("ga_st_truth_spatial.csv")$phi_true
gamma_true <- read.csv("ga_st_truth_temporal.csv")$gamma_true
# long format: spatial units nested within time periods (CARBayesST ordering) = column-major stack
long <- data.frame(
Y = as.vector(as.matrix(w[, paste0("y", 0:11)])),
x = rep(w$x, times = Tt),
E = rep(w$pop / 1000, times = Tt),
fips = rep(w$fips, times = Tt),
year = rep(0:11, each = N))
cat(sprintf("Georgia space-time panel: %d counties x %d years = %d rows; %d events\n",
N, Tt, nrow(long), sum(long$Y)))
cat("W:", nrow(Wm), "x", ncol(Wm), " spatial adjacency (symmetric:", isSymmetric(Wm), ")\n")
Georgia space-time panel: 159 counties x 12 years = 1908 rows; 59114 events
W: 159 x 159 spatial adjacency (symmetric: TRUE )
1. Fit the space-time ANOVA¶
ST.CARanova with a Poisson likelihood, the covariate x, a log(E) offset, and the 159×159 spatial weight matrix. interaction=FALSE gives the additive spatial + temporal model matching the from-scratch engine.
set.seed(1)
m <- ST.CARanova(Y ~ x + offset(log(E)), family="poisson", data=long, W=Wm,
interaction=FALSE, burnin=2000, n.sample=7000, thin=5, verbose=FALSE)
cat("samples available:", paste(names(m$samples), collapse=", "), "\n\n")
beta_x <- mean(m$samples$beta[, 2])
bq <- quantile(m$samples$beta[, 2], c(.025, .975))
phi_e <- colMeans(m$samples$phi) # spatial main effect (length N)
del_e <- colMeans(m$samples$delta) # temporal main effect (length Tt)
cat(sprintf("covariate beta: CARBayesST %.2f [%.2f, %.2f] (true 0.30)\n", beta_x, bq[1], bq[2]))
cat(sprintf("spatial field recovery: corr(estimated, true) = %.2f\n", cor(phi_e, phi_true)))
cat(sprintf("temporal wave recovery: corr(estimated, true) = %.2f\n", cor(del_e, gamma_true)))
samples available: beta, phi, delta, gamma, tau2, rho, fitted, Y
covariate beta: CARBayesST 0.29 [0.25, 0.34] (true 0.30)
spatial field recovery: corr(estimated, true) = 0.99
temporal wave recovery: corr(estimated, true) = 1.00
2. Map the space-time structure¶
The estimated spatial field (which counties run high, averaged over time) beside the truth, and the temporal trend — CARBayesST's smoothed epidemic wave against the simulated one. Maps rendered inline from the Georgia county geometry.
geo <- st_read("ga_counties.geojson", quiet=TRUE)
geo$phi_e <- phi_e[match(geo$id, w$fips)]
geo$phi_t <- phi_true[match(geo$id, w$fips)]
lim <- max(abs(c(geo$phi_e, geo$phi_t)))
suppressWarnings(suppressMessages(library(gridExtra)))
g1 <- ggplot(geo) + geom_sf(aes(fill=phi_t), color="white", size=0.05) +
scale_fill_gradient2(limits=c(-lim,lim), name=expression(phi)) +
ggtitle("TRUE spatial field") + theme_void() + theme(plot.title=element_text(size=11))
g2 <- ggplot(geo) + geom_sf(aes(fill=phi_e), color="white", size=0.05) +
scale_fill_gradient2(limits=c(-lim,lim), name=expression(phi)) +
ggtitle("ESTIMATED spatial field (CARBayesST)") + theme_void() + theme(plot.title=element_text(size=11))
grid.arrange(g1, g2, ncol=2)
options(repr.plot.width=6, repr.plot.height=3.4)
td <- data.frame(year=0:11, est=del_e, true=gamma_true)
ggplot(td, aes(year)) +
geom_hline(yintercept=0, linewidth=0.3, color="grey60") +
geom_line(aes(y=true, color="true"), linewidth=1) + geom_point(aes(y=true, color="true"), size=2) +
geom_line(aes(y=est, color="estimated"), linewidth=1, linetype=2) + geom_point(aes(y=est, color="estimated"), size=2) +
scale_color_manual(values=c(true="#2f855a", estimated="#c53030"), name="") +
labs(x="year", y=expression(delta[t]), title="Temporal main effect: CARBayesST vs truth") +
theme_minimal() + theme(legend.position="top")
3. Summary¶
CARBayesST fits the space-time ANOVA directly and recovers the simulated structure — the covariate near its true 0.30, the spatial field and temporal wave both closely correlated with the truth — reproducing the from-scratch and PyMC results of the Python notebook on the identical Georgia panel. The three engines agree.
The package's interaction=TRUE adds the space-time interaction $\psi_{it}$ (counties departing from the shared wave), and ST.CARar swaps the temporal CAR for an AR(1) process — the natural next steps once a real multi-year panel is in hand. That real application is the disability belt over time: SSA's yearly county receipt panel run through exactly this model would show whether the belt is expanding, stable, or shifting — the spatiotemporal continuation of The Disability Belt — a Spatial Application, and the capstone of the spatial arc (areal → econometric → geostatistical → point-process → spatiotemporal).