Bayesian Changepoint Detection

José Mauricio Gómez Julián

2026-02-11

Introduction

Bayesian changepoint detection offers several advantages over frequentist methods:

  1. Probabilistic uncertainty: Full posterior distributions over changepoint locations
  2. Prior information: Incorporate domain knowledge
  3. Natural online updates: Sequential updating as new data arrives
  4. Existence probability: Probability that a changepoint exists at each location

BOCPD: Bayesian Online Changepoint Detection

BOCPD (Adams & MacKay, 2007) is the flagship Bayesian method:

library(RegimeChange)

# Generate example data
set.seed(123)
data <- c(rnorm(100, 0, 1), rnorm(100, 3, 1))

# Basic BOCPD
result <- detect_regimes(data, method = "bocpd")
print(result)
#> 
#> Regime Change Detection Results
#> ================================
#> 
#> Method: bocpd 
#> Change type: both 
#> Mode: offline 
#> 
#> Data: n = 200 observations
#> 
#> Changepoints detected: 0

Prior Specification

Normal-Gamma Prior

For unknown mean and variance (most common case):

# Define prior
prior <- normal_gamma(
  mu0 = 0,      # Prior mean for the mean
  kappa0 = 1,   # Prior precision for the mean
  alpha0 = 1,   # Shape for inverse-gamma on variance
  beta0 = 1     # Rate for inverse-gamma on variance
)

print(prior)
#> Regime Prior Specification
#> ==========================
#> Type: normal_gamma 
#> Description: Normal-Gamma prior for unknown mean and variance 
#> 
#> Parameters:
#>   mu0: 0
#>   kappa0: 1
#>   alpha0: 1
#>   beta0: 1

Using the prior:

result <- detect_regimes(data, method = "bocpd", prior = prior)

Normal Prior with Known Variance

When variance is known:

prior_known <- normal_known_var(
  mu0 = 0,         # Prior mean
  sigma0 = 1,      # Prior standard deviation for mean
  known_var = 1    # Known variance
)

result <- detect_regimes(data, method = "bocpd", prior = prior_known)

Poisson-Gamma Prior

For count data:

prior_poisson <- poisson_gamma(alpha0 = 1, beta0 = 1)

Hazard Prior

The hazard prior controls the expected frequency of changepoints:

Geometric Hazard

Constant hazard rate:

# Expected run length = 1/lambda = 100
hazard <- geometric_hazard(lambda = 0.01)
print(hazard)
#> Hazard Prior Specification
#> ==========================
#> Type: geometric 
#> Description: Geometric hazard (expected run length: 100.0) 
#> 
#> Parameters:
#>   lambda: 0.01
#>   Expected run length: 100.0

Constant Hazard

Fixed probability per time step:

hazard_const <- constant_hazard(lambda = 0.05)

Negative Binomial Hazard

For overdispersed run lengths:

hazard_nb <- negbin_hazard(r = 5, p = 0.1)

Posterior Analysis

Probability of Change at Each Point

result <- detect_regimes(data, method = "bocpd")

# Access changepoint probabilities
prob_change <- result$prob_change

# Plot posterior probability
plot(prob_change, type = "l", 
     main = "Posterior Probability of Changepoint",
     xlab = "Time", ylab = "P(changepoint)")
abline(v = 100, col = "red", lty = 2)

Posterior Visualization

plot(result, type = "posterior")

Run Length Distribution

plot(result, type = "runlength")

Shiryaev-Roberts Procedure

An alternative Bayesian approach optimal for minimizing detection delay:

result_sr <- detect_regimes(data, method = "shiryaev",
                            mu0 = 0,      # Pre-change mean
                            mu1 = 3,      # Post-change mean
                            sigma = 1,    # Known standard deviation
                            threshold = 100)
print(result_sr)
#> 
#> Regime Change Detection Results
#> ================================
#> 
#> Method: shiryaev 
#> Change type: both 
#> Mode: offline 
#> 
#> Data: n = 200 observations
#> 
#> Changepoints detected: 5 
#> Locations: 107, 112, 134, 142, 200 
#> 
#> Segments:
#>   Segment 1: [1, 107] (n=107) | mean=0.254, sd=1.085
#>   Segment 2: [108, 112] (n=5) | mean=2.781, sd=1.029
#>   Segment 3: [113, 134] (n=22) | mean=2.870, sd=0.796
#>   Segment 4: [135, 142] (n=8) | mean=2.908, sd=1.434
#>   Segment 5: [143, 200] (n=58) | mean=2.944, sd=1.014
#>   Segment 6: [201, 200] (n=0) | mean=NA, sd=NA

The Shiryaev-Roberts statistic:

plot(result_sr, type = "diagnostic")
#> Warning: Removed 1 row containing missing values or values outside the scale range
#> (`geom_hline()`).

Uncertainty Quantification

Credible Intervals

BOCPD provides natural credible intervals from the posterior:

# Highest posterior density interval
if (length(result$changepoints) > 0) {
  cat("Changepoint estimate:", result$changepoints[1], "\n")
  
  # Find 95% credible interval
  prob <- result$prob_change
  cumprob <- cumsum(prob) / sum(prob)
  lower <- which(cumprob >= 0.025)[1]
  upper <- which(cumprob >= 0.975)[1]
  cat("95% Credible interval: [", lower, ",", upper, "]\n")
}

Existence Probability

Probability that at least one changepoint exists:

existence_prob <- max(result$prob_change)
cat("Maximum changepoint probability:", round(existence_prob, 3), "\n")
#> Maximum changepoint probability: 0.02

Online Mode

BOCPD is naturally suited for online detection:

# Create online detector
detector <- regime_detector(
  method = "bocpd",
  prior = normal_gamma(),
  hazard = geometric_hazard(0.01),
  threshold = 0.5
)

# Simulate online processing
set.seed(123)
stream <- c(rnorm(100, 0, 1), rnorm(50, 3, 1))

alarm_time <- NULL
for (i in seq_along(stream)) {
  detector <- update(detector, stream[i])
  
  if (isTRUE(detector$last_result$alarm) && is.null(alarm_time)) {
    alarm_time <- i
    cat("Alarm at observation:", i, "\n")
    cat("Probability of change:", round(detector$last_result$prob_change, 3), "\n")
    break
  }
}

Multivariate BOCPD

For multivariate time series:

# Generate bivariate data
set.seed(42)
n <- 200
cp <- 100
data_mv <- rbind(
  matrix(rnorm(cp * 2, 0), ncol = 2),
  matrix(rnorm((n - cp) * 2, 2), ncol = 2)
)

# Normal-Wishart prior for multivariate data
prior_mv <- normal_wishart(
  mu0 = c(0, 0),
  kappa0 = 1,
  nu0 = 4,
  Psi0 = diag(2)
)

result_mv <- detect_regimes(data_mv, method = "bocpd", prior = prior_mv)
print(result_mv)
#> 
#> Regime Change Detection Results
#> ================================
#> 
#> Method: bocpd 
#> Change type: both 
#> Mode: offline 
#> 
#> Data: n = 200 observations
#> 
#> Changepoints detected: 0

Comparison: BOCPD vs Frequentist

# Compare BOCPD with PELT
comparison <- compare_methods(
  data = data,
  methods = c("bocpd", "pelt"),
  true_changepoints = 100
)
print(comparison)
#> 
#> Changepoint Detection Method Comparison
#> ========================================
#> 
#>  method n_changepoints   time_sec f1 hausdorff adj_rand
#>   bocpd              0 0.10733008  0       Inf        0
#>    pelt              1 0.02529216  1         0        1
#> 
#> True changepoints: 100 
#> 
#> Detected changepoints by method:
#>   bocpd: (none)
#>   pelt: 100

Advantages and Limitations

Advantages

Limitations

Best Practices

  1. Choose appropriate priors: Use domain knowledge when available
  2. Calibrate hazard rate: Set based on expected changepoint frequency
  3. Check sensitivity: Try different priors to assess robustness
  4. Use truncation: For long series, truncate run length for efficiency
  5. Visualize posterior: Always examine the full posterior distribution

References

Adams, R. P., & MacKay, D. J. C. (2007). Bayesian online changepoint detection. arXiv preprint arXiv:0710.3742.

Tartakovsky, A. G., & Moustakides, G. V. (2010). State-of-the-art in Bayesian changepoint detection. Sequential Analysis, 29(2), 125-145.