pub fn kde(values: &[f64], points: usize) -> Option<(Vec<f64>, Vec<f64>)> {
let finite: Vec<f64> = values.iter().copied().filter(|v| v.is_finite()).collect();
if finite.is_empty() || points < 2 {
return None;
}
let n = finite.len() as f64;
let mean = finite.iter().sum::<f64>() / n;
let variance = finite.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / n;
let sigma = variance.sqrt();
let mut sorted = finite.clone();
sorted.sort_by(f64::total_cmp);
let quantile = |p: f64| -> f64 {
let position = (sorted.len() - 1) as f64 * p;
let index = position.floor() as usize;
let fraction = position - index as f64;
if index + 1 < sorted.len() {
sorted[index] + fraction * (sorted[index + 1] - sorted[index])
} else {
sorted[index]
}
};
let iqr = quantile(0.75) - quantile(0.25);
let spread = if iqr > 0.0 {
sigma.min(iqr / 1.34)
} else {
sigma
};
let bandwidth = if spread > 0.0 {
0.9 * spread * n.powf(-0.2)
} else {
1.0
};
let (low, high) = (sorted[0], sorted[sorted.len() - 1]);
let start = low - 3.0 * bandwidth;
let end = high + 3.0 * bandwidth;
let step = (end - start) / (points - 1) as f64;
let mut binned = vec![0.0f64; points];
for &value in &finite {
let position = (value - start) / step;
let index = position.floor() as usize;
let fraction = position - position.floor();
if index + 1 < points {
binned[index] += 1.0 - fraction;
binned[index + 1] += fraction;
} else {
binned[points - 1] += 1.0;
}
}
let radius = ((3.0 * bandwidth / step).ceil() as usize).max(1);
let kernel: Vec<f64> = (0..=radius)
.map(|k| {
let distance = k as f64 * step / bandwidth;
(-0.5 * distance * distance).exp()
})
.collect();
let normalization = 1.0 / (n * bandwidth * (2.0 * std::f64::consts::PI).sqrt());
let densities: Vec<f64> = (0..points)
.map(|i| {
let mut sum = binned[i] * kernel[0];
for k in 1..=radius {
if i >= k {
sum += binned[i - k] * kernel[k];
}
if i + k < points {
sum += binned[i + k] * kernel[k];
}
}
sum * normalization
})
.collect();
let positions = (0..points).map(|i| start + i as f64 * step).collect();
Some((positions, densities))
}
#[cfg(test)]
#[path = "tests/kde_tests.rs"]
mod tests;