pub(crate) fn loess(x: &[f64], y: &[f64], bandwidth: f64) -> (Vec<f64>, Vec<f64>) {
let n = x.len();
if n < 2 {
return (x.to_vec(), y.to_vec());
}
let k = (n as f64 * bandwidth).max(2.0).min(n as f64) as usize;
let mut smoothed_y = Vec::with_capacity(n);
let mut distances: Vec<(usize, f64)> = Vec::with_capacity(n);
for i in 0..n {
let target_x = x[i];
distances.clear();
for (j, &val) in x.iter().enumerate() {
distances.push((j, (val - target_x).abs()));
}
let (neighbors, _, _) = distances.select_nth_unstable_by(k - 1, |a, b| {
a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal)
});
let max_dist = neighbors.iter().map(|d| d.1).fold(0.0, f64::max);
if max_dist == 0.0 {
smoothed_y.push(y[i]);
continue;
}
if let Some(pred) =
weighted_linear_regression_optimized(x, y, neighbors, max_dist, target_x)
{
smoothed_y.push(pred);
} else {
smoothed_y.push(y[i]);
}
}
(x.to_vec(), smoothed_y)
}
fn weighted_linear_regression_optimized(
original_x: &[f64],
original_y: &[f64],
neighbors: &[(usize, f64)],
max_dist: f64,
target_x: f64,
) -> Option<f64> {
let mut sum_w = 0.0;
let mut sum_wx = 0.0;
let mut sum_wy = 0.0;
let mut sum_wxx = 0.0;
let mut sum_wxy = 0.0;
for &(idx, dist) in neighbors {
let u = dist / max_dist;
let w = (1.0 - u.powi(3)).powi(3).max(0.0);
let xi = original_x[idx];
let yi = original_y[idx];
sum_w += w;
sum_wx += w * xi;
sum_wy += w * yi;
sum_wxx += w * xi * xi;
sum_wxy += w * xi * yi;
}
if sum_w == 0.0 {
return None;
}
let denom = sum_w * sum_wxx - sum_wx * sum_wx;
if denom.abs() < 1e-12 {
return Some(sum_wy / sum_w);
}
let slope = (sum_w * sum_wxy - sum_wx * sum_wy) / denom;
let intercept = (sum_wy - slope * sum_wx) / sum_w;
Some(intercept + slope * target_x)
}