use std::cmp::Ordering;
use std::collections::BinaryHeap;
use crate::changepoint::detector::{Cost, Detector, DetectorResult};
use crate::changepoint::signal::Signal;
use crate::error::{ForecastError, Result};
#[derive(Debug, Clone)]
pub struct BinsegDetector<C: Cost> {
cost: C,
min_size: usize,
jump: usize,
n: Option<usize>,
}
impl<C: Cost> BinsegDetector<C> {
pub fn new(cost: C) -> Self {
let min_size = cost.min_size().max(2);
Self {
cost,
min_size,
jump: 1,
n: None,
}
}
pub fn min_size(mut self, min_size: usize) -> Self {
self.min_size = min_size.max(self.cost.min_size()).max(1);
self
}
pub fn jump(mut self, jump: usize) -> Self {
self.jump = jump.max(1);
self
}
pub fn cost(&self) -> &C {
&self.cost
}
}
#[derive(Debug, Clone, Copy)]
struct Split {
gain: f64,
parent_start: usize,
parent_end: usize,
bkp: usize,
}
impl PartialEq for Split {
fn eq(&self, other: &Self) -> bool {
self.gain == other.gain
}
}
impl Eq for Split {}
impl PartialOrd for Split {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Split {
fn cmp(&self, other: &Self) -> Ordering {
self.gain
.partial_cmp(&other.gain)
.unwrap_or(Ordering::Equal)
.then(other.parent_start.cmp(&self.parent_start))
}
}
impl<C: Cost> BinsegDetector<C> {
fn best_split(&self, start: usize, end: usize) -> Result<Option<Split>> {
let min_size = self.min_size.max(self.cost.min_size());
if end < start + 2 * min_size {
return Ok(None);
}
let parent = self.cost.error(start, end)?;
let mut best_gain = f64::NEG_INFINITY;
let mut best_bkp = 0usize;
let mut b = start + min_size;
while b + min_size <= end {
let left = self.cost.error(start, b)?;
let right = self.cost.error(b, end)?;
let gain = parent - (left + right);
if gain > best_gain {
best_gain = gain;
best_bkp = b;
}
b = (b + self.jump).min(end - min_size);
if b == end - min_size {
let left = self.cost.error(start, b)?;
let right = self.cost.error(b, end)?;
let gain = parent - (left + right);
if gain > best_gain {
best_gain = gain;
best_bkp = b;
}
break;
}
}
if !best_gain.is_finite() {
return Ok(None);
}
Ok(Some(Split {
gain: best_gain,
parent_start: start,
parent_end: end,
bkp: best_bkp,
}))
}
}
impl<C: Cost> Detector for BinsegDetector<C> {
fn fit(&mut self, signal: &Signal) -> Result<()> {
self.cost.fit(signal)?;
self.n = Some(signal.n());
Ok(())
}
fn predict_n_bkps(&self, n_bkps: usize) -> Result<DetectorResult> {
let n = self.n.ok_or(ForecastError::FitRequired {
model: Some("BinsegDetector".into()),
})?;
if n_bkps == 0 || n == 0 {
return Ok(DetectorResult {
bkps: if n == 0 { vec![0] } else { vec![n] },
});
}
let mut heap = BinaryHeap::new();
if let Some(s) = self.best_split(0, n)? {
heap.push(s);
}
let mut bkps: Vec<usize> = Vec::with_capacity(n_bkps + 1);
bkps.push(n);
for _ in 0..n_bkps {
let s = match heap.pop() {
Some(s) if s.gain.is_finite() => s,
_ => break,
};
bkps.push(s.bkp);
if let Some(left) = self.best_split(s.parent_start, s.bkp)? {
heap.push(left);
}
if let Some(right) = self.best_split(s.bkp, s.parent_end)? {
heap.push(right);
}
}
bkps.sort_unstable();
bkps.dedup();
Ok(DetectorResult { bkps })
}
fn predict_pen(&self, pen: f64) -> Result<DetectorResult> {
let n = self.n.ok_or(ForecastError::FitRequired {
model: Some("BinsegDetector".into()),
})?;
if n == 0 {
return Ok(DetectorResult { bkps: vec![0] });
}
let mut heap = BinaryHeap::new();
if let Some(s) = self.best_split(0, n)? {
heap.push(s);
}
let mut bkps: Vec<usize> = vec![n];
while let Some(s) = heap.pop() {
if !s.gain.is_finite() || s.gain <= pen {
break;
}
bkps.push(s.bkp);
if let Some(left) = self.best_split(s.parent_start, s.bkp)? {
if left.gain > pen {
heap.push(left);
}
}
if let Some(right) = self.best_split(s.bkp, s.parent_end)? {
if right.gain > pen {
heap.push(right);
}
}
}
bkps.sort_unstable();
bkps.dedup();
Ok(DetectorResult { bkps })
}
fn name(&self) -> &str {
"Binseg"
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::changepoint::costs::CostL2;
fn level_shift(n_per: usize, levels: &[f64]) -> Vec<f64> {
let mut out = Vec::with_capacity(n_per * levels.len());
for &lvl in levels {
out.extend(std::iter::repeat_n(lvl, n_per));
}
out
}
#[test]
fn binseg_single_changepoint() {
let series = level_shift(30, &[0.0, 10.0]);
let s = Signal::univariate(&series);
let mut d = BinsegDetector::new(CostL2::new()).min_size(5);
d.fit(&s).unwrap();
let r = d.predict_n_bkps(1).unwrap();
assert_eq!(r.n_changepoints(), 1);
assert_eq!(r.bkps[0], 30);
}
#[test]
fn binseg_three_segments_via_n_bkps() {
let series = level_shift(25, &[0.0, 5.0, 0.0]);
let s = Signal::univariate(&series);
let mut d = BinsegDetector::new(CostL2::new()).min_size(5);
d.fit(&s).unwrap();
let r = d.predict_n_bkps(2).unwrap();
assert_eq!(r.n_changepoints(), 2);
assert_eq!(r.bkps, vec![25, 50, 75]);
}
#[test]
fn binseg_penalty_stops_early() {
let series = level_shift(20, &[0.0, 10.0]);
let s = Signal::univariate(&series);
let mut d = BinsegDetector::new(CostL2::new()).min_size(5);
d.fit(&s).unwrap();
let r = d.predict_pen(1e9).unwrap();
assert_eq!(r.n_changepoints(), 0);
}
}