---
title: "Dynamic Bayesian Predictive Stacking for Spatiotemporal Analysis - Tutotial"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{Dynamic Bayesian Predictive Stacking for Spatiotemporal Analysis - Tutotial}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
bibliography: references.bib
---

```{r, include = FALSE}
knitr::opts_chunk$set(
  collapse = TRUE,
  comment = "#>",
  warning = FALSE,
  message = FALSE
)
```

We provide a brief tutorial of the `spFFBS` package. Here we shows the implementation of the Dynamic Bayesian Predictive Stacking on synthetically generated data. For any further details please refer to [@presicce2026]. More examples, are available in documentation, and functions help.

### Working packages
```{r setup}
library(spFFBS)
library(spBPS)
```

```{r, echo=FALSE, warning=FALSE, message=FALSE}
library(mniw)
library(MBA)
library(abind)
library(ggplot2)
library(patchwork)
library(reshape2)
```


### Data generation
We generate data from the model detailed in Equation (1) [@presicce2026], over a unit square.
```{r, results=F}
# Dimensions
tmax <- 25
tnew <- 5
n    <- 150
q    <- 2
p    <- 2
u    <- 50

# Parameters
Sigma  <- matrix(c(1, -0.3, -0.3, 1), q, q)
phi <- 8
alfa <- 0.8
a <- ((1/alfa)-1)
V <- a*diag((n+u))

set.seed(1)
# Generate constant sinthetic data structure
coords <- matrix(runif((n+u) * 2), ncol = 2)
D <- spBPS:::arma_dist(coords)
K <- exp(-phi * D)
W <- rbind( cbind(diag(p), matrix(0, p, (n+u))), cbind(matrix(0, (n+u), p), K) )

# Prior information and initial state
m0     <- matrix(0, (n+u)+p, q)
C0 <- rbind( cbind(diag(0.005, p), matrix(0, p, (n+u))), cbind(matrix(0, (n+u), p), K) )
theta0 <- mniw::rMNorm(n = 1, Lambda = m0, SigmaR = C0, SigmaC = Sigma)

# Generate dynamic sinthetic data structure
G <- array(0, c((n+u)+p, (n+u)+p, tmax+tnew))
theta <- array(0, c((n+u)+p, q, tmax+tnew))
X <- array(0, c((n+u), p, tmax+tnew))
P <- array(0, c((n+u), (n+u)+p, tmax+tnew))
Y <- array(0, c((n+u), q, tmax+tnew))

set.seed(1)
for (t in 1:(tmax+tnew)) {
  if (t >= 2) {  
    
  G[,,t]     <- diag(p+n+u)
  theta[,,t] <- G[,,t] %*% theta[,,t-1] + mniw::rMNorm(n = 1, Lambda = m0, SigmaR = W, SigmaC = Sigma)
  X[,,t]     <- matrix(runif((n+u)*p), (n+u), p)
  P[,,t]     <- cbind(X[,,t], diag((n+u)))
  Y[,,t]     <- P[,,t] %*% theta[,,t] + mniw::rMNorm(n = 1, Lambda = matrix(0, (n+u), q), SigmaR = V, SigmaC = Sigma)
  } 
  else {
    
  G[,,t]     <- diag(p+n+u)
  theta[,,t] <- G[,,t] %*% theta0 + mniw::rMNorm(n = 1, Lambda = m0, SigmaR = W, SigmaC = Sigma)
  X[,,t]     <- matrix(runif((n+u)*p), (n+u), p)
  P[,,t]     <- cbind(X[,,t], diag((n+u)))
  Y[,,t]     <- P[,,t] %*% theta[,,t] + mniw::rMNorm(n = 1, Lambda = matrix(0, (n+u), q), SigmaR = V, SigmaC = Sigma)
  }
}

# Unobserved data
Yfuture <- Y[(1:n),,(tmax+1):(tmax+tnew)]
Ytilde <- Y[-(1:n),,]
thetatilde <- theta[-(1:(n+p)),,]
Xtilde <- X[-(1:n),,]
crdtilde <- coords[-(1:n),]
Dtilde   <- as.matrix(dist(crdtilde))
Ktilde   <- exp(-phi*Dtilde)

# Observed data
Y <- Y[(1:n),,1:tmax]
X <- X[(1:n),,]
P     <- P[(1:n),1:(n+p),]
G     <- G[1:(n+p), 1:(n+p),]
crd <- coords[1:n,]
D   <- as.matrix(dist(crd))
K   <- exp(-D)
W <- rbind( cbind(diag(p), matrix(0, p, n)), cbind(matrix(0, n, p), K) )
V <- a*diag((n))
```

### Setting priors and hyperparameters
```{r, results=F}
# Priors
m0     <- matrix(0, n+p, q)
C0 <- rbind( cbind(diag(0.005, p), matrix(0, p, n)), cbind(matrix(0, n, p), K) )
nu0 <- 3
Psi0 <- diag(q)
prior <- list("m" = m0, "C" = C0, "nu" = nu0, "Psi" = Psi0)

# hyperparameters values
alfa_seq <- c(0.7, 0.8, 0.9)
phi_seq <- c(6, 8, 9)
par_grid <- list(tau = alfa_seq, phi = phi_seq)
```

### Dynamic BPS fit
Posterior inference, forecast and spatial interpolation call (using 1 computing core):
```{r, results=F}
out <- spFFBS::spFFBS(Y = Y, X = X[,,1:tmax], D = D,
                      grid = par_grid,
                      prior = prior,
                      L = 200,
                      do_BS = TRUE,
                      do_forecast = TRUE,
                      tnew = tnew,
                      X_all = X,
                      do_spatial = TRUE,
                      spatial = list(crd = crd,
                                     crdtilde = crdtilde,
                                     Xtilde = Xtilde[,,tmax+tnew],
                                     t = tmax+tnew),
                      n_threads = 1)
```

### Beta posterior inference
```{r}
theta_post <- sapply(1:tmax, function(t){ out$BS[[t]] }, simplify = "array")
beta_post <- theta_post[1:p, 1:q,,]
```

```{r, echo=F, fig.dim = c(7.25, 5)}
# 1. Configurazione margini
oldpar <- par(no.readonly = TRUE)
# oma = 7 lascia lo spazio necessario a destra per la legenda
par(mfrow = c(p, q), mar = c(3, 3, 2, 1), oma = c(0, 0, 0, 7))

for (i in 1:p) {
  for (j in 1:q) {
    quants <- t(apply(theta_post[i,j,,], 2, quantile, c(0.025, 0.5, 0.975)))
    
    plot(1:nrow(quants), quants[,2], type = "n", 
         ylim = range(quants, theta[i,j,]),
         xlab = "", ylab = "", 
         main = bquote(hat(Beta)[list(.(i),.(j))]),
         cex.main = 0.9)
    
    polygon(c(1:nrow(quants), rev(1:nrow(quants))), c(quants[,1], rev(quants[,3])), 
            col = rgb(0, 0, 1, 0.15), border = NA)
    lines(1:nrow(quants), quants[,1], col = 4, lty = 2, lwd = 0.7)
    lines(1:nrow(quants), quants[,3], col = 4, lty = 2, lwd = 0.7)
    lines(1:nrow(quants), quants[,2], col = 4, lwd = 1.5)
    lines(1:tmax, theta[i,j,1:tmax], col = 2, lwd = 1.2)
  }
}

# 2. Posizionamento Legenda "Ancorato al Dispositivo"
par(xpd = NA)

# Usiamo NDC per l'intera pagina: 
# x = 0.92 (circa centro dello spazio OMA a destra)
# y = 0.5  (centro esatto verticale della figura)
legend(x = grconvertX(0.92, from = "ndc"), 
       y = grconvertY(0.5, from = "ndc"),  
       legend = c("True", "MAP", "95% CI"),
       col = c(2, 4, rgb(0, 0, 1, 0.15)), 
       lty = c(1, 1, 1), 
       lwd = c(1.2, 1.5, NA),
       pch = c(NA, NA, 15),
       pt.cex = c(0, 0, 1.8),
       bty = "n", 
       xjust = 0.5, # Centra la legenda rispetto al punto X
       yjust = 0.5, # Centra la legenda rispetto al punto Y
       cex = 0.8)

par(oldpar)
```

```{r, echo=FALSE}
# Global weights
Wglobal <- out$Wglobal
models_labels <- attr(out$Wi, "model_labels")
# rownames(Wglobal) <- models_labels
# Wglobal
```

### Sigma posterior inference
```{r}
# Global weights
Wglobal <- out$Wglobal
J <- nrow(Wglobal)
L <- 200

# Posterior sampling
set.seed(1)
indL <- sample(1:J, L, Wglobal, rep = T)
Sigma_post <- sapply(1:L, function(l) {
  mniw::riwish(1, nu = out$FF[[tmax]]$filtered_results[[indL[l]]]$nu,
               Psi = out$FF[[tmax]]$filtered_results[[indL[l]]]$Psi) },
  simplify = "array")
Sigma_map <- apply(Sigma_post, c(1,2), median)
```

```{r, echo=F, fig.dim = c(7.25, 3)}
# Plotting
# 2. Calcoliamo il range comune per la legenda
all_values <- c(as.vector(Sigma), as.vector(Sigma_map))
common_limits <- range(all_values)

# 3. Funzione per creare i singoli heatmap
plot_matrix <- function(mat, titolo, limits) {
  df <- melt(mat)
  ggplot(df, aes(Var2, Var1, fill = value)) +
    geom_tile(color = "white", size = 0.5) +
    geom_text(aes(label = sprintf("%.2f", value)), color = "black", size = 5) +
    # Usiamo limits prefissati per uniformare la scala
    scale_fill_gradient2(low = "#3B9AB2", mid = "#EEEEEE", high = "#F21A00", 
                         midpoint = 0, limits = limits) +
    labs(title = titolo, x = NULL, y = NULL, fill = "Valore") +
    theme_minimal() +
    scale_y_reverse() +
    coord_fixed() +
    theme(plot.title = element_text(hjust = 0.5, size = 16))
}

# 4. Generazione dei grafici con bquote (resa perfetta di Sigma e hat)
p1 <- plot_matrix(Sigma, bquote(Sigma), common_limits)
p2 <- plot_matrix(Sigma_map, bquote(hat(Sigma)), common_limits)

# 5. Unione con patchwork: 'guides = "collect"' unifica la legenda
(p1 | p2) + 
  plot_layout(guides = "collect") +
  plot_annotation(title = "",
                  theme = theme(plot.title = element_text(size = 18, face = "bold", hjust = 0.5)))
```


### Multivariate outcome temporal forecasts
```{r}
Y_forc <- out$forecast$Y_pred
```

```{r, echo=F, fig.dim = c(7.25, 3)}
oldpar <- par(no.readonly = TRUE)
# Ridotto oma da 8 a 5 per avvicinare la legenda ai plot
par(mfrow = c(1, 2), mar = c(3, 3, 2, 1), oma = c(0, 0, 0, 5))

site_idx <- 1
t_obs <- tmax
t_total <- tmax + tnew 

for (v in 1:2) {
  pred_samps <- Y_forc[site_idx, v, , ] 
  quants <- t(apply(pred_samps, 2, quantile, c(0.025, 0.5, 0.975)))
  
  y_true_path <- c(Y[site_idx, v, ], Yfuture[site_idx, v, ])
  x_axis <- 1:t_total
  
  plot(x_axis, quants[,2], type = "n", 
       ylim = range(quants, y_true_path),
       xlab = "", ylab = "", 
       main = bquote(hat(Y)[.(v)] ~ " Forecast"), cex.main = 0.9)
  
  polygon(c(x_axis, rev(x_axis)), c(quants[,1], rev(quants[,3])), 
          col = rgb(0, 0, 1, 0.15), border = NA)
  
  lines(x_axis, quants[,1], col = 4, lty = 2, lwd = 0.7)
  lines(x_axis, quants[,3], col = 4, lty = 2, lwd = 0.7)
  lines(x_axis, quants[,2], col = 4, lwd = 1.5) 
  lines(x_axis, y_true_path, col = 2, lwd = 1.2)
  
  abline(v = t_obs, lty = 3, col = "grey50") 
}

par(xpd = NA)

# x = 0.88 invece di 0.92 per "attaccare" la legenda ai grafici
legend(x = grconvertX(0.88, from = "ndc"), 
       y = grconvertY(0.5, from = "ndc"),  
       legend = c("True", "MAP", "95% CI"),
       col = c(2, 4, rgb(0, 0, 1, 0.15)), 
       lty = c(1, 1, 1), 
       lwd = c(1.2, 1.5, NA),
       pch = c(NA, NA, 15),
       pt.cex = c(0, 0, 1.5),
       bty = "n", 
       xjust = 0,    # Allineamento a sinistra del punto X per guadagnare spazio
       yjust = 0.5, 
       cex = 0.7)    # Testo leggermente più piccolo per non rubare spazio
par(oldpar)
```



### Unobserved spacetime-points interpolation
Spatial interpolation for unobserved locations at future time points, here time point `r tmax+tnew`
```{r}
Y_pred <- out$spatial[[1]]$Y
```


```{r, echo=F, fig.dim = c(7.25, 6)}
plot_rows <- list()
grid_res <- 100

for (v in 1:2) {
  z_true <- Ytilde[, v, tmax + tnew]
  z_pred <- apply(Y_pred[, v, ], 1, mean)
  
  surf_t <- mba.surf(cbind(crdtilde, z_true), grid_res, grid_res, extend=TRUE)$xyz.est
  surf_p <- mba.surf(cbind(crdtilde, z_pred), grid_res, grid_res, extend=TRUE)$xyz.est
  
  df_t <- data.frame(expand.grid(x=surf_t$x, y=surf_t$y), z=as.vector(surf_t$z))
  df_p <- data.frame(expand.grid(x=surf_p$x, y=surf_p$y), z=as.vector(surf_p$z))
  lims <- range(c(df_t$z, df_p$z), na.rm=TRUE)
  
  make_spat_plot <- function(df, title) {
    ggplot(df, aes(x, y, fill = z)) + 
      geom_raster(interpolate = TRUE) +
      scale_fill_distiller(palette = "RdBu", limits = lims, name = NULL) +
      # Rimuove lo spazio tra dati e assi
      scale_x_continuous(expand = c(0, 0)) + 
      scale_y_continuous(expand = c(0, 0)) +
      labs(title = title, x = "Easting", y = "Northing") +
      theme_minimal() +
      theme(panel.grid = element_blank(),
            panel.border = element_rect(color = "black", fill = NA, size = 1)) 
  }
  
  p1 <- make_spat_plot(df_t, bquote(Y[list(t+.(tnew),.(v))] ~ "(True)"))
  p2 <- make_spat_plot(df_p, bquote(hat(Y)[list(t+.(tnew),.(v))] ~ "(MAP)"))
  
  plot_rows[[v]] <- (p1 + p2) + plot_layout(guides = "collect")
}

wrap_plots(plot_rows, ncol = 1)
```


------
