1use solow_distributions::{norm_cdf, norm_pdf, norm_ppf};
4
5pub fn mad_c() -> f64 {
10 norm_ppf(0.75)
11}
12
13pub fn median(a: &[f64]) -> f64 {
18 let mut v: Vec<f64> = a.to_vec();
19 v.sort_by(|x, y| x.total_cmp(y));
20 let n = v.len();
21 if n == 0 {
22 return f64::NAN;
23 }
24 if n % 2 == 1 {
25 v[n / 2]
26 } else {
27 0.5 * (v[n / 2 - 1] + v[n / 2])
28 }
29}
30
31pub fn mad(a: &[f64], c: f64, center: Option<f64>) -> f64 {
37 let cen = center.unwrap_or_else(|| median(a));
38 let dev: Vec<f64> = a.iter().map(|&x| (x - cen).abs() / c).collect();
39 median(&dev)
40}
41
42#[derive(Clone, Copy, Debug)]
54pub struct HuberScale {
55 pub d: f64,
57 pub tol: f64,
59 pub maxiter: usize,
61}
62
63impl Default for HuberScale {
64 fn default() -> Self {
65 HuberScale {
66 d: 2.5,
67 tol: 1e-8,
68 maxiter: 30,
69 }
70 }
71}
72
73impl HuberScale {
74 pub fn scale(&self, df_resid: f64, nobs: f64, resid: &[f64]) -> f64 {
79 let d = self.d;
80 let h = df_resid / nobs
81 * (d * d + (1.0 - d * d) * norm_cdf(d)
82 - 0.5
83 - d / (2.0 * std::f64::consts::PI).sqrt() * (-0.5 * d * d).exp());
84 let s0 = mad(resid, mad_c(), None);
85
86 let chi_sum = |s: f64| -> f64 {
87 resid
88 .iter()
89 .map(|&r| {
90 if (r / s).abs() < d {
91 (r / s).powi(2) / 2.0
92 } else {
93 d * d / 2.0
94 }
95 })
96 .sum::<f64>()
97 };
98
99 let mut prev = f64::INFINITY;
100 let mut cur = s0;
101 let mut niter = 1;
102 while (prev - cur).abs() > self.tol && niter < self.maxiter {
103 let nscale = (1.0 / (nobs * h) * chi_sum(cur) * cur * cur).sqrt();
104 prev = cur;
105 cur = nscale;
106 niter += 1;
107 }
108 cur
109 }
110}
111
112#[derive(Clone, Copy, Debug)]
118pub struct Huber {
119 pub c: f64,
121 pub tol: f64,
123 pub maxiter: usize,
125}
126
127impl Default for Huber {
128 fn default() -> Self {
129 Huber {
130 c: 1.5,
131 tol: 1e-8,
132 maxiter: 30,
133 }
134 }
135}
136
137impl Huber {
138 fn gamma(&self) -> f64 {
140 let tmp = 2.0 * norm_cdf(self.c) - 1.0;
141 tmp + self.c * self.c * (1.0 - tmp) - 2.0 * self.c * norm_pdf(self.c)
142 }
143
144 pub fn estimate(&self, a: &[f64]) -> Option<(f64, f64)> {
148 let n = (a.len() - 1) as f64;
149 let gamma = self.gamma();
150 let mut mu = median(a);
151 let mut sc = mad(a, mad_c(), None);
152
153 for _ in 0..self.maxiter {
154 let lo = mu - self.c * sc;
155 let hi = mu + self.c * sc;
156 let nmu = a.iter().map(|&x| x.clamp(lo, hi)).sum::<f64>() / a.len() as f64;
157
158 let mut card = 0usize;
159 let mut num = 0.0;
160 for &x in a {
161 if ((x - mu) / sc).abs() <= self.c {
162 card += 1;
163 num += (x - nmu).powi(2);
164 }
165 }
166 let denom = n * gamma - (a.len() - card) as f64 * self.c * self.c;
167 let nscale = (num / denom).sqrt();
168
169 let test1 = (sc - nscale).abs() <= nscale * self.tol;
170 let test2 = (mu - nmu).abs() <= nscale * self.tol;
171 if test1 && test2 {
172 return Some((nmu, nscale));
173 }
174 mu = nmu;
175 sc = nscale;
176 }
177 None
178 }
179}
180
181#[cfg(test)]
182mod tests {
183 use super::*;
184
185 #[test]
186 fn median_handles_even_and_odd() {
187 assert_eq!(median(&[3.0, 1.0, 2.0]), 2.0);
188 assert_eq!(median(&[1.0, 2.0, 3.0, 4.0]), 2.5);
189 }
190
191 #[test]
192 fn mad_of_standard_normal_constant_is_unit_scale() {
193 let x = [-2.0, -1.0, 0.0, 1.0, 2.0];
195 let want = 1.0 / mad_c();
196 assert!((mad(&x, mad_c(), Some(0.0)) - want).abs() < 1e-12);
197 }
198
199 #[test]
200 fn mad_default_centers_on_median() {
201 let x = [10.0, 11.0, 12.0, 13.0, 14.0];
202 assert!((mad(&x, mad_c(), None) - 1.0 / mad_c()).abs() < 1e-12);
204 }
205}