Detectors¶
The online detector (Adams & MacKay 2007), the offline detector (Fearnhead
2006), and the helpers that turn the run-length posterior R into
changepoints. All of them can be imported from the top-level package.
Core Bayesian changepoint detection algorithms.
This module implements both online and offline Bayesian changepoint detection algorithms using PyTorch for efficient computation and GPU acceleration.
offline_changepoint_detection(data, prior_function, likelihood_model, truncate=float('-inf'), device=None)
¶
Offline Bayesian changepoint detection using dynamic programming.
Computes the exact posterior distribution over changepoint locations using the algorithm described in Fearnhead (2006).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
Tensor
|
Time series data of shape [T] or [T, D] where T is time and D is dimensions. |
required |
prior_function
|
callable
|
Log prior probability mass of a segment length: |
required |
likelihood_model
|
BaseLikelihood
|
Likelihood model for computing segment probabilities. |
required |
truncate
|
float
|
Deprecated; default |
float('-inf')
|
device
|
str, torch.device, or None
|
Device to place tensors on; defaults to the likelihood's device (the CPU unless the likelihood was built elsewhere). |
None
|
Returns:
| Name | Type | Description |
|---|---|---|
Q |
Tensor
|
Log evidence for data[t:] for each time t. Shape: [T]. |
P |
Tensor
|
Log likelihood of segment [t, s] with no changepoints. Shape: [T, T]. |
Pcp |
Tensor
|
Log probability of j-th changepoint at time t. Shape: [T-1, T-1].
Row |
Examples:
>>> import torch
>>> from functools import partial
>>> from bayesian_changepoint_detection import (
... offline_changepoint_detection, const_prior, StudentT
... )
>>>
>>> data = torch.randn(100)
>>> prior_func = partial(const_prior, p=0.01)
>>> likelihood = StudentT()
>>> Q, P, Pcp = offline_changepoint_detection(data, prior_func, likelihood)
>>>
>>> # Get changepoint probabilities
>>> changepoint_probs = torch.exp(Pcp).sum(0)
>>> detected_changepoints = torch.where(changepoint_probs > 0.5)[0]
Notes
The backward recursion for Q and P takes O(T^2) time and memory.
The changepoint table Pcp takes O(J T^2) time, where J is the
number of rows computed: every row j holds the probability that
there are more than j changepoints, which can only decrease with
j, and the rows after it drops below exp(-1000) are skipped. J
is about the largest plausible number of changepoints plus a margin (for
three clear changes, about 190 rows whatever T), and at most T - 1.
Model (Fearnhead 2006, section 2): segment lengths are i.i.d. with mass
function g, except the last segment, whose length is only known to be
at least what is observed, P(length >= l) = 1 - G(l - 1) with
G(l) = sum_{i <= l} g(i). Segments are independent given the
changepoints. With P[t, s] the log marginal likelihood of
data[t:s+1], the backward recursion (eq. 2 of the paper, 0-indexed) is
Q[t] = sum_{s=t}^{T-2} P[t, s] Q[s+1] g(s+1-t) + P[t, T-1] (1 - G(T-1-t))
and the changepoint posteriors are Pcp[0, t] = P[0, t] Q[t+1] g(t+1) / Q[0]
for the first changepoint (first segment data[0:t+1] has length
t + 1) and, for the j-th, a sum over the previous changepoint s
of Pcp[j-1, s] P[s+1, t] Q[t+1] g(t-s) / Q[s+1]. Versions up to 1.0.x
evaluated g at length minus one in the first row, paired g with
the wrong segment length in the later rows, and included a "length 0"
term in G; none of that is visible with const_prior, all of it is
with the geometric and negative binomial priors.
References
Fearnhead, P. (2006). Exact and efficient Bayesian inference for multiple changepoint problems. Statistics and Computing, 16(2), 203-213.
online_changepoint_detection(data, hazard_function, likelihood_model, device=None)
¶
Online Bayesian changepoint detection with run length filtering.
Processes data sequentially, maintaining a posterior distribution over run lengths (time since last changepoint) as described in Adams & MacKay (2007).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
Tensor
|
Time series data of shape [T] or [T, D] where T is time and D is dimensions. |
required |
hazard_function
|
callable
|
Function that takes run length tensor and returns hazard probabilities. Should accept torch.Tensor of run lengths and return torch.Tensor of same shape. |
required |
likelihood_model
|
BaseLikelihood
|
Online likelihood model that maintains sufficient statistics. |
required |
device
|
str, torch.device, or None
|
Device to place tensors on. |
None
|
Returns:
| Name | Type | Description |
|---|---|---|
R |
Tensor
|
Run length posterior. |
map_run_lengths |
Tensor
|
|
Examples:
>>> import torch
>>> from functools import partial
>>> from bayesian_changepoint_detection import (
... online_changepoint_detection, constant_hazard, StudentT,
... get_map_changepoints, changepoint_probabilities,
... )
>>>
>>> _ = torch.manual_seed(0)
>>> data = torch.cat([torch.randn(80), torch.randn(80) + 5])
>>> hazard_func = partial(constant_hazard, 100) # Expected run length = 100
>>> likelihood = StudentT(alpha=0.1, beta=0.01, kappa=1, mu=0, device="cpu")
>>> R, map_run_lengths = online_changepoint_detection(
... data, hazard_func, likelihood, device="cpu"
... )
>>> get_map_changepoints(R)
tensor([80])
>>> changepoint_probabilities(R, lag=10)[80] > 0.85
tensor(True)
Notes
This algorithm has O(T^2) time complexity but is naturally online and can process streaming data. The run length distribution is normalized at each step for numerical stability.
Why the second return value is a run length and not a probability.
Under the Adams & MacKay recursion the posterior probability of run
length 0 after each observation, R[0, t], is the hazard evaluated
under the previous run-length distribution; with a constant hazard it
is identically 1/lam and carries no information about the data. The
evidence for a changepoint at position tau accumulates in the
following columns as mass at run length k in column tau + k.
Versions 1.0.x returned the un-normalized R[0, t] under the name
changepoint_probs; that quantity could not detect changepoints. This
version restores the pre-1.0 return value (the MAP run length) and adds
changepoint_probabilities for the lagged probability.
References
Adams, R. P., & MacKay, D. J. (2007). Bayesian online changepoint detection. arXiv preprint arXiv:0710.3742.
changepoint_probabilities(R, lag=10)
¶
Probability that a new segment started at each position, judged lag
observations later.
result[tau] = R[lag, tau + lag]: the posterior probability, after
observing x_0 .. x_{tau+lag-1}, that the current run length is exactly
lag, which is the event "the segment containing the latest point began
at tau". This is the quantity the original notebook plotted as
R[Nw, Nw:] and the natural online detector with a fixed decision delay.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
R
|
Tensor
|
Run length posterior from |
required |
lag
|
int
|
Detection delay in observations (default 10). |
10
|
Returns:
| Type | Description |
|---|---|
Tensor
|
Shape |
Examples:
>>> probs = changepoint_probabilities(R, lag=10)
>>> detected = torch.where(probs > 0.5)[0]
get_map_changepoints(R, threshold=None, min_separation=0)
¶
Segment start indices implied by the MAP run-length path.
After t observations the MAP run length r_t = argmax R[:, t] says
the current segment began at data index t - r_t. Whenever that implied
start moves forward (the MAP run length drops), a changepoint is
reported at the new start. This is the classic BOCPD decision rule and is
what the pre-1.0 versions of this library exposed as maxes.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
R
|
Tensor
|
Run length posterior from |
required |
threshold
|
float
|
Deprecated and ignored. Earlier versions thresholded |
None
|
min_separation
|
int
|
When the posterior is split between two nearby starts the MAP path can flip between them and both get reported. Starts closer than this many observations to an earlier reported start are dropped (default 0: report every distinct start). |
0
|
Returns:
| Type | Description |
|---|---|
Tensor
|
Sorted data indices at which a new segment starts ( |
Examples:
>>> R, map_run_lengths = online_changepoint_detection(data, hazard_func, likelihood)
>>> get_map_changepoints(R)
compute_run_length_posterior(data, hazard_function, likelihood_model, device=None)
¶
Compute the full run length posterior distribution.
This is a convenience function that returns just the run length posterior from online changepoint detection.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
Tensor
|
Time series data. |
required |
hazard_function
|
callable
|
Hazard function for changepoint prior. |
required |
likelihood_model
|
BaseLikelihood
|
Online likelihood model. |
required |
device
|
str, torch.device, or None
|
Device to place tensors on. |
None
|
Returns:
| Type | Description |
|---|---|
Tensor
|
Run length posterior distribution R[r, t]. |
Examples:
>>> posterior = compute_run_length_posterior(data, hazard_func, likelihood)
>>> # Most likely run length at each time
>>> map_run_lengths = torch.argmax(posterior, dim=0)
viterbi_changepoints(data, hazard_function, likelihood_model, device=None)
¶
Most probable run-length path (Viterbi / max-product) under the BOCPD model.
online_changepoint_detection marginalizes over paths and returns the
posterior of the run length at each step. This function instead keeps,
for every run length, only the single best path leading to it, and
returns the jointly most probable sequence of run lengths, i.e. the MAP
segmentation of the series under the same model (hazard prior on
segment boundaries, conjugate predictive likelihood within a segment).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
Tensor
|
Time series of shape |
required |
hazard_function
|
callable
|
Maps a tensor of run lengths to changepoint probabilities. |
required |
likelihood_model
|
BaseLikelihood
|
Fresh online likelihood model (it is consumed by this call). |
required |
device
|
str, torch.device, or None
|
Device to place tensors on; defaults to the likelihood's device. |
None
|
Returns:
| Name | Type | Description |
|---|---|---|
run_lengths |
Tensor
|
Shape |
changepoints |
Tensor
|
Data indices at which a new segment starts on the best path
( |
Examples:
>>> import torch
>>> from functools import partial
>>> from bayesian_changepoint_detection import (
... viterbi_changepoints, constant_hazard, StudentT,
... )
>>> _ = torch.manual_seed(0)
>>> data = torch.cat([torch.randn(80), torch.randn(80) + 5])
>>> run_lengths, changepoints = viterbi_changepoints(
... data, partial(constant_hazard, 100), StudentT(0.1, 0.01, 1, 0, device="cpu"),
... device="cpu",
... )
>>> changepoints
tensor([80])
Notes
Same recursion as the forward pass with max in place of sum:
V[r + 1, t + 1] = V[r, t] + log p(x_t | r) + log(1 - H(r))
V[0, t + 1] = max_r V[r, t] + log p(x_t | r) + log H(r)
in log space, vectorized over r at each step (O(T) per observation,
O(T^2) total, like the forward pass). Versions 1.0.x summed over r
in the second line, which is neither the forward pass nor Viterbi, and
looped over r in Python.