use solow_distributions::{norm_cdf, norm_pdf, norm_ppf};
pub fn mad_c() -> f64 {
norm_ppf(0.75)
}
pub fn median(a: &[f64]) -> f64 {
let mut v: Vec<f64> = a.to_vec();
v.sort_by(|x, y| x.total_cmp(y));
let n = v.len();
if n == 0 {
return f64::NAN;
}
if n % 2 == 1 {
v[n / 2]
} else {
0.5 * (v[n / 2 - 1] + v[n / 2])
}
}
pub fn mad(a: &[f64], c: f64, center: Option<f64>) -> f64 {
let cen = center.unwrap_or_else(|| median(a));
let dev: Vec<f64> = a.iter().map(|&x| (x - cen).abs() / c).collect();
median(&dev)
}
#[derive(Clone, Copy, Debug)]
pub struct HuberScale {
pub d: f64,
pub tol: f64,
pub maxiter: usize,
}
impl Default for HuberScale {
fn default() -> Self {
HuberScale {
d: 2.5,
tol: 1e-8,
maxiter: 30,
}
}
}
impl HuberScale {
pub fn scale(&self, df_resid: f64, nobs: f64, resid: &[f64]) -> f64 {
let d = self.d;
let h = df_resid / nobs
* (d * d + (1.0 - d * d) * norm_cdf(d)
- 0.5
- d / (2.0 * std::f64::consts::PI).sqrt() * (-0.5 * d * d).exp());
let s0 = mad(resid, mad_c(), None);
let chi_sum = |s: f64| -> f64 {
resid
.iter()
.map(|&r| {
if (r / s).abs() < d {
(r / s).powi(2) / 2.0
} else {
d * d / 2.0
}
})
.sum::<f64>()
};
let mut prev = f64::INFINITY;
let mut cur = s0;
let mut niter = 1;
while (prev - cur).abs() > self.tol && niter < self.maxiter {
let nscale = (1.0 / (nobs * h) * chi_sum(cur) * cur * cur).sqrt();
prev = cur;
cur = nscale;
niter += 1;
}
cur
}
}
#[derive(Clone, Copy, Debug)]
pub struct Huber {
pub c: f64,
pub tol: f64,
pub maxiter: usize,
}
impl Default for Huber {
fn default() -> Self {
Huber {
c: 1.5,
tol: 1e-8,
maxiter: 30,
}
}
}
impl Huber {
fn gamma(&self) -> f64 {
let tmp = 2.0 * norm_cdf(self.c) - 1.0;
tmp + self.c * self.c * (1.0 - tmp) - 2.0 * self.c * norm_pdf(self.c)
}
pub fn estimate(&self, a: &[f64]) -> Option<(f64, f64)> {
let n = (a.len() - 1) as f64;
let gamma = self.gamma();
let mut mu = median(a);
let mut sc = mad(a, mad_c(), None);
for _ in 0..self.maxiter {
let lo = mu - self.c * sc;
let hi = mu + self.c * sc;
let nmu = a.iter().map(|&x| x.clamp(lo, hi)).sum::<f64>() / a.len() as f64;
let mut card = 0usize;
let mut num = 0.0;
for &x in a {
if ((x - mu) / sc).abs() <= self.c {
card += 1;
num += (x - nmu).powi(2);
}
}
let denom = n * gamma - (a.len() - card) as f64 * self.c * self.c;
let nscale = (num / denom).sqrt();
let test1 = (sc - nscale).abs() <= nscale * self.tol;
let test2 = (mu - nmu).abs() <= nscale * self.tol;
if test1 && test2 {
return Some((nmu, nscale));
}
mu = nmu;
sc = nscale;
}
None
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn median_handles_even_and_odd() {
assert_eq!(median(&[3.0, 1.0, 2.0]), 2.0);
assert_eq!(median(&[1.0, 2.0, 3.0, 4.0]), 2.5);
}
#[test]
fn mad_of_standard_normal_constant_is_unit_scale() {
let x = [-2.0, -1.0, 0.0, 1.0, 2.0];
let want = 1.0 / mad_c();
assert!((mad(&x, mad_c(), Some(0.0)) - want).abs() < 1e-12);
}
#[test]
fn mad_default_centers_on_median() {
let x = [10.0, 11.0, 12.0, 13.0, 14.0];
assert!((mad(&x, mad_c(), None) - 1.0 / mad_c()).abs() < 1e-12);
}
}