use crate::changepoint::detector::Cost;
use crate::changepoint::signal::Signal;
use crate::error::{ForecastError, Result};
#[derive(Debug, Default, Clone)]
pub struct CostL1 {
n: usize,
d: usize,
columns: Vec<Vec<f64>>,
}
impl CostL1 {
pub fn new() -> Self {
Self::default()
}
}
impl Cost for CostL1 {
fn fit(&mut self, signal: &Signal) -> Result<()> {
self.n = signal.n();
self.d = signal.d();
self.columns = (0..self.d)
.map(|j| (0..self.n).map(|i| signal.row(i)[j]).collect())
.collect();
Ok(())
}
fn error(&self, start: usize, end: usize) -> Result<f64> {
if end <= start || end > self.n {
return Err(ForecastError::InvalidParameter(format!(
"CostL1: invalid segment [{}, {}) for n = {}",
start, end, self.n
)));
}
let mut total = 0.0;
let mut buf: Vec<f64> = Vec::with_capacity(end - start);
for j in 0..self.d {
buf.clear();
buf.extend_from_slice(&self.columns[j][start..end]);
buf.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let m = buf.len();
let median = if m % 2 == 1 {
buf[m / 2]
} else {
0.5 * (buf[m / 2 - 1] + buf[m / 2])
};
for &x in &buf {
total += (x - median).abs();
}
}
Ok(total)
}
fn min_size(&self) -> usize {
1
}
fn name(&self) -> &str {
"l1"
}
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
#[test]
fn univariate_l1_uses_median() {
let values = vec![1.0, 3.0, 5.0, 7.0, 9.0];
let s = Signal::univariate(&values);
let mut c = CostL1::new();
c.fit(&s).unwrap();
assert_relative_eq!(c.error(0, 5).unwrap(), 12.0, epsilon = 1e-10);
}
#[test]
fn multivariate_sums_across_dims() {
let data = vec![1.0, 100.0, 2.0, 200.0, 3.0, 300.0];
let s = Signal::from_row_major(&data, 3, 2).unwrap();
let mut c = CostL1::new();
c.fit(&s).unwrap();
assert_relative_eq!(c.error(0, 3).unwrap(), 202.0, epsilon = 1e-10);
}
}