use ndarray::{Array1, Array2};
use rand::Rng;
use robust_rs_core::error::RobustError;
use robust_rs_core::rho::{RhoFunction, TukeyBiweight};
use robust_rs_core::scale::{SScale, ScaleEstimator};
use robust_rs_core::solver::Control;
use robust_rs_core::types::Scale;
use super::subsample::{fast_resample, SearchConfig};
use crate::estimator::RegressionFit;
use crate::wls::weighted_least_squares;
const DEFAULT_SEED: u64 = 0xFA57_5EED;
#[derive(Debug, Clone, Copy)]
pub struct SEstimator<R = TukeyBiweight> {
rho: R,
delta: f64,
n_subsamples: usize,
seed: u64,
control: Control,
}
impl Default for SEstimator<TukeyBiweight> {
fn default() -> Self {
Self::new(TukeyBiweight::new(1.547).expect("1.547 is a valid tuning"))
}
}
impl<R: RhoFunction + Clone + 'static> SEstimator<R> {
pub fn new(rho: R) -> Self {
let delta = rho.rho_sup().map_or(f64::NAN, |sup| 0.5 * sup);
Self {
rho,
delta,
n_subsamples: 500,
seed: DEFAULT_SEED,
control: Control::default(),
}
}
pub fn delta(mut self, delta: f64) -> Self {
self.delta = delta;
self
}
pub fn n_subsamples(mut self, n: usize) -> Self {
self.n_subsamples = n;
self
}
pub fn seed(mut self, seed: u64) -> Self {
self.seed = seed;
self
}
pub fn control(mut self, control: Control) -> Self {
self.control = control;
self
}
pub fn fit(&self, x: &Array2<f64>, y: &Array1<f64>) -> Result<RegressionFit, RobustError> {
self.fit_from_seed(x, y, self.seed)
}
pub fn fit_with_rng<G: Rng>(
&self,
x: &Array2<f64>,
y: &Array1<f64>,
rng: &mut G,
) -> Result<RegressionFit, RobustError> {
self.fit_from_seed(x, y, rng.random::<u64>())
}
fn fit_from_seed(
&self,
x: &Array2<f64>,
y: &Array1<f64>,
master_seed: u64,
) -> Result<RegressionFit, RobustError> {
let sscale = SScale::new(self.rho.clone(), self.delta)?.with_control(self.control);
let rho = &self.rho;
let score = |beta: &Array1<f64>| -> Result<f64, RobustError> {
let resid = y - &x.dot(beta);
Ok(sscale
.scale(resid.as_slice().expect("contiguous residuals"))?
.get())
};
let cstep = |beta: &Array1<f64>| -> Result<Array1<f64>, RobustError> {
let resid = y - &x.dot(beta);
let s = sscale
.scale(resid.as_slice().expect("contiguous residuals"))?
.get();
let w = resid.mapv(|r| rho.weight(r / s));
weighted_least_squares(x, y, &w)
};
let cfg = SearchConfig {
n_subsamples: self.n_subsamples,
control: self.control,
..SearchConfig::default()
};
let (coefficients, best_scale) = fast_resample(x, y, master_seed, &cfg, score, cstep)?;
let scale = Scale::new(best_scale)?;
let s = scale.get();
let residuals = y - &x.dot(&coefficients);
let weights = residuals.mapv(|r| self.rho.weight(r / s));
Ok(RegressionFit {
coefficients,
scale,
residuals,
weights,
rho: Box::new(self.rho.clone()),
breakdown_point: breakdown_from_delta(self.delta, self.rho.rho_sup()),
})
}
}
fn breakdown_from_delta(delta: f64, rho_sup: Option<f64>) -> f64 {
match rho_sup {
Some(sup) if sup > 0.0 => {
let ratio = delta / sup;
ratio.min(1.0 - ratio)
}
_ => 0.0,
}
}