use crate::{Error, Result};
const SQRT_2PI: f64 = 2.506_628_274_631_000_2;
#[derive(Debug, Clone, PartialEq)]
pub enum Bandwidth {
Silverman,
Fixed(Vec<f64>),
}
#[derive(Debug, Clone, PartialEq)]
pub struct KdeSampler {
samples: Vec<Vec<f64>>,
widths: Vec<f64>,
}
impl KdeSampler {
pub fn fit(samples: &[Vec<f64>], bandwidth: Bandwidth) -> Result<Self> {
if samples.is_empty() {
return Err(Error::EmptyPdf);
}
let dims = samples[0].len();
if dims == 0 {
return Err(Error::LengthMismatch {
expected: 1,
got: 0,
});
}
for (i, row) in samples.iter().enumerate() {
if row.len() != dims {
return Err(Error::LengthMismatch {
expected: dims,
got: row.len(),
});
}
if row.iter().any(|v| !v.is_finite()) {
return Err(Error::NonFiniteTally {
field: "kde sample",
index: i,
});
}
}
let n = samples.len() as f64;
let d = dims as f64;
let widths = match bandwidth {
Bandwidth::Fixed(w) => {
if w.len() != dims {
return Err(Error::LengthMismatch {
expected: dims,
got: w.len(),
});
}
if w.iter().any(|v| !v.is_finite() || *v <= 0.0) {
return Err(Error::NonFiniteTally {
field: "kde bandwidth",
index: w
.iter()
.position(|v| !v.is_finite() || *v <= 0.0)
.unwrap_or(0),
});
}
w
}
Bandwidth::Silverman => {
let factor = (4.0 / (d + 2.0) / n).powf(1.0 / (d + 4.0));
let mut widths = Vec::with_capacity(dims);
for j in 0..dims {
let mean = samples.iter().map(|row| row[j]).sum::<f64>() / n;
let var = samples
.iter()
.map(|row| (row[j] - mean).powi(2))
.sum::<f64>()
/ (n - 1.0).max(1.0);
let sigma = var.sqrt();
if sigma <= 0.0 {
return Err(Error::ZeroVarianceDim { dim: j });
}
widths.push(sigma * factor);
}
widths
}
};
Ok(Self {
samples: samples.to_vec(),
widths,
})
}
pub fn n_samples(&self) -> usize {
self.samples.len()
}
pub fn dims(&self) -> usize {
self.widths.len()
}
pub fn bandwidths(&self) -> &[f64] {
&self.widths
}
pub fn pdf(&self, point: &[f64]) -> Result<f64> {
if point.len() != self.dims() {
return Err(Error::LengthMismatch {
expected: self.dims(),
got: point.len(),
});
}
if point.iter().any(|v| !v.is_finite()) {
return Err(Error::NonFiniteTally {
field: "kde point",
index: point.iter().position(|v| !v.is_finite()).unwrap_or(0),
});
}
let norm = self.widths.iter().map(|h| h * SQRT_2PI).product::<f64>();
let mut density = 0.0;
for row in &self.samples {
let mut z2 = 0.0;
for ((x, c), h) in point.iter().zip(row).zip(&self.widths) {
let z = (x - c) / h;
z2 += z * z;
}
density += (-0.5 * z2).exp();
}
Ok(density / norm / self.samples.len() as f64)
}
pub fn draw(&self, u: f64, normals: &[f64]) -> Result<Vec<f64>> {
if !(0.0..1.0).contains(&u) {
return Err(Error::BadDraw { value: u });
}
if normals.len() != self.dims() {
return Err(Error::LengthMismatch {
expected: self.dims(),
got: normals.len(),
});
}
if normals.iter().any(|v| !v.is_finite()) {
return Err(Error::NonFiniteTally {
field: "kde normal",
index: normals.iter().position(|v| !v.is_finite()).unwrap_or(0),
});
}
let centre = &self.samples[(u * self.samples.len() as f64) as usize];
Ok(centre
.iter()
.zip(&self.widths)
.zip(normals)
.map(|((c, h), z)| c + h * z)
.collect())
}
}
#[cfg(test)]
mod tests {
use super::*;
struct TestRng(u64);
impl TestRng {
fn uniform(&mut self) -> f64 {
self.0 = self
.0
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
((self.0 >> 11) as f64) / ((1u64 << 53) as f64)
}
fn normal(&mut self) -> f64 {
let (u1, u2) = (self.uniform().max(1e-300), self.uniform());
(-2.0 * u1.ln()).sqrt() * (2.0 * std::f64::consts::PI * u2).cos()
}
}
fn gaussian_samples(mean: f64, sigma: f64, n: usize) -> Vec<Vec<f64>> {
let mut rng = TestRng(0x1234_5678_9abc_def0);
(0..n).map(|_| vec![mean + sigma * rng.normal()]).collect()
}
#[test]
fn input_errors_are_loud() {
assert!(KdeSampler::fit(&[], Bandwidth::Silverman).is_err());
assert!(KdeSampler::fit(&[vec![]], Bandwidth::Silverman).is_err());
assert!(KdeSampler::fit(&[vec![1.0], vec![1.0, 2.0]], Bandwidth::Silverman).is_err());
assert!(KdeSampler::fit(&[vec![f64::NAN]], Bandwidth::Silverman).is_err());
assert!(KdeSampler::fit(&[vec![1.0]], Bandwidth::Fixed(vec![])).is_err());
assert!(KdeSampler::fit(&[vec![1.0]], Bandwidth::Fixed(vec![0.0])).is_err());
assert!(KdeSampler::fit(&[vec![1.0]], Bandwidth::Fixed(vec![-2.0])).is_err());
assert!(KdeSampler::fit(&vec![vec![3.0]; 8], Bandwidth::Silverman).is_err());
assert!(KdeSampler::fit(&vec![vec![3.0]; 8], Bandwidth::Fixed(vec![0.5])).is_ok());
}
#[test]
fn draw_is_exact_and_deterministic() {
let kde = KdeSampler::fit(
&[vec![1.0, 2.0], vec![3.0, 4.0]],
Bandwidth::Fixed(vec![0.5, 2.0]),
)
.unwrap();
let got = kde.draw(0.75, &[1.0, -0.5]).unwrap();
assert_eq!(got, vec![3.5, 3.0]);
assert_eq!(kde.draw(0.75, &[1.0, -0.5]).unwrap(), got);
assert!(kde.draw(1.0, &[0.0, 0.0]).is_err());
assert!(kde.draw(-0.1, &[0.0, 0.0]).is_err());
assert!(kde.draw(0.5, &[0.0]).is_err());
assert!(kde.draw(0.5, &[0.0, f64::INFINITY]).is_err());
}
#[test]
fn gaussian_recovery_and_normalization() {
let samples = gaussian_samples(5.0, 2.0, 20_000);
let kde = KdeSampler::fit(&samples, Bandwidth::Silverman).unwrap();
let h = kde.bandwidths()[0];
assert!(h > 0.0 && h < 1.0, "silverman width {h}");
let mut draws = Vec::with_capacity(4096);
let mut rng = TestRng(0xabcd);
for _ in 0..4096 {
draws.push(kde.draw(rng.uniform(), &[rng.normal()]).unwrap()[0]);
}
let mean = draws.iter().sum::<f64>() / draws.len() as f64;
let se = 2.0 / (draws.len() as f64).sqrt();
assert!((mean - 5.0).abs() < 5.0 * se, "mean {mean}");
let closed = (-0.5 * ((5.0f64 - 5.0) / 2.0).powi(2)).exp() / (2.0 * SQRT_2PI);
let got = kde.pdf(&[5.0]).unwrap();
assert!(
(got - closed).abs() / closed < 0.02,
"pdf {got} vs {closed}"
);
let (lo, hi, m) = (-11.0, 21.0, 2048);
let mut area = 0.0;
let mut prev = kde.pdf(&[lo]).unwrap();
for i in 1..=m {
let x = lo + (hi - lo) * i as f64 / m as f64;
let cur = kde.pdf(&[x]).unwrap();
area += 0.5 * (prev + cur) * (hi - lo) / m as f64;
prev = cur;
}
assert!((area - 1.0).abs() < 1e-3, "integral {area}");
}
#[test]
fn bandwidth_is_deterministic() {
let samples = gaussian_samples(0.0, 1.0, 512);
let a = KdeSampler::fit(&samples, Bandwidth::Silverman).unwrap();
let b = KdeSampler::fit(&samples, Bandwidth::Silverman).unwrap();
assert_eq!(a.bandwidths(), b.bandwidths());
}
}