use super::{sorted_finite, StatsError};
use std::f64::consts::PI;
#[derive(Debug, Clone, PartialEq)]
pub struct KdeCurve {
pub xs: Vec<f64>,
pub density: Vec<f64>,
pub bandwidth: f64,
}
impl KdeCurve {
pub fn max_density(&self) -> f64 {
self.density.iter().copied().fold(0.0_f64, f64::max)
}
}
pub fn silverman_bandwidth(data: &[f64]) -> Result<f64, StatsError> {
let sorted = sorted_finite(data);
let n = sorted.len();
if n < 2 {
return Err(StatsError::InvalidBandwidth);
}
let nf = n as f64;
let mean = sorted.iter().sum::<f64>() / nf;
let var = sorted.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / (nf - 1.0);
let std = var.sqrt();
let q1 = super::percentile_sorted(&sorted, 0.25);
let q3 = super::percentile_sorted(&sorted, 0.75);
let iqr = q3 - q1;
let spread = if iqr > 0.0 { std.min(iqr / 1.34) } else { std };
let h = 0.9 * spread * nf.powf(-1.0 / 5.0);
if h.is_finite() && h > 0.0 {
Ok(h)
} else {
Err(StatsError::InvalidBandwidth)
}
}
fn gaussian_kernel(u: f64) -> f64 {
(-0.5 * u * u).exp() / (2.0 * PI).sqrt()
}
pub fn kde_curve(
data: &[f64],
bandwidth: Option<f64>,
n_points: usize,
cut: f64,
) -> Result<KdeCurve, StatsError> {
let sorted = sorted_finite(data);
if sorted.is_empty() {
return Err(StatsError::EmptyInput);
}
let h = match bandwidth {
Some(b) if b.is_finite() && b > 0.0 => b,
Some(_) => return Err(StatsError::InvalidBandwidth),
None => silverman_bandwidth(&sorted)?,
};
let n_points = n_points.max(2);
let lo = sorted[0] - cut * h;
let hi = sorted[sorted.len() - 1] + cut * h;
let step = (hi - lo) / (n_points as f64 - 1.0);
let nf = sorted.len() as f64;
let mut xs = Vec::with_capacity(n_points);
let mut density = Vec::with_capacity(n_points);
for i in 0..n_points {
let x = lo + step * i as f64;
let d = sorted
.iter()
.map(|&xi| gaussian_kernel((x - xi) / h))
.sum::<f64>()
/ (nf * h);
xs.push(x);
density.push(d);
}
Ok(KdeCurve {
xs,
density,
bandwidth: h,
})
}