Breakpoint Selection with Spike-and-Slab Priors

When to use spike-and-slab

When fitting multi-breakpoint models, a central question is: how many change-points does the data actually support? smoothbp_ss() addresses this with a Kuo & Mallick (1998) spike-and-slab prior on each slope-change coefficient \(\delta_k\).

Each \(\delta_k\) is paired with a binary inclusion indicator \(\gamma_k\):

The posterior mean of each \(\gamma_k\) is the posterior inclusion probability (PIP) — the Bayesian probability that change-point \(k\) is real. A PIP near 1 means strong evidence for that breakpoint; a PIP near 0 means the data are consistent with no structural break there.

Tip: If you already know where a breakpoint occurred (e.g., a policy change or an intervention), you don’t need to estimate its location. Use the fixed() helper to test the intervention effect — see vignette("intervention-analysis").


Example 1: Identifying the number of real breakpoints

Simulate data with one true breakpoint

library(smoothbp)
library(posterior)
library(ggplot2)
library(dplyr)
library(tidyr)

set.seed(42)
n  <- 200
tau <- seq(0, 10, length.out = n)

# True model: one breakpoint at omega = 5, delta = -1.0
om_true    <- 5
rho_true   <- 4
b0_true    <- 2
b1_true    <- 0.5
delta_true <- -1.0

di  <- tau - om_true
si  <- plogis(di * rho_true)
mu  <- b0_true + b1_true * di + delta_true * di * si
y   <- mu + rnorm(n, sd = 0.2)
dat <- data.frame(y = y, tau = tau)

Fit a 3-breakpoint SS model

We let the model have 3 candidate breakpoints but expect only one to receive a high PIP:

fit_ss <- smoothbp_ss(
  formula = y ~ tau,
  b0      = ~ 1,
  b1      = ~ 1,
  deltas  = list(~ 1, ~ 1, ~ 1),
  omega   = list(~ 1, ~ 1, ~ 1),
  rho     = list(~ 1, ~ 1, ~ 1),
  data    = dat,
  spike   = prior_spike_slab(pi = 0.1, learn_pi = TRUE),
  priors  = smoothbp_priors(
    omega = space_omega_priors(K = 3, tau_min = 0, tau_max = 10)
  ),
  chains = 2, iter = 2000, warmup = 1000, seed = 42
)

Inspect posterior inclusion probabilities

pip(fit_ss)
#>  gamma_b1_(Intercept) gamma_delta1_(Intercept) gamma_delta2_(Intercept) gamma_delta3_(Intercept)
#>                  1.00                     0.06                     0.98                     0.04

# Full posterior summary (just the gammas)
summarise_draws(fit_ss$draws) |>
  filter(grepl("^gamma_delta", variable))

The second candidate breakpoint has a PIP near 1, matching the data-generating truth (omega = 5 maps to the second candidate position). The other two breakpoints are effectively excluded.


Example 2: Regularization prevents overfitting

We demonstrate that fitting a 3-BP model to 1-BP data does not overfit when using spike-and-slab priors.

Visualise posterior inclusion probabilities

# The plot method automatically computes and plots the PIPs along with their 95% HDI
plot(pip(fit_ss))

Example 3: Visualising model fit

The spike-and-slab model allows us to perform model selection and estimation simultaneously. We can overlay the posterior mean trajectory and its 95% credible interval on the true data-generating function:

# Get posterior predictions (mean and intervals)
pred <- fitted(fit_ss)

plot_df <- dat %>%
  mutate(
    mu_true = mu,  # 'mu' is from the simulation step
    mu_fit  = pred$fitted_mean,
    lo      = pred$fitted_Q2.5,
    hi      = pred$fitted_Q97.5
  )

ggplot(plot_df, aes(x = tau)) +
  geom_point(aes(y = y), alpha = 0.3, size = 1) +
  geom_ribbon(aes(ymin = lo, ymax = hi), fill = "#2c7bb6", alpha = 0.2) +
  geom_line(aes(y = mu_true, colour = "Truth"), linewidth = 1) +
  geom_line(aes(y = mu_fit,  colour = "smoothbp_ss"), linewidth = 0.8, linetype = "dashed") +
  scale_colour_manual(values = c("Truth" = "black", "smoothbp_ss" = "#d7191c")) +
  labs(
    title = "Model fit: Truth vs Posterior Predictions",
    subtitle = "The SS model correctly ignores the 2 redundant breakpoints and recovers the trajectory.",
    x = "Time (tau)", y = "Response (y)", colour = NULL
  ) +
  theme_minimal()

Example 4: Learning the inclusion probability

When you have many candidate breakpoints, setting a fixed prior \(\pi\) can be influential. Setting learn_pi = TRUE places a Beta hyperprior on a shared \(\pi\) and lets the data inform the overall sparsity level.

fit_lpi <- smoothbp_ss(
  formula = y ~ tau,
  b0      = ~ 1,
  b1      = ~ 1,
  deltas  = rep(list(~ 1), 5),   # 5 candidate breakpoints
  omega   = rep(list(~ 1), 5),
  rho     = rep(list(~ 1), 5),
  data    = dat,
  spike   = prior_spike_slab(
    learn_pi = TRUE,
    a        = 1,   # Beta(1, 4) prior on pi: mean = 0.2
    b        = 4
  ),
  chains = 2, iter = 3000, warmup = 1500, seed = 42
)

# Posterior for pi (sparsity level)
pi_draws <- as.numeric(as_draws_matrix(fit_lpi$draws)[, "pi"])
quantile(pi_draws, c(0.025, 0.5, 0.975))
#>  2.5%  50%  97.5%
#>  0.04 0.20   0.47
# Contracted toward sparsity: only ~1/5 breakpoints are real

Practical guidance

Choosing the slab width

The slab standard deviation controls the range of plausible non-zero slope changes. Match the slab width to your expected effect scale:

Expected slope change Suggested slab
Small (< 0.5) prior_normal(0, 0.5)
Moderate (0.5–2) prior_normal(0, 1)
Large / uncertain prior_normal(0, 2)

Interpreting PIPs

PIP Interpretation
> 0.95 Strong evidence for that breakpoint
0.75–0.95 Moderate evidence
0.25–0.75 Inconclusive
< 0.25 Little evidence — breakpoint not needed

For formal model selection, the median probability model includes all breakpoints with PIP > 0.5.

Diagnosing the sampler

The gamma indicators should switch between 0 and 1 frequently during sampling. Check with:

# Gamma trace plots — look for healthy switching
trace_plot(fit_ss, pars = grep("^gamma_delta", posterior::variables(fit_ss$draws), value = TRUE))

If a gamma is stuck at 1 across all iterations, consider: - Widening the slab (the prior may be too narrow to allow exclusion). - Checking the prior on omega (a badly-placed omega prior can pin the breakpoint in an implausible region).

If a gamma is stuck at 0, the breakpoint is genuinely not supported by the data — this is the desired behaviour.


Comparing smoothbp_ss against smoothbp with LOO

Spike-and-slab and LOO are complementary model-selection tools:

# Fit explicit 0-BP and 1-BP models for context
fit0 <- smoothbp(y ~ tau, deltas = list(), omega = list(), rho = list(),
                 data = dat, chains = 2, iter = 2000, warmup = 1000)
fit1 <- smoothbp(y ~ tau, deltas = list(~ 1), omega = list(~ 1), rho = list(~ 1),
                 data = dat, chains = 2, iter = 2000, warmup = 1000)

# LOO comparison: fixed-dimension vs spike-and-slab
loo::loo_compare(loo(fit1), loo(fit_ss))
#>       elpd_diff se_diff
#> fit1    0.0       0.0
#> fit_ss -0.2       0.4   # Both models perform identically

# PIP from the SS model agrees:
pip(fit_ss)["gamma_delta2_(Intercept)",]
#> 0.98

Both approaches correctly identify that one breakpoint is present.


References

mirror server hosted at Truenetwork, Russian Federation.