pub fn loess_series(xs: &[f64], ys: &[f64], span: f64) -> Vec<f64> {
let n = xs.len();
if n == 0 {
return vec![];
}
let span = span.clamp(1.0 / n as f64, 1.0);
let window = ((n as f64 * span).ceil() as usize).max(2);
let mut yhat = vec![0.0; n];
for i in 0..n {
let mut idx: Vec<usize> = (0..n).collect();
idx.sort_by(|&a, &b| {
(xs[a] - xs[i])
.abs()
.partial_cmp(&(xs[b] - xs[i]).abs())
.unwrap()
});
let idxw = &idx[..window];
let max_d = (xs[*idxw.last().unwrap()] - xs[i]).abs();
#[allow(clippy::similar_names)]
let mut sw = 0.0;
#[allow(clippy::similar_names)]
let mut swx = 0.0;
#[allow(clippy::similar_names)]
let mut swy = 0.0;
#[allow(clippy::similar_names)]
let mut swxx = 0.0;
#[allow(clippy::similar_names)]
let mut swxy = 0.0;
for &j in idxw {
let d = (xs[j] - xs[i]).abs();
let u = if max_d == 0.0 {
0.0
} else {
(d / max_d).min(1.0)
};
let w = (1.0 - u * u * u).powi(3);
sw += w;
swx += w * xs[j];
swy += w * ys[j];
swxx += w * xs[j] * xs[j];
swxy += w * xs[j] * ys[j];
}
let denom = sw * swxx - swx * swx;
if denom.abs() < 1e-12 {
yhat[i] = swy / sw.max(1e-12);
} else {
let b = (sw * swxy - swx * swy) / denom;
let a = (swy - b * swx) / sw;
yhat[i] = a + b * xs[i];
}
}
yhat
}