use crate::statistics::mean;
pub struct BayesianCPD {
hazard_rate: f64,
}
impl BayesianCPD {
pub fn new(hazard_rate: f64) -> Self {
Self { hazard_rate }
}
pub fn update(&mut self, value: f64, historical: &[f64]) -> f64 {
if historical.is_empty() {
return 0.0;
}
let likelihood = self.student_t_likelihood(value, historical);
let unlikelihood = 1.0 - likelihood.min(1.0);
let prior_weight = self.hazard_rate;
let evidence_weight = unlikelihood;
let change_prob =
(evidence_weight * 0.7) + (prior_weight * 0.3) + (evidence_weight * prior_weight * 0.5);
change_prob.min(1.0)
}
fn student_t_likelihood(&self, value: f64, historical: &[f64]) -> f64 {
if historical.is_empty() {
return 0.5;
}
let n = historical.len() as f64;
let hist_mean = mean(historical);
let variance = if n > 1.0 {
historical
.iter()
.map(|&x| (x - hist_mean).powi(2))
.sum::<f64>()
/ n
} else {
1.0 };
if variance < 1e-10 {
let stddev = variance.sqrt().max(1e-5);
let z = ((value - hist_mean) / stddev).abs();
return (-0.5 * z * z).exp();
}
let df = (n - 1.0).max(1.0);
let t = (value - hist_mean) / variance.sqrt();
let t_squared = t * t;
(1.0 + t_squared / df).powf(-(df + 1.0) / 2.0)
}
}
pub fn bayesian_change_point_probability(
new_value: f64,
historical: &[f64],
hazard_rate: f64,
) -> f64 {
let mut cpd = BayesianCPD::new(hazard_rate);
cpd.update(new_value, historical)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_stable_data_low_change_probability() {
let historical = vec![1.0, 1.01, 0.99, 1.0, 1.02, 0.98, 1.0, 1.01];
let new_value = 1.0;
let prob = bayesian_change_point_probability(new_value, &historical, 0.1);
assert!(prob < 0.5, "Expected low change probability, got {}", prob);
}
#[test]
fn test_clear_change_point() {
let historical = vec![1.0, 1.01, 0.99, 1.0, 1.02, 0.98, 1.0, 1.01];
let new_value = 2.0;
let prob = bayesian_change_point_probability(new_value, &historical, 0.1);
assert!(prob > 0.3, "Expected high change probability, got {}", prob);
}
#[test]
fn test_empty_historical() {
let historical: Vec<f64> = vec![];
let new_value = 1.0;
let prob = bayesian_change_point_probability(new_value, &historical, 0.1);
assert_eq!(prob, 0.0);
}
#[test]
fn test_gradual_drift() {
let historical = vec![1.0, 1.05, 1.1, 1.15, 1.2, 1.25, 1.3];
let new_value = 1.35;
let prob_gradual = bayesian_change_point_probability(new_value, &historical, 0.1);
let historical_stable = vec![1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0];
let new_value_jump = 1.35;
let prob_sudden =
bayesian_change_point_probability(new_value_jump, &historical_stable, 0.1);
assert!(
prob_sudden > prob_gradual,
"Sudden jump ({}) should have higher change probability than gradual drift ({})",
prob_sudden,
prob_gradual
);
}
#[test]
fn test_higher_hazard_rate_increases_change_probability() {
let historical = vec![1.0, 1.0, 1.0, 1.0, 1.0];
let new_value = 1.2;
let prob_low_hazard = bayesian_change_point_probability(new_value, &historical, 0.05);
let prob_high_hazard = bayesian_change_point_probability(new_value, &historical, 0.3);
assert!(prob_high_hazard > prob_low_hazard,
"Higher hazard rate ({}) should produce higher change probability than lower hazard rate ({})",
prob_high_hazard, prob_low_hazard);
}
}