use super::types::{ChangepointConfig, SegmentStats};
#[must_use]
pub fn compute_segment_stats(values: &[f64]) -> SegmentStats {
if values.is_empty() {
return SegmentStats::default();
}
let count = values.len();
let mean: f64 = values.iter().sum::<f64>() / count as f64;
let variance: f64 = if count > 1 {
values.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / (count - 1) as f64
} else {
0.0
};
let std_dev = variance.sqrt();
let min = values.iter().copied().fold(f64::INFINITY, f64::min);
let max = values.iter().copied().fold(f64::NEG_INFINITY, f64::max);
SegmentStats {
count,
mean,
std_dev,
min,
max,
}
}
#[inline]
#[must_use]
pub fn normal_cdf(x: f64) -> f64 {
let a1 = 0.254_829_592;
let a2 = -0.284_496_736;
let a3 = 1.421_413_741;
let a4 = -1.453_152_027;
let a5 = 1.061_405_429;
let p = 0.327_591_1;
let z = x / 2_f64.sqrt();
let sign = if z < 0.0 { -1.0 } else { 1.0 };
let z_abs = z.abs();
let t = 1.0 / (1.0 + p * z_abs);
let erf = sign
* (1.0 - (((((a5 * t + a4) * t) + a3) * t + a2) * t + a1) * t * (-z_abs * z_abs).exp());
0.5 * (1.0 + erf)
}
#[must_use]
pub fn calculate_changepoint_probability(
config: &ChangepointConfig,
values: &[f64],
cp_index: usize,
detection_index: usize,
) -> f64 {
let base_prob = if detection_index == 0 {
config.min_probability.max(0.7)
} else {
config.min_probability
};
let window = 5;
let start = cp_index.saturating_sub(window);
let end = (cp_index + window).min(values.len().saturating_sub(1));
if start < end {
let segment = &values[start..end];
let mean: f64 = segment.iter().sum::<f64>() / segment.len() as f64;
let variance: f64 =
segment.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / segment.len() as f64;
let std_dev = variance.sqrt();
let variance_factor = (1.0 - (std_dev / mean.abs().max(0.001))).clamp(0.5, 1.0);
(base_prob * variance_factor).clamp(0.0, 1.0)
} else {
base_prob
}
}