---
title: "Bayesian Spatial Item Factor Analysis"
output: rmarkdown::html_vignette
bibliography: references.bib
vignette: >
  %\VignetteIndexEntry{Bayesian Spatial Item Factor Analysis}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r setup, include = FALSE}
knitr::opts_chunk$set(echo = TRUE, fig.align = "center", out.width = "100%")
knitr::opts_chunk$set(fig.width = 7, fig.height = 4)
knitr::opts_chunk$set(comment = "#>")
old_options <- options(width = 100)
```

In this vignette, we show how to use the **spifa** package to fit a spatial item
factor model, summarise and visualize the posterior samples, and predict and map
the latent factors at new locations.

## Introduction

We will focus on the spatial item factor model proposed in @chaconmontalvan2025. Let
$Y_{ij}$ be the binary response for item $j$ in individual $i$. The model can be defined
using an auxiliary variable $Z_{ij}$ such that:

$$
\begin{aligned}
  {Y}_{ij}  & =
  \begin{cases}
    1, & \text{if} ~ {Z}_{ij} > 0\\
    0, & \text{otherwise}
  \end{cases}\\
  {Z}_{ij} & = c_j + \boldsymbol{a}_j^\intercal\boldsymbol{\theta}_i + \epsilon_{ij},
  ~~ \epsilon_{ij} \sim {N}(0, 1),
\end{aligned}
$$

where the easiness parameters $c_j$ define how common is to endorse the item $j$, and the
discrimination parameters $\boldsymbol{a}_j$ define how important is item $j$ to
discriminate the latent factors/abilities $\boldsymbol{\theta}_i$.

The vector of latent abilities $\boldsymbol{\theta}_i$ is modelled in terms of
predictors $\boldsymbol{x}_i$, a vector of Gaussian processes $\boldsymbol{w}(s_i)$, and a
multivariate non-spatial term $\boldsymbol{v}_i$:
$$
\boldsymbol{\theta}_i = \boldsymbol{B} \boldsymbol{x}_i + \boldsymbol{T} \boldsymbol{w}(s_i) + \boldsymbol{v}_i,
$$
where $\boldsymbol{B}$ is the matrix of multivariate effects of the predictors,
$\boldsymbol{T}$ defines the relationship between the spatial processes and the latent
abilities, and $\boldsymbol{v}_i$ is allowed to have a correlations structure
$\boldsymbol{R}$.

This model is unidentifiable, so it is important to restrict the discrimination
parameters $\boldsymbol{a}_j$ and set informative priors for the spatial range of the
Gaussian processes $\boldsymbol{w}(s_i)$ before fitting it, to facilitate convergence
of the posterior sampling algorithm -- in practice, both are best informed by
exploratory analysis (e.g. a preliminary non-spatial item factor analysis for the
former, an empirical variogram for the latter), though we use them directly below
without walking through that derivation. The linear transformation $\boldsymbol{T}$,
by contrast, is not determined from exploratory analysis -- it is a structural choice
made by the user (usually diagonal, i.e. one independent Gaussian process per
factor), though alternative structures can be specified and compared.


## Ipixuna data

```{r}
library(spifa)
library(ggplot2)
library(sf)
```

The `ipixuna` data under analysis is included in the **spifa** package: a simulated
dataset of 10 `items` responses on 100 household locations, on the same area of study
as in @chaconmontalvan2025. It is a `sf` object containing the `items` responses as a
matrix, locations under the `geometry` column, and a predictor `wealth`.

```{r}
data(ipixuna)
ipixuna
```

The `items` can be easily obtained as an element of the data `ipixuna`:

```{r}
ipixuna$items |> head()
```

## Sample from the posterior

As mentioned before, an identifiable spifa model requires restricting the
discrimination parameters, along with adequate initial values and priors for the
spatial parameters (mainly the range). In practice, these are best obtained from a
preliminary analysis, as shown in `vignette("vg04-integrated-workflow")`. Here, we
instead assume the discrimination structure and some initial knowledge of the spatial
parameters are already available, and focus on defining, fitting, and diagnosing the
model.

First, we obtain the number of items (10) and define the number of factors (3) we will
use:

```{r}
nitems <- ncol(ipixuna$items)
nfactors <- 3
```

### Restrictions

We define the discrimination matrix (10x3): items 4 and 8 do not inform factor 1,
items 4, 5, 6, 7, 8, and 10 do not inform factor 2, and items 5 and 6 do not inform
factor 3.

```{r}
A <- matrix(1, nitems, nfactors)
A[c(4, 8), 1] <- 0
A[c(4, 5, 6, 7, 8, 10), 2] <- 0
A[c(5, 6), 3] <- 0
A
```

### Priors

In factor analysis, the sign of the factors is not identifiable, so we need to fix the
direction of each factor (which determines how we interpret it). A common way to do
this is to set a strong positive or negative prior on a few discrimination parameters.
The discrimination parameters are assumed to have a Normal prior, so `mean`/`sd` are
directly its mean and standard deviation. Here, we set the mean prior to 1 for items 5
and 6 on factor 1, and to -1 for items 4 and 8 on factor 3, each with a standard
deviation prior of 0.45:

```{r}
A_mean <- matrix(0, nitems, nfactors)
A_mean[c(5, 6), 1] <- 1
A_mean[c(4, 8), 3] <- -1
A_mean

A_sd <- matrix(1, nitems, nfactors)
A_sd[A_mean != 0] <- 0.45
A_sd
```

Similarly, we will define the prior for the range parameter of the Gaussian processes. By
default the `spifa()` function introduces 1 GP per factor, meaning that 3 range parameters
will be used in the model. Since the range must be positive, it is instead assumed to
have a Log-Normal prior: `mean`/`sd` are the mean and standard deviation of its
logarithm, not of the range itself. The range is in the same units as the coordinates
(metres here); `150` is a plausible order of magnitude for the spacing between
households in this study area. If we want to set the same prior for all three, we can
define a single mean and standard deviation that will be recycled across them:

```{r}
phi_mean <- 150
phi_sd <- 0.4
```

### Sampling

The function `spifa()` is used to sample from the posterior distribution of the model.
The main arguments are:

- `formula`: expression to define the response items and predictors to include in the
  latent abilities,
- `data`: a `sf` object containing the terms defined in `formula`,
- `nfactors`: the number of factors for the model,
- `niter`, `thin`, `burnin`: usual sampling arguments,
- `constraints`: a named list of constraints for certain parameters,
- `priors`: a named list of prior information including initial values.

In the following, we fit a spifa model with `3` latent factors, explained by `wealth`,
constraints for the *discrimination* parameters and priors for the *discrimination* and
*range* parameters. By default, it will introduce a GP for each latent factor.

```{r, eval = FALSE}
samples <- spifa(
  items ~ wealth, data = ipixuna, nfactors = nfactors,
  burnin = 10000, niter = 8000, thin = 2,
  constraints = list(discrimination = A),
  priors = list(
    discrimination = list(initial = A_mean, mean = A_mean, sd = A_sd),
    range = list(initial = phi_mean, mean = phi_mean, sd = phi_sd)
  )
)
samples
```

```{r, echo = FALSE}
samples <- readRDS(system.file("vigdata", "spifa.rds", package = "spifa"))
samples
```

The `print()` method shows metadata of the model such as *model type*, *formula*,
*dimensions*, MCMC dimensions including the number of saved *samples*. Additionally, it
shows the summary and diagnostics of the item model parameters (easiness and
discrimination), and the factor model parameters (effects, loading, range, and
correlation). Among the diagnostics, `ess_bulk` is the bulk effective sample size and
`rhat` is the potential scale reduction factor on split chains, which should be close
to 1 at convergence.

## Summarise and visualize samples

### Summary

While `print()` gives a quick overview, `summary()` computes the full set of posterior
statistics (mean, median, sd, mad, quantiles, `ess_bulk`, `ess_tail`, and `rhat`) for a
chosen parameter group, returned as a tidy tibble. Its main arguments are `select` (the
parameter group, e.g. `"c"`, `"A"`, `"phi"`), and `burnin`/`thin`, in case we want to
discard some initial samples or thin them further before summarising:

```{r}
summary(samples, select = "c")
```

### Plot

`plot()` gives a quick overview of a fitted model: traceplots and density plots, side
by side, for the `easiness` and `discrimination` parameters by default.

```{r, fig.height = 6}
plot(samples)
```

Use the `select` argument for a specific parameter group and `nshow` to define the
number of random parameters to show:

```{r}
plot(samples, select = "c", nshow = 5)
```

### Plotting functions

For more flexibility, **spifa** provides three individual plotting functions:

- `plot_trace()`,
- `plot_density()`,
- `plot_interval()`

They take as input the spifa `samples` with the `select` argument to define the parameter
group.

#### Trace plots

`plot_trace()` draws the values against iteration per parameter. By default, it plots
one panel per parameter (`facet = TRUE`), or all of them in a single panel when
`facet = FALSE`.

Traceplots of the residual correlation:

```{r}
plot_trace(samples, select = "Corr")
```

Traceplots of the range parameter of the GPs:

```{r}
plot_trace(samples, select = "phi", facet = FALSE)
```

#### Density plots

`plot_density()` draws the posterior density of each parameter in the selected group.
Similar to `plot_trace()`, you can control the `facet` argument.

Comparing the easiness parameters:

```{r, fig.height = 5}
plot_density(samples, select = "c")
```

Comparing the predictor effects:

```{r, fig.height = 5}
plot_density(samples, select = "B", facet = TRUE, facet_scales = "free_y")
```

#### Interval plots

`plot_interval()` draws the posterior mean/median and credible interval of
each parameter in the selected group.

Compare the discrimination parameters:

```{r, fig.height = 5}
plot_interval(samples, select = "A")
```

Compare the range parameters:

```{r, fig.height = 5}
plot_interval(samples, select = "phi", horizontal = TRUE)
```

## Predict the latent factors

**spifa** provides Bayesian prediction of the latent factors at (i) the observed locations
and (ii) new locations through the `predict()` function.

### Observed locations

By default `predict()` will simply return the samples of the latent factors at observed
locations as a `posterior::draws_array()` object:

```{r, eval = FALSE}
pred_samples <- predict(samples, burnin = 1000, thin = 5)
posterior::as_draws_df(pred_samples)
```

```{r, echo = FALSE}
pred_samples <- readRDS(system.file("vigdata", "spifa-pred-observed.rds", package = "spifa"))
attr(pred_samples, "newdata") <- st_geometry(ipixuna)
posterior::as_draws_df(pred_samples)
```

We can visualize a summary (`mean` by default) of these predictive samples using
`plot_predict()` and integrate it with `ggplot2` functionality:

```{r}
plot_predict(pred_samples) +
  scale_colour_distiller(palette = "RdBu")
```

### New locations

If prediction is desired for new location/profiles, then the argument `newdata` can be
provided, which is expected to be a `sf`/`sfc` object containing the new
location/profiles.

First, we create a grid where prediction will be performed. Then, we create a `newdata`
`sf` object using a predictor profile representing an individual with average
(standardized) wealth:

```{r}
bnd <- st_geometry(ipixuna) |>
  st_union() |>
  st_convex_hull() |>
  st_buffer(50)

newdata <- st_sf(wealth = 0, geometry = st_make_grid(bnd, n = c(40, 40))) |>
  st_filter(bnd)
newdata
```

We perform prediction for this `newdata` and thin the posterior samples to reduce
computational cost:

```{r, eval = FALSE}
pred_samples <- predict(samples, newdata = newdata, thin = 10)
```

```{r, echo = FALSE}
pred_grid <- readRDS(system.file("vigdata", "spifa-prediction.rds", package = "spifa"))
newdata <- pred_grid$grid
bnd <- pred_grid$bnd

plot_predict_vig <- function (grid, prefix, boundary = NULL) {
  grid |>
    tidyr::pivot_longer(starts_with(prefix), names_to = "factor", values_to = "value") |>
    dplyr::mutate(factor = sub(paste0(prefix, "(\\d)"), "theta[\\1]", factor)) |>
    ggplot() +
    geom_sf(aes(fill = value), colour = NA) +
    geom_sf(data = boundary, fill = NA, linewidth = 0.6, color = "gray50") +
    facet_wrap(~ factor, labeller = label_parsed) +
    theme_bw(base_size = 9) +
    theme(
      axis.title = element_blank(),
      panel.grid = element_line(linetype = 2, colour = "grey80", linewidth = 0.25),
      panel.background = element_blank(),
      panel.ontop = TRUE,
      legend.position = "bottom",
      legend.title = element_blank(),
      legend.key.height = unit(0.4, "cm"),
      strip.background = element_blank(),
      strip.text = element_text(size = rel(1))
    )
}
```

Now, we use the `plot_predict()` function to visualize the predictive `mean` by default.

```{r, eval = FALSE}
plot_predict(pred_samples, boundary = bnd) +
  scale_fill_distiller(palette = "RdBu") +
  labs(title = "Predictive mean")
```

```{r, echo = FALSE, fig.height = 4}
plot_predict_vig(newdata, "theta", bnd) +
  scale_fill_distiller(palette = "RdBu") +
  labs(title = "Predictive mean")
```

Other summaries can be mapped by simply providing a function in the `stat` argument:

```{r, eval = FALSE}
plot_predict(pred_samples, boundary = bnd, stat = sd) +
  scale_fill_viridis_c(option = "magma") +
  labs(title = "Predictive standard deviation")
```

```{r, echo = FALSE, fig.height = 4}
plot_predict_vig(newdata, "sd", bnd) +
  scale_fill_viridis_c(option = "magma") +
  labs(title = "Predictive standard deviation")
```

Custom functions can easily be provided, for example, we can plot the probability of
exceeding the value of 1 to identify hotspots:

```{r, eval = FALSE}
plot_predict(pred_samples, boundary = bnd, stat = function (v) mean(v > 1)) +
  scale_fill_viridis_c(limits = c(0, 1), direction = -1) +
  labs(title = expression(paste("Exceedance probability: ", P(theta[j] > 1))))
```

```{r, echo = FALSE, fig.height = 4}
plot_predict_vig(newdata, "exceedance", bnd) +
  scale_fill_viridis_c(limits = c(0, 1), direction = -1) +
  labs(title = expression(paste("Exceedance probability: ", P(theta[j] > 1))))
```

## References

```{r, include = FALSE}
options(old_options)
```
