use ndarray::{Array1, Array2, Axis};
use rand::Rng;
use robust_rs_core::error::RobustError;
use robust_rs_core::scale::{Mad, ScaleEstimator};
use robust_rs_core::solver::Control;
use robust_rs_core::types::Scale;
use super::subsample::{fast_resample, SearchConfig};
use crate::wls::weighted_least_squares;
const DEFAULT_SEED: u64 = 0x0175_5EED;
#[derive(Debug, Clone, Copy)]
pub struct Lts {
coverage: Option<f64>,
n_subsamples: usize,
seed: u64,
control: Control,
}
impl Default for Lts {
fn default() -> Self {
Self {
coverage: None,
n_subsamples: 500,
seed: DEFAULT_SEED,
control: Control::default(),
}
}
}
impl Lts {
pub fn new() -> Self {
Self::default()
}
pub fn coverage(mut self, fraction: f64) -> Self {
self.coverage = Some(fraction);
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<LtsFit, 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<LtsFit, 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<LtsFit, RobustError> {
let (n, p) = x.dim();
if p == 0 {
return Err(RobustError::SingularDesign);
}
if y.len() != n {
return Err(RobustError::DimensionMismatch {
expected: n,
got: y.len(),
});
}
if n < p {
return Err(RobustError::InsufficientData { needed: p, got: n });
}
let h = match self.coverage {
Some(fraction) => {
if !(fraction.is_finite() && fraction > 0.0 && fraction <= 1.0) {
return Err(RobustError::InvalidTuning { value: fraction });
}
((fraction * n as f64).floor() as usize).clamp(p, n)
}
None => (n + p).div_ceil(2).clamp(p, n), };
let residuals = |beta: &Array1<f64>| -> Array1<f64> { y - &x.dot(beta) };
let ones_h = Array1::ones(h);
let score = |beta: &Array1<f64>| -> Result<f64, RobustError> {
let r = residuals(beta);
let idx = h_smallest_abs(&r, h);
Ok(idx.iter().map(|&i| r[i] * r[i]).sum())
};
let cstep = |beta: &Array1<f64>| -> Result<Array1<f64>, RobustError> {
let r = residuals(beta);
let idx = h_smallest_abs(&r, h);
let xs = x.select(Axis(0), &idx);
let ys = y.select(Axis(0), &idx);
weighted_least_squares(&xs, &ys, &ones_h)
};
let cfg = SearchConfig {
n_subsamples: self.n_subsamples,
control: self.control,
..SearchConfig::default()
};
let (coefficients, objective) = fast_resample(x, y, master_seed, &cfg, score, cstep)?;
let resid = y - &x.dot(&coefficients);
let mut subset = h_smallest_abs(&resid, h);
subset.sort_unstable(); let scale = Mad::default().scale(resid.as_slice().expect("contiguous residuals"))?;
let breakdown_point = (n - h + 1) as f64 / n as f64;
Ok(LtsFit {
coefficients,
scale,
residuals: resid,
subset,
objective,
coverage: h,
breakdown_point,
})
}
}
#[derive(Debug, Clone)]
pub struct LtsFit {
pub coefficients: Array1<f64>,
pub scale: Scale,
pub residuals: Array1<f64>,
pub subset: Vec<usize>,
pub objective: f64,
pub coverage: usize,
pub breakdown_point: f64,
}
impl LtsFit {
pub fn coefficients(&self) -> &Array1<f64> {
&self.coefficients
}
pub fn scale(&self) -> Scale {
self.scale
}
pub fn subset(&self) -> &[usize] {
&self.subset
}
pub fn breakdown_point(&self) -> f64 {
self.breakdown_point
}
}
fn h_smallest_abs(resid: &Array1<f64>, h: usize) -> Vec<usize> {
let mut idx: Vec<usize> = (0..resid.len()).collect();
idx.sort_by(|&a, &b| resid[a].abs().total_cmp(&resid[b].abs()));
idx.truncate(h);
idx
}