1use ndarray::{Array1, Array2};
9use solow_core::error::{Error, Result};
10use solow_core::tools::{ensure_all_finite, ensure_all_finite_2d};
11use solow_distributions::norm_sf;
12use solow_linalg::{matrix_rank, pinv};
13
14use crate::norms::RobustNorm;
15use crate::scale::{mad, mad_c, HuberScale};
16
17#[derive(Clone, Copy, Debug, Default)]
19pub enum ScaleEst {
20 #[default]
22 Mad,
23 Huber(HuberScale),
25}
26
27#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
29pub enum Conv {
30 #[default]
32 Deviance,
33 Coefs,
35}
36
37#[derive(Clone, Debug)]
42pub struct Rlm<N: RobustNorm> {
43 endog: Array1<f64>,
44 exog: Array2<f64>,
45 norm: N,
46 scale_est: ScaleEst,
47 conv: Conv,
48 update_scale: bool,
49 maxiter: usize,
50 tol: f64,
51}
52
53impl<N: RobustNorm> Rlm<N> {
54 pub fn new(endog: Array1<f64>, exog: Array2<f64>, norm: N) -> Result<Self> {
59 if endog.len() != exog.nrows() {
60 return Err(Error::Shape("endog length != exog rows".into()));
61 }
62 ensure_all_finite(&endog.view(), "endog")?;
63 ensure_all_finite_2d(&exog.view(), "exog")?;
64 Ok(Rlm {
65 endog,
66 exog,
67 norm,
68 scale_est: ScaleEst::Mad,
69 conv: Conv::Deviance,
70 update_scale: true,
71 maxiter: 50,
72 tol: 1e-8,
73 })
74 }
75
76 pub fn scale_est(mut self, s: ScaleEst) -> Self {
78 self.scale_est = s;
79 self
80 }
81
82 pub fn conv(mut self, c: Conv) -> Self {
84 self.conv = c;
85 self
86 }
87
88 pub fn update_scale(mut self, u: bool) -> Self {
90 self.update_scale = u;
91 self
92 }
93
94 pub fn maxiter(mut self, m: usize) -> Self {
96 self.maxiter = m;
97 self
98 }
99
100 pub fn tol(mut self, t: f64) -> Self {
102 self.tol = t;
103 self
104 }
105
106 pub fn nobs(&self) -> usize {
108 self.endog.len()
109 }
110
111 fn wls(&self, weights: &Array1<f64>) -> Result<(Array1<f64>, Array1<f64>)> {
116 let (n, p) = self.exog.dim();
117 let mut wexog = Array2::<f64>::zeros((n, p));
118 let mut wy = Array1::<f64>::zeros(n);
119 for i in 0..n {
120 let s = weights[i].sqrt();
121 wy[i] = self.endog[i] * s;
122 for j in 0..p {
123 wexog[[i, j]] = self.exog[[i, j]] * s;
124 }
125 }
126 let (pinv_w, _sv) = pinv(&wexog)?;
127 let params = pinv_w.dot(&wy);
128 let resid = &self.endog - &self.exog.dot(¶ms);
129 Ok((params, resid))
130 }
131
132 fn estimate_scale(&self, resid: &[f64], df_resid: f64, nobs: f64) -> f64 {
133 match self.scale_est {
134 ScaleEst::Mad => mad(resid, mad_c(), Some(0.0)),
135 ScaleEst::Huber(h) => h.scale(df_resid, nobs, resid),
136 }
137 }
138
139 pub fn fit(&self) -> Result<RlmResults> {
141 let (n, _p) = self.exog.dim();
142 let nobs = n as f64;
143 let rank = matrix_rank(&self.exog)?;
144 let df_resid = nobs - rank as f64;
145 let df_model = rank as f64 - 1.0;
146
147 let (pinv_x, _sv) = pinv(&self.exog)?;
149 let normalized_cov_params = pinv_x.dot(&pinv_x.t());
150
151 let ones = Array1::<f64>::ones(n);
153 let (mut params, mut resid) = self.wls(&ones)?;
154 let mut scale = self.estimate_scale(
155 resid
156 .as_slice()
157 .ok_or_else(|| Error::Value("resid must be contiguous".into()))?,
158 df_resid,
159 nobs,
160 );
161
162 let mut crit_cur = self.deviance(&resid, scale);
164 let mut weights = Array1::<f64>::ones(n);
165 let mut iteration = 1usize;
166 let mut converged = false;
167
168 while iteration < self.maxiter {
169 if scale == 0.0 {
170 break;
171 }
172 weights = resid.mapv(|r| self.norm.weights(r / scale));
174 let params_prev = params.clone();
175 let (new_params, new_resid) = self.wls(&weights)?;
176 params = new_params;
177 resid = new_resid;
178 if self.update_scale {
179 scale = self.estimate_scale(
180 resid
181 .as_slice()
182 .ok_or_else(|| Error::Value("resid must be contiguous".into()))?,
183 df_resid,
184 nobs,
185 );
186 }
187 iteration += 1;
188
189 match self.conv {
191 Conv::Deviance => {
192 let crit_prev = crit_cur;
193 crit_cur = self.deviance(&resid, scale);
194 if (crit_cur - crit_prev).abs() <= self.tol {
195 converged = true;
196 break;
197 }
198 }
199 Conv::Coefs => {
200 let d = (¶ms - ¶ms_prev).mapv(f64::abs);
201 let maxd = d.iter().cloned().fold(0.0_f64, f64::max);
202 if maxd <= self.tol {
203 converged = true;
204 break;
205 }
206 }
207 }
208 }
209
210 Ok(RlmResults::new(
211 self,
212 params,
213 resid,
214 weights,
215 scale,
216 normalized_cov_params,
217 df_model,
218 df_resid,
219 nobs,
220 iteration,
221 converged,
222 ))
223 }
224
225 fn deviance(&self, resid: &Array1<f64>, scale: f64) -> f64 {
227 resid.iter().map(|&r| self.norm.rho(r / scale)).sum()
228 }
229}
230
231#[derive(Clone, Debug)]
233pub struct RlmResults {
234 pub params: Array1<f64>,
236 pub bse: Array1<f64>,
238 pub tvalues: Array1<f64>,
240 pub pvalues: Array1<f64>,
242
243 pub scale: f64,
245 pub fittedvalues: Array1<f64>,
247 pub resid: Array1<f64>,
249 pub sresid: Array1<f64>,
251 pub weights: Array1<f64>,
253
254 pub df_model: f64,
256 pub df_resid: f64,
258 pub nobs: f64,
260
261 pub bcov_scaled: Array2<f64>,
263 pub bcov_unscaled: Array2<f64>,
265
266 pub iteration: usize,
268 pub converged: bool,
270}
271
272impl RlmResults {
273 #[allow(clippy::too_many_arguments)]
274 fn new<N: RobustNorm>(
275 model: &Rlm<N>,
276 params: Array1<f64>,
277 resid: Array1<f64>,
278 weights: Array1<f64>,
279 scale: f64,
280 normalized_cov_params: Array2<f64>,
281 df_model: f64,
282 df_resid: f64,
283 nobs: f64,
284 iteration: usize,
285 converged: bool,
286 ) -> RlmResults {
287 let fittedvalues = model.exog.dot(¶ms);
288 let sresid: Array1<f64> = if scale == 0.0 {
289 Array1::zeros(resid.len())
290 } else {
291 resid.mapv(|r| r / scale)
292 };
293
294 let psi_d: Vec<f64> = model.norm.psi_deriv_arr(sresid.as_slice().unwrap_or(&[]));
299 let m = mean(&psi_d);
300 let var_psiprime = variance(&psi_d, m);
301 let k = 1.0 + (df_model + 1.0) / nobs * var_psiprime / (m * m);
302
303 let psi: Vec<f64> = model.norm.psi_arr(sresid.as_slice().unwrap_or(&[]));
305 let ss_psi: f64 = psi.iter().map(|&v| v * v).sum();
306 let s_psi_deriv: f64 = psi_d.iter().sum();
307
308 let factor = k * k * (ss_psi * scale * scale / df_resid) / ((s_psi_deriv / nobs).powi(2));
309 let bcov_scaled = &normalized_cov_params * factor;
310
311 let p = params.len();
312 let mut bse = Array1::<f64>::zeros(p);
313 for i in 0..p {
314 bse[i] = bcov_scaled[[i, i]].sqrt();
315 }
316 let tvalues = ¶ms / &bse;
317 let pvalues = tvalues.mapv(|z| 2.0 * norm_sf(z.abs()));
318
319 RlmResults {
320 params,
321 bse,
322 tvalues,
323 pvalues,
324 scale,
325 fittedvalues,
326 resid,
327 sresid,
328 weights,
329 df_model,
330 df_resid,
331 nobs,
332 bcov_scaled,
333 bcov_unscaled: normalized_cov_params,
334 iteration,
335 converged,
336 }
337 }
338
339 pub fn conf_int(&self, alpha: f64) -> Array2<f64> {
344 let q = solow_distributions::norm_ppf(1.0 - alpha / 2.0);
345 let p = self.params.len();
346 let mut ci = Array2::<f64>::zeros((p, 2));
347 for i in 0..p {
348 ci[[i, 0]] = self.params[i] - q * self.bse[i];
349 ci[[i, 1]] = self.params[i] + q * self.bse[i];
350 }
351 ci
352 }
353}
354
355fn mean(a: &[f64]) -> f64 {
356 a.iter().sum::<f64>() / a.len() as f64
357}
358
359fn variance(a: &[f64], m: f64) -> f64 {
361 a.iter().map(|&v| (v - m) * (v - m)).sum::<f64>() / a.len() as f64
362}
363
364#[cfg(test)]
365mod tests {
366 use super::*;
367 use crate::norms::LeastSquares;
368 use ndarray::array;
369
370 #[test]
371 fn least_squares_norm_reduces_to_ols() {
372 let y = array![1.0, 2.0, 2.0, 3.0, 5.0, 4.0];
374 let x = array![
375 [1.0, 0.0],
376 [1.0, 1.0],
377 [1.0, 2.0],
378 [1.0, 3.0],
379 [1.0, 4.0],
380 [1.0, 5.0]
381 ];
382 let res = Rlm::new(y.clone(), x.clone(), LeastSquares)
383 .unwrap()
384 .fit()
385 .unwrap();
386 let xtx = x.t().dot(&x);
388 let (inv, _sv) = pinv(&xtx).unwrap();
389 let ols = inv.dot(&x.t().dot(&y));
390 for i in 0..2 {
391 assert!((res.params[i] - ols[i]).abs() < 1e-9);
392 }
393 assert!(res.weights.iter().all(|&w| (w - 1.0).abs() < 1e-15));
394 }
395
396 #[test]
397 fn degrees_of_freedom() {
398 let y = array![1.0, 2.0, 3.0, 4.0, 5.0];
399 let x = array![[1.0, 0.0], [1.0, 1.0], [1.0, 2.0], [1.0, 3.0], [1.0, 4.0]];
400 let res = Rlm::new(y, x, LeastSquares).unwrap().fit().unwrap();
401 assert_eq!(res.nobs, 5.0);
402 assert_eq!(res.df_model, 1.0);
403 assert_eq!(res.df_resid, 3.0);
404 }
405}