Skip to main content

quantwave_core/regimes/
pelt.rs

1//! Changepoint Detection (Killick et al. 2012)
2//!
3//! Source: Killick, R., Fearnhead, P., & Eckley, I. A. (2012).
4//! "Optimal Detection of Changepoints with a Linear Computational Cost."
5//! Journal of the American Statistical Association, 107(500), 1590-1598.
6//!
7//! Implementation of the Pruned Exact Linear Time (PELT) algorithm for exact segmentation.
8//! PELT identifies change points by minimizing a cost function over all possible partitions.
9
10use serde::{Deserialize, Serialize};
11
12/// A PELT changepoint detector.
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct PELT {
15    penalty: f64,
16    min_dist: usize,
17}
18
19impl PELT {
20    /// Creates a new PELT detector.
21    ///
22    /// # Arguments
23    /// * `penalty` - The penalty (beta) for adding a new changepoint (e.g., ln(n)).
24    /// * `min_dist` - Minimum distance between changepoints.
25    pub fn new(penalty: f64, min_dist: usize) -> Self {
26        Self { penalty, min_dist }
27    }
28
29    /// Normal log-likelihood cost function for change in mean.
30    /// C(y_s:t) = (t-s) * var(y_s:t)
31    fn cost(&self, data: &[f64], start: usize, end: usize) -> f64 {
32        if end <= start {
33            return 0.0;
34        }
35        let n = (end - start) as f64;
36        let mut sum = 0.0;
37        let mut sum_sq = 0.0;
38        for i in start..end {
39            sum += data[i];
40            sum_sq += data[i] * data[i];
41        }
42        let mean = sum / n;
43        let var = (sum_sq / n) - (mean * mean);
44        n * var.max(0.0)
45    }
46
47    /// Detect changepoints in a batch of data.
48    /// Returns indices of changepoints.
49    pub fn detect(&self, data: &[f64]) -> Vec<usize> {
50        let n = data.len();
51        if n < self.min_dist * 2 {
52            return vec![];
53        }
54
55        let mut f = vec![0.0; n + 1];
56        let mut cp = vec![0; n + 1];
57        let mut r = vec![0]; // Potential last changepoints
58
59        f[0] = -self.penalty;
60
61        for t in 1..=n {
62            let mut min_val = f64::MAX;
63            let mut best_tau = 0;
64
65            for &tau in &r {
66                if t - tau < self.min_dist {
67                    continue;
68                }
69                let val = f[tau] + self.cost(data, tau, t) + self.penalty;
70                if val < min_val {
71                    min_val = val;
72                    best_tau = tau;
73                }
74            }
75
76            f[t] = min_val;
77            cp[t] = best_tau;
78
79            // Pruning step
80            let mut next_r = vec![0];
81            for &tau in &r {
82                if f[tau] + self.cost(data, tau, t) <= f[t] {
83                    next_r.push(tau);
84                }
85            }
86            next_r.push(t);
87            r = next_r;
88        }
89
90        // Backtrack to find changepoints
91        let mut changepoints = Vec::new();
92        let mut curr = cp[n];
93        while curr > 0 {
94            changepoints.push(curr);
95            curr = cp[curr];
96        }
97        changepoints.sort();
98        changepoints
99    }
100}