use crate::error::StatError;
use crate::nan::{self, NanPolicy};
use crate::accum::{Histogram, HistResult, Accumulator};
use crate::accum;
use crate::descriptive;
use core::f64::consts::PI;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Kernel {
Gaussian,
Epanechnikov,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Bandwidth {
Silverman,
Scott,
Fixed(f64),
}
#[derive(Debug, Clone)]
pub struct Kde {
data: Vec<f64>,
h: f64,
kernel: Kernel,
}
pub fn kde(xs: &[f64], kernel: Kernel, bandwidth: Bandwidth) -> Result<Kde, StatError> {
let finite = nan::clean(xs, NanPolicy::Omit)?; let n = finite.len();
if n < 2 {
return Err(StatError::TooFewObservations { needed: 2, got: n });
}
let h = match bandwidth {
Bandwidth::Fixed(h) => {
if !h.is_finite() || h <= 0.0 {
return Err(StatError::DomainError(
"Fixed bandwidth must be finite and > 0",
));
}
h
}
Bandwidth::Silverman => {
let sd = descriptive::sd(&finite, descriptive::Ddof::Sample)
.map_err(|_| StatError::DomainError("zero-variance data: bandwidth would be 0"))?;
if sd == 0.0 {
return Err(StatError::DomainError(
"zero-variance data: Silverman bandwidth would be 0",
));
}
sd * libm::pow(3.0 * n as f64 / 4.0, -0.2)
}
Bandwidth::Scott => {
let sd = descriptive::sd(&finite, descriptive::Ddof::Sample)
.map_err(|_| StatError::DomainError("zero-variance data: bandwidth would be 0"))?;
if sd == 0.0 {
return Err(StatError::DomainError(
"zero-variance data: Scott bandwidth would be 0",
));
}
sd * libm::pow(n as f64, -0.2)
}
};
let mut data = finite;
data.sort_by(|a, b| a.partial_cmp(b).unwrap());
Ok(Kde { data, h, kernel })
}
impl Kde {
pub fn density(&self, x: f64) -> f64 {
if !x.is_finite() {
return 0.0;
}
let n = self.data.len() as f64;
let h = self.h;
let sum: f64 = self.data.iter().map(|&xi| kernel_eval(self.kernel, (x - xi) / h)).sum();
sum / (n * h)
}
pub fn evaluate(&self, grid: &[f64]) -> Vec<f64> {
grid.iter().map(|&x| self.density(x)).collect()
}
pub fn bandwidth(&self) -> f64 {
self.h
}
pub fn n(&self) -> usize {
self.data.len()
}
}
#[inline]
fn kernel_eval(k: Kernel, u: f64) -> f64 {
match k {
Kernel::Gaussian => {
libm::exp(-0.5 * u * u) / libm::sqrt(2.0 * PI)
}
Kernel::Epanechnikov => {
if u.abs() <= 1.0 {
0.75 * (1.0 - u * u)
} else {
0.0
}
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum Bins {
Fixed(usize),
Width(f64),
Edges(Vec<f64>),
Rule(Rule),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Rule {
Sturges,
Scott,
FreedmanDiaconis,
Rice,
Sqrt,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Norm {
Count,
Density,
Probability,
}
pub fn histogram_auto(
xs: &[f64],
bins: Bins,
norm: Norm,
) -> Result<(Vec<f64>, Vec<f64>), StatError> {
let finite = nan::clean(xs, NanPolicy::Omit)?;
let n = finite.len();
let n_f = n as f64;
let lo = descriptive::min(&finite)?;
let hi = descriptive::max(&finite)?;
if lo == hi {
return Err(StatError::DomainError("min == max: cannot form histogram bins"));
}
if let Bins::Edges(ref edges) = bins {
return histogram_edges(&finite, edges, norm);
}
let n_bins = resolve_bin_count(&finite, &bins, lo, hi, n_f)?;
let mut hist = Histogram::new(lo, hi, n_bins)?;
for &x in &finite {
hist.update(x);
}
let result: HistResult = hist.finalize();
let counts_u64 = &result.counts;
let n_bins_actual = counts_u64.len();
let bin_width = (hi - lo) / n_bins_actual as f64;
let edges: Vec<f64> = (0..=n_bins_actual)
.map(|i| lo + i as f64 * bin_width)
.collect();
let values = apply_norm_uniform(counts_u64, n_f, bin_width, norm);
Ok((edges, values))
}
fn resolve_bin_count(
finite: &[f64],
bins: &Bins,
lo: f64,
hi: f64,
n_f: f64,
) -> Result<usize, StatError> {
let range = hi - lo;
match bins {
Bins::Fixed(k) => {
if *k == 0 {
return Err(StatError::DomainError("Fixed(0): bin count must be ≥ 1"));
}
Ok(*k)
}
Bins::Width(w) => {
if !w.is_finite() || *w <= 0.0 {
return Err(StatError::DomainError(
"Width must be finite and > 0",
));
}
Ok(libm::ceil(range / w) as usize)
}
Bins::Rule(rule) => resolve_rule_bin_count(finite, *rule, lo, hi, n_f),
Bins::Edges(_) => unreachable!("Edges handled before resolve_bin_count"),
}
}
fn resolve_rule_bin_count(
finite: &[f64],
rule: Rule,
lo: f64,
hi: f64,
n_f: f64,
) -> Result<usize, StatError> {
let range = hi - lo;
match rule {
Rule::Sturges => {
Ok(libm::ceil(libm::log2(n_f)) as usize + 1)
}
Rule::Scott => {
let sd = descriptive::sd(finite, descriptive::Ddof::Sample)?;
let w = 3.49 * sd * libm::pow(n_f, -1.0 / 3.0);
Ok(libm::ceil(range / w) as usize)
}
Rule::FreedmanDiaconis => {
let mut sorted = finite.to_vec();
sorted.sort_by(|a, b| a.partial_cmp(b).unwrap());
let q1 = accum::quantile_sorted(&sorted, 0.25);
let q3 = accum::quantile_sorted(&sorted, 0.75);
let iqr = q3 - q1;
if iqr == 0.0 {
let sd = descriptive::sd(finite, descriptive::Ddof::Sample)?;
if sd == 0.0 {
return Err(StatError::DomainError(
"FreedmanDiaconis: IQR=0 and sd=0 — cannot determine bin width",
));
}
let w = 3.49 * sd * libm::pow(n_f, -1.0 / 3.0);
return Ok(libm::ceil(range / w) as usize);
}
let w = 2.0 * iqr * libm::pow(n_f, -1.0 / 3.0);
Ok(libm::ceil(range / w) as usize)
}
Rule::Rice => {
Ok(libm::ceil(2.0 * libm::cbrt(n_f)) as usize)
}
Rule::Sqrt => {
Ok(libm::ceil(libm::sqrt(n_f)) as usize)
}
}
}
fn histogram_edges(
finite: &[f64],
edges: &[f64],
norm: Norm,
) -> Result<(Vec<f64>, Vec<f64>), StatError> {
if edges.len() < 2 {
return Err(StatError::DomainError(
"Edges must have at least 2 elements",
));
}
for w in edges.windows(2) {
if w[0] >= w[1] {
return Err(StatError::DomainError(
"Edges must be strictly increasing",
));
}
}
let n_bins = edges.len() - 1;
let mut counts = vec![0u64; n_bins];
let n_total = finite.len() as f64;
let last_edge = *edges.last().unwrap();
for &x in finite {
let idx = if x == last_edge {
n_bins
} else {
edges.partition_point(|&e| e <= x)
};
if idx >= 1 && idx <= n_bins {
counts[idx - 1] += 1;
}
}
let values = match norm {
Norm::Count => counts.iter().map(|&c| c as f64).collect(),
Norm::Probability => counts.iter().map(|&c| c as f64 / n_total).collect(),
Norm::Density => counts
.iter()
.enumerate()
.map(|(i, &c)| {
let bin_width = edges[i + 1] - edges[i];
c as f64 / (n_total * bin_width)
})
.collect(),
};
Ok((edges.to_vec(), values))
}
fn apply_norm_uniform(counts: &[u64], n_total: f64, bin_width: f64, norm: Norm) -> Vec<f64> {
match norm {
Norm::Count => counts.iter().map(|&c| c as f64).collect(),
Norm::Probability => counts.iter().map(|&c| c as f64 / n_total).collect(),
Norm::Density => counts
.iter()
.map(|&c| c as f64 / (n_total * bin_width))
.collect(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::error::StatError;
#[test]
fn kde_empty_input() {
let xs: &[f64] = &[];
assert_eq!(kde(xs, Kernel::Gaussian, Bandwidth::Silverman).unwrap_err(), StatError::EmptyInput);
}
#[test]
fn kde_too_few_observations() {
let xs = &[1.0_f64];
assert_eq!(
kde(xs, Kernel::Gaussian, Bandwidth::Silverman).unwrap_err(),
StatError::TooFewObservations { needed: 2, got: 1 }
);
}
#[test]
fn kde_zero_variance_silverman() {
let xs = &[3.0_f64, 3.0, 3.0, 3.0];
assert!(matches!(
kde(xs, Kernel::Gaussian, Bandwidth::Silverman).unwrap_err(),
StatError::DomainError(_)
));
}
#[test]
fn kde_zero_variance_scott() {
let xs = &[5.0_f64, 5.0, 5.0];
assert!(matches!(
kde(xs, Kernel::Gaussian, Bandwidth::Scott).unwrap_err(),
StatError::DomainError(_)
));
}
#[test]
fn kde_fixed_nonpositive() {
let xs = &[1.0_f64, 2.0, 3.0];
assert!(matches!(
kde(xs, Kernel::Gaussian, Bandwidth::Fixed(0.0)).unwrap_err(),
StatError::DomainError(_)
));
assert!(matches!(
kde(xs, Kernel::Gaussian, Bandwidth::Fixed(-1.0)).unwrap_err(),
StatError::DomainError(_)
));
assert!(matches!(
kde(xs, Kernel::Gaussian, Bandwidth::Fixed(f64::NAN)).unwrap_err(),
StatError::DomainError(_)
));
}
#[test]
fn kde_fixed_resolves_bandwidth() {
let xs = &[1.0_f64, 2.0, 3.0, 4.0, 5.0];
let k = kde(xs, Kernel::Gaussian, Bandwidth::Fixed(0.5)).unwrap();
assert_eq!(k.bandwidth(), 0.5);
assert_eq!(k.n(), 5);
}
#[test]
fn kde_silverman_bandwidth_formula() {
let xs = &[1.0_f64, 2.0, 3.0, 4.0, 5.0];
let k = kde(xs, Kernel::Gaussian, Bandwidth::Silverman).unwrap();
let sd = crate::descriptive::sd(xs, crate::descriptive::Ddof::Sample).unwrap();
let n = 5_f64;
let expected_h = sd * libm::pow(3.0 * n / 4.0, -0.2);
let diff = (k.bandwidth() - expected_h).abs();
assert!(diff < 1e-14, "Silverman h: got {}, want {}", k.bandwidth(), expected_h);
}
#[test]
fn kde_scott_bandwidth_formula() {
let xs = &[1.0_f64, 2.0, 3.0, 4.0, 5.0];
let k = kde(xs, Kernel::Gaussian, Bandwidth::Scott).unwrap();
let sd = crate::descriptive::sd(xs, crate::descriptive::Ddof::Sample).unwrap();
let expected_h = sd * libm::pow(5.0_f64, -0.2);
let diff = (k.bandwidth() - expected_h).abs();
assert!(diff < 1e-14, "Scott h: got {}, want {}", k.bandwidth(), expected_h);
}
#[test]
fn kde_gaussian_density_formula() {
let xs = &[0.0_f64, 1.0];
let k = kde(xs, Kernel::Gaussian, Bandwidth::Fixed(1.0)).unwrap();
let expected = libm::exp(-0.125) / libm::sqrt(2.0 * PI);
let got = k.density(0.5);
let diff = (got - expected).abs();
assert!(diff < 1e-14, "density at 0.5: got {got}, want {expected}");
}
#[test]
fn kde_epanechnikov_density_formula() {
let xs = &[0.0_f64, 1.0];
let k = kde(xs, Kernel::Epanechnikov, Bandwidth::Fixed(2.0)).unwrap();
let u1 = (0.5 - 0.0) / 2.0; let u2 = (0.5 - 1.0) / 2.0; let expected = 0.25 * (0.75 * (1.0 - u1 * u1) + 0.75 * (1.0 - u2 * u2));
let got = k.density(0.5);
let diff = (got - expected).abs();
assert!(diff < 1e-15, "epanechnikov density: got {got}, want {expected}");
}
#[test]
fn kde_evaluate_length() {
let xs = &[1.0_f64, 2.0, 3.0, 4.0, 5.0];
let k = kde(xs, Kernel::Gaussian, Bandwidth::Fixed(1.0)).unwrap();
let grid = &[1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
assert_eq!(k.evaluate(grid).len(), grid.len());
}
#[test]
fn kde_nan_in_input_omitted() {
let xs = &[1.0_f64, f64::NAN, 3.0, f64::NAN, 5.0];
let k = kde(xs, Kernel::Gaussian, Bandwidth::Silverman).unwrap();
assert_eq!(k.n(), 3);
}
#[test]
fn hist_empty_input() {
let xs: &[f64] = &[];
assert_eq!(
histogram_auto(xs, Bins::Fixed(5), Norm::Count).unwrap_err(),
StatError::EmptyInput
);
}
#[test]
fn hist_min_eq_max() {
let xs = &[3.0_f64, 3.0, 3.0];
assert!(matches!(
histogram_auto(xs, Bins::Fixed(5), Norm::Count).unwrap_err(),
StatError::DomainError(_)
));
}
#[test]
fn hist_fixed_zero() {
let xs = &[1.0_f64, 2.0, 3.0];
assert!(matches!(
histogram_auto(xs, Bins::Fixed(0), Norm::Count).unwrap_err(),
StatError::DomainError(_)
));
}
#[test]
fn hist_width_nonpositive() {
let xs = &[1.0_f64, 2.0, 3.0];
assert!(matches!(
histogram_auto(xs, Bins::Width(0.0), Norm::Count).unwrap_err(),
StatError::DomainError(_)
));
assert!(matches!(
histogram_auto(xs, Bins::Width(-1.0), Norm::Count).unwrap_err(),
StatError::DomainError(_)
));
}
#[test]
fn hist_edges_too_few() {
let xs = &[1.0_f64, 2.0, 3.0];
assert!(matches!(
histogram_auto(xs, Bins::Edges(vec![1.0]), Norm::Count).unwrap_err(),
StatError::DomainError(_)
));
}
#[test]
fn hist_edges_not_strictly_increasing() {
let xs = &[1.0_f64, 2.0, 3.0];
assert!(matches!(
histogram_auto(xs, Bins::Edges(vec![1.0, 2.0, 2.0, 3.0]), Norm::Count).unwrap_err(),
StatError::DomainError(_)
));
}
#[test]
fn hist_sturges_bin_count() {
let xs: Vec<f64> = (0..16).map(|i| i as f64).collect();
let (edges, counts) = histogram_auto(&xs, Bins::Rule(Rule::Sturges), Norm::Count).unwrap();
assert_eq!(counts.len(), 5, "Sturges n=16: expected 5 bins");
assert_eq!(edges.len(), 6);
let total: f64 = counts.iter().sum();
assert_eq!(total, 16.0);
}
#[test]
fn hist_rice_bin_count() {
let xs: Vec<f64> = (0..8).map(|i| i as f64).collect();
let (_, counts) = histogram_auto(&xs, Bins::Rule(Rule::Rice), Norm::Count).unwrap();
assert_eq!(counts.len(), 4, "Rice n=8: expected 4 bins");
}
#[test]
fn hist_sqrt_bin_count() {
let xs: Vec<f64> = (0..9).map(|i| i as f64).collect();
let (_, counts) = histogram_auto(&xs, Bins::Rule(Rule::Sqrt), Norm::Count).unwrap();
assert_eq!(counts.len(), 3, "Sqrt n=9: expected 3 bins");
}
#[test]
fn hist_edges_partition_point_assignment() {
let xs = &[0.0_f64, 0.5, 1.0, 1.5, 2.0, 2.5, 2.9, 3.0];
let edges = vec![0.0, 1.0, 2.0, 3.0];
let (ret_edges, counts) =
histogram_auto(xs, Bins::Edges(edges.clone()), Norm::Count).unwrap();
assert_eq!(ret_edges, edges);
assert_eq!(counts, vec![2.0, 2.0, 4.0]);
}
#[test]
fn hist_norm_probability() {
let xs = &[1.0_f64, 2.0, 3.0, 4.0];
let (_, vals) = histogram_auto(xs, Bins::Fixed(2), Norm::Probability).unwrap();
let total: f64 = vals.iter().sum();
assert!((total - 1.0).abs() < 1e-14, "Probability sum != 1: {total}");
}
#[test]
fn hist_norm_density_integrates_to_one() {
let xs = &[1.0_f64, 2.0, 3.0, 4.0, 5.0];
let (edges, vals) = histogram_auto(xs, Bins::Fixed(4), Norm::Density).unwrap();
let integral: f64 = vals
.iter()
.enumerate()
.map(|(i, &v)| v * (edges[i + 1] - edges[i]))
.sum();
assert!((integral - 1.0).abs() < 1e-13, "Density integral != 1: {integral}");
}
#[test]
fn hist_norm_density_edges_variable_width() {
let xs = &[0.0_f64, 0.5, 1.5, 2.5];
let edges = vec![0.0, 1.0, 3.0];
let (_, vals) = histogram_auto(xs, Bins::Edges(edges), Norm::Density).unwrap();
assert!((vals[0] - 0.5).abs() < 1e-14, "density bin0: {}", vals[0]);
assert!((vals[1] - 0.25).abs() < 1e-14, "density bin1: {}", vals[1]);
}
}