#![allow(clippy::needless_range_loop)]
use crate::{error::Error, linalg::Matrix};
use super::{
neighbors::{compute_bandwidth, euclidean_distance},
normalize::normalize_predictors,
types::{LoessFit, LoessOptions},
weights::MIN_NEIGHBORS_QUADRATIC,
wls::weighted_least_squares,
};
pub fn fit_at_point_impl(
query: &[f64],
x_normalized: &Matrix,
y: &[f64],
options: &LoessOptions,
robust_weights: Option<&[f64]>,
) -> Result<f64, Error> {
let p = x_normalized.cols;
if options.degree == 0 {
return fit_at_point_degree_0(query, x_normalized, y, options, robust_weights);
}
if p == 1 && options.degree == 1 {
return fit_at_point_1d_linear(query, x_normalized, y, options, robust_weights);
}
fit_at_point_general(query, x_normalized, y, options, robust_weights)
}
fn fit_at_point_1d_linear(
query: &[f64],
x_normalized: &Matrix,
y: &[f64],
options: &LoessOptions,
robust_weights: Option<&[f64]>,
) -> Result<f64, Error> {
let n = x_normalized.rows;
let query_value = query[0];
let mut x_min = f64::INFINITY;
let mut x_max = f64::NEG_INFINITY;
for i in 0..n {
let xi = x_normalized.get(i, 0);
if xi < x_min { x_min = xi; }
if xi > x_max { x_max = xi; }
}
let range = x_max - x_min;
let (bandwidth, neighbors) = compute_bandwidth(query, x_normalized, options.span, options.degree);
let k = neighbors.len();
let mut weighted_data: Vec<(usize, f64, f64)> = Vec::with_capacity(k);
for &idx in &neighbors {
let xi = x_normalized.get(idx, 0);
let r = (xi - query_value).abs();
let w = if bandwidth <= 0.0 {
1.0
} else {
let outer_boundary = 0.999 * bandwidth;
let inner_boundary = 0.001 * bandwidth;
if r > outer_boundary {
0.0
} else if r <= inner_boundary {
1.0
} else {
let normalized_distance = r / bandwidth;
let tricube_inner = 1.0 - normalized_distance.powi(3);
tricube_inner.powi(3)
}
};
if w > 0.0 {
weighted_data.push((idx, xi, w));
}
}
if let Some(rw) = robust_weights {
for data in &mut weighted_data {
data.2 *= rw[data.0];
}
}
let mut sum_w = 0.0;
for (_, _, w) in &weighted_data {
sum_w += w;
}
if sum_w <= 0.0 {
let nearest_idx = neighbors[0];
return Ok(y[nearest_idx]);
}
for data in &mut weighted_data {
data.2 /= sum_w;
}
if bandwidth > 0.0 {
let mut weighted_center = 0.0;
for (_, xi, w) in &weighted_data {
weighted_center += w * xi;
}
let mut center_offset = query_value - weighted_center;
let mut weighted_variance = 0.0;
for (_, xi, w) in &weighted_data {
let diff = xi - weighted_center;
weighted_variance += w * diff * diff;
}
if range > 0.0 && weighted_variance.sqrt() > 0.001 * range {
center_offset /= weighted_variance;
for data in &mut weighted_data {
let xi = data.1;
data.2 *= center_offset * (xi - weighted_center) + 1.0;
}
}
}
let mut ys = 0.0;
for (idx, _, w) in &weighted_data {
ys += w * y[*idx];
}
Ok(ys)
}
fn fit_at_point_general(
query: &[f64],
x_normalized: &Matrix,
y: &[f64],
options: &LoessOptions,
robust_weights: Option<&[f64]>,
) -> Result<f64, Error> {
let p = x_normalized.cols;
let (bandwidth, neighbors) = compute_bandwidth(query, x_normalized, options.span, options.degree);
let k = neighbors.len();
let mut weights = Vec::with_capacity(k);
for &idx in &neighbors {
let mut neighbor_point = Vec::with_capacity(p);
for j in 0..p {
neighbor_point.push(x_normalized.get(idx, j));
}
let dist = euclidean_distance(query, &neighbor_point);
let w = if bandwidth <= 0.0 {
1.0
} else {
let outer_boundary = 0.999 * bandwidth;
let inner_boundary = 0.001 * bandwidth;
if dist > outer_boundary {
0.0
} else if dist <= inner_boundary {
1.0
} else {
let normalized_distance = dist / bandwidth;
let tricube_inner = 1.0 - normalized_distance.powi(3);
tricube_inner.powi(3)
}
};
weights.push(w);
}
if let Some(rw) = robust_weights {
for i in 0..k {
weights[i] *= rw[neighbors[i]];
}
}
let n_poly_terms = if options.degree == 1 {
1 + p
} else {
1 + p + p + p * (p - 1) / 2
};
if k < n_poly_terms {
return Err(Error::InsufficientData {
required: n_poly_terms,
available: k,
});
}
let mut local_x_data = Vec::with_capacity(k * n_poly_terms);
let mut local_y = Vec::with_capacity(k);
for &idx in &neighbors {
local_y.push(y[idx]);
local_x_data.push(1.0);
for j in 0..p {
local_x_data.push(x_normalized.get(idx, j));
}
if options.degree == 2 {
for j1 in 0..p {
let val = x_normalized.get(idx, j1);
local_x_data.push(val * val);
}
for j1 in 0..p {
for j2 in (j1 + 1)..p {
let v1 = x_normalized.get(idx, j1);
let v2 = x_normalized.get(idx, j2);
local_x_data.push(v1 * v2);
}
}
}
}
let local_x = Matrix::new(k, n_poly_terms, local_x_data);
let coeffs = weighted_least_squares(&local_x, &local_y, &weights)?;
evaluate_polynomial(&coeffs, query, p, options.degree)
}
pub fn fit_at_point(
query: &[f64],
x_normalized: &Matrix,
y: &[f64],
options: &LoessOptions,
) -> Result<f64, Error> {
fit_at_point_impl(query, x_normalized, y, options, None)
}
fn fit_at_point_degree_0(
query: &[f64],
x_normalized: &Matrix,
y: &[f64],
options: &LoessOptions,
robust_weights: Option<&[f64]>,
) -> Result<f64, Error> {
let (bandwidth, neighbors) = compute_bandwidth(query, x_normalized, options.span, options.degree);
let k = neighbors.len();
let mut weights = Vec::with_capacity(k);
for &idx in &neighbors {
let p = x_normalized.cols;
let mut neighbor_point = Vec::with_capacity(p);
for j in 0..p {
neighbor_point.push(x_normalized.get(idx, j));
}
let dist = euclidean_distance(query, &neighbor_point);
let w = if bandwidth <= 0.0 {
1.0
} else {
let outer_boundary = 0.999 * bandwidth;
let inner_boundary = 0.001 * bandwidth;
if dist > outer_boundary {
0.0
} else if dist <= inner_boundary {
1.0
} else {
let normalized_distance = dist / bandwidth;
let tricube_inner = 1.0 - normalized_distance.powi(3);
tricube_inner.powi(3)
}
};
weights.push(w);
}
if let Some(rw) = robust_weights {
for i in 0..k {
weights[i] *= rw[neighbors[i]];
}
}
fit_weighted_average(&neighbors.iter().map(|&idx| y[idx]).collect::<Vec<_>>(), &weights)
}
fn fit_weighted_average(values: &[f64], weights: &[f64]) -> Result<f64, Error> {
let weight_sum: f64 = weights.iter().sum();
if weight_sum <= 0.0 {
return Err(Error::InvalidInput("All weights are zero".to_string()));
}
let weighted_sum: f64 = values.iter().zip(weights.iter()).map(|(v, w)| v * w).sum();
Ok(weighted_sum / weight_sum)
}
fn evaluate_polynomial(
coeffs: &[f64],
query: &[f64],
p: usize,
degree: usize,
) -> Result<f64, Error> {
let mut fitted_value = coeffs[0];
let mut coeff_idx = 1;
for j in 0..p {
fitted_value += coeffs[coeff_idx] * query[j];
coeff_idx += 1;
}
if degree == 2 {
for j in 0..p {
fitted_value += coeffs[coeff_idx] * query[j] * query[j];
coeff_idx += 1;
}
for j1 in 0..p {
for j2 in (j1 + 1)..p {
fitted_value += coeffs[coeff_idx] * query[j1] * query[j2];
coeff_idx += 1;
}
}
}
Ok(fitted_value)
}
impl LoessFit {
pub fn predict(
&self,
new_x: &[Vec<f64>],
original_x: &[Vec<f64>],
original_y: &[f64],
options: &LoessOptions,
) -> Result<Vec<f64>, Error> {
if options.span != self.span {
return Err(Error::InvalidInput(format!(
"Span mismatch: fit used {}, prediction uses {}",
self.span, options.span
)));
}
if options.degree != self.degree {
return Err(Error::InvalidInput(format!(
"Degree mismatch: fit used {}, prediction uses {}",
self.degree, options.degree
)));
}
if options.robust_iterations != self.robust_iterations {
return Err(Error::InvalidInput(format!(
"Robust iterations mismatch: fit used {}, prediction uses {}",
self.robust_iterations, options.robust_iterations
)));
}
let n_train = original_y.len();
let p = original_x.len();
let m = if new_x.is_empty() {
return Ok(Vec::new());
} else {
new_x[0].len()
};
let min_required = if options.degree == 2 {
MIN_NEIGHBORS_QUADRATIC
} else {
2
};
if n_train < min_required {
return Err(Error::InsufficientData {
required: min_required,
available: n_train,
});
}
if p == 0 {
return Err(Error::InvalidInput(
"At least one predictor variable is required".to_string(),
));
}
for (i, x_var) in new_x.iter().enumerate() {
if x_var.len() != m {
return Err(Error::InvalidInput(format!(
"new_x[{}] has {} elements, expected {}",
i,
x_var.len(),
m
)));
}
if original_x[i].len() != n_train {
return Err(Error::InvalidInput(format!(
"original_x[{}] has {} elements, expected {}",
i,
original_x[i].len(),
n_train
)));
}
}
let mut train_x_data = Vec::with_capacity(n_train * p);
for i in 0..n_train {
for j in 0..p {
train_x_data.push(original_x[j][i]);
}
}
let train_x_matrix = Matrix::new(n_train, p, train_x_data);
let train_x_normalized = if p == 1 {
train_x_matrix.clone()
} else {
let (normalized, _) = normalize_predictors(&train_x_matrix);
normalized
};
let mut predictions = Vec::with_capacity(m);
for i in 0..m {
let query_normalized = if p == 1 {
let mut qn = Vec::with_capacity(p);
for j in 0..p {
qn.push(new_x[j][i]);
}
qn
} else {
let mut qn = Vec::with_capacity(p);
for j in 0..p {
let query_val = new_x[j][i];
let mut col_min = f64::INFINITY;
let mut col_max = f64::NEG_INFINITY;
for row in 0..n_train {
let val = original_x[j][row];
if val < col_min {
col_min = val;
}
if val > col_max {
col_max = val;
}
}
let col_range = col_max - col_min;
let normalized_val = if col_range <= f64::EPSILON {
0.5
} else {
(query_val - col_min) / col_range
};
qn.push(normalized_val);
}
qn
};
let pred = fit_at_point(&query_normalized, &train_x_normalized, original_y, options)?;
predictions.push(pred);
}
Ok(predictions)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::loess::normalize::normalize_predictors;
#[test]
fn test_fit_at_point_simple_linear() {
let x_data = vec![0.0, 1.0, 2.0, 3.0, 4.0, 5.0];
let y_data = vec![1.0, 3.0, 5.0, 7.0, 9.0, 11.0];
let x_matrix = Matrix::new(6, 1, x_data);
let (x_normalized, _) = normalize_predictors(&x_matrix);
let mut options = LoessOptions::default();
options.degree = 1;
options.span = 0.75;
let query = vec![0.5]; let fitted = fit_at_point(&query, &x_normalized, &y_data, &options).unwrap();
assert!((fitted - 6.0).abs() < 1.0);
}
#[test]
fn test_fit_at_point_quadratic() {
let x_data = vec![0.0, 1.0, 2.0, 3.0, 4.0, 5.0];
let y_data: Vec<f64> = x_data.iter().map(|x| x * x).collect();
let x_matrix = Matrix::new(6, 1, x_data);
let (x_normalized, _) = normalize_predictors(&x_matrix);
let mut options = LoessOptions::default();
options.degree = 2;
options.span = 1.0;
let query = vec![0.5];
let fitted = fit_at_point(&query, &x_normalized, &y_data, &options).unwrap();
assert!((fitted - 6.25).abs() < 1.0);
}
#[test]
fn test_fit_at_point_degree_0() {
let x_data = vec![0.0, 1.0, 2.0, 3.0, 4.0, 5.0];
let y_data = vec![5.0; 6];
let x_matrix = Matrix::new(6, 1, x_data);
let (x_normalized, _) = normalize_predictors(&x_matrix);
let mut options = LoessOptions::default();
options.degree = 0;
options.span = 0.5;
let query = vec![0.5];
let fitted = fit_at_point(&query, &x_normalized, &y_data, &options).unwrap();
assert!((fitted - 5.0).abs() < 1.0);
}
#[test]
fn test_fit_weighted_average() {
let values = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let weights = vec![1.0, 1.0, 1.0, 1.0, 1.0];
let result = fit_weighted_average(&values, &weights).unwrap();
assert_eq!(result, 3.0);
let weights2 = vec![0.0, 0.0, 1.0, 0.0, 0.0];
let result2 = fit_weighted_average(&values, &weights2).unwrap();
assert_eq!(result2, 3.0); }
#[test]
fn test_fit_weighted_average_zero_weights() {
let values = vec![1.0, 2.0, 3.0];
let weights = vec![0.0, 0.0, 0.0];
let result = fit_weighted_average(&values, &weights);
assert!(result.is_err());
}
#[test]
fn test_evaluate_polynomial_linear() {
let coeffs = vec![2.0, 3.0];
let query = vec![4.0];
let result = evaluate_polynomial(&coeffs, &query, 1, 1).unwrap();
assert_eq!(result, 14.0); }
#[test]
fn test_evaluate_polynomial_quadratic_1d() {
let coeffs = vec![1.0, 2.0, 3.0];
let query = vec![2.0];
let result = evaluate_polynomial(&coeffs, &query, 1, 2).unwrap();
assert_eq!(result, 17.0); }
#[test]
fn test_evaluate_polynomial_quadratic_2d() {
let coeffs = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
let query = vec![1.0, 2.0];
let result = evaluate_polynomial(&coeffs, &query, 2, 2).unwrap();
assert_eq!(result, 45.0);
}
#[test]
fn test_fit_at_point_degree_0_direct() {
let x_data = vec![0.0, 1.0, 2.0, 3.0, 4.0];
let y_data = vec![5.0, 7.0, 9.0, 11.0, 13.0];
let x_matrix = Matrix::new(5, 1, x_data);
let (x_normalized, _) = normalize_predictors(&x_matrix);
let mut options = LoessOptions::default();
options.degree = 0;
options.span = 0.6;
let query = vec![0.5];
let fitted = fit_at_point_degree_0(&query, &x_normalized, &y_data, &options, None).unwrap();
assert!(fitted.is_finite());
}
#[test]
fn test_fit_at_point_degree_0_with_robust_weights() {
let x_data = vec![0.0, 1.0, 2.0, 3.0, 4.0];
let y_data = vec![5.0, 7.0, 9.0, 11.0, 13.0];
let robust_weights = vec![1.0, 0.5, 1.0, 0.5, 1.0];
let x_matrix = Matrix::new(5, 1, x_data);
let (x_normalized, _) = normalize_predictors(&x_matrix);
let mut options = LoessOptions::default();
options.degree = 0;
options.span = 0.6;
let query = vec![0.5];
let fitted = fit_at_point_degree_0(&query, &x_normalized, &y_data, &options, Some(&robust_weights)).unwrap();
assert!(fitted.is_finite());
}
#[test]
fn test_fit_at_point_1d_linear_edge_case_bandwidth_zero() {
let x_data = vec![0.0, 1.0, 2.0, 3.0, 4.0];
let y_data = vec![1.0, 3.0, 5.0, 7.0, 9.0];
let x_matrix = Matrix::new(5, 1, x_data);
let (x_normalized, _) = normalize_predictors(&x_matrix);
let mut options = LoessOptions::default();
options.degree = 1;
options.span = 0.75;
let query = vec![0.5];
let result = fit_at_point_1d_linear(&query, &x_normalized, &y_data, &options, None);
assert!(result.is_ok());
}
#[test]
fn test_fit_at_point_1d_linear_all_zero_weights() {
let x_data = vec![0.0, 1.0, 2.0, 3.0, 4.0];
let y_data = vec![1.0, 3.0, 5.0, 7.0, 9.0];
let x_matrix = Matrix::new(5, 1, x_data);
let (x_normalized, _) = normalize_predictors(&x_matrix);
let mut options = LoessOptions::default();
options.degree = 1;
options.span = 0.5;
let query = vec![0.5];
let result = fit_at_point_1d_linear(&query, &x_normalized, &y_data, &options, None);
assert!(result.is_ok());
}
#[test]
fn test_fit_at_point_1d_linear_small_variance_fallback() {
let x_data = vec![1.0, 1.001, 0.999, 1.0, 1.001]; let y_data = vec![2.0, 4.0, 6.0, 8.0, 10.0];
let x_matrix = Matrix::new(5, 1, x_data);
let (x_normalized, _) = normalize_predictors(&x_matrix);
let mut options = LoessOptions::default();
options.degree = 1;
options.span = 1.0;
let query = vec![0.5]; let fitted = fit_at_point_1d_linear(&query, &x_normalized, &y_data, &options, None).unwrap();
assert!(fitted.is_finite());
}
#[test]
fn test_fit_at_point_impl_multivariate_dispatch() {
let x_data = vec![0.0, 0.5, 1.0, 0.0, 0.5, 1.0];
let y_data = vec![1.0, 3.0, 5.0, 2.0, 4.0, 6.0];
let x_matrix = Matrix::new(3, 2, x_data);
let (x_normalized, _x_info) = normalize_predictors(&x_matrix);
let mut options = LoessOptions::default();
options.degree = 1;
options.span = 1.0;
let query = vec![0.5, 0.5];
let fitted = fit_at_point(&query, &x_normalized, &y_data, &options).unwrap();
assert!(fitted.is_finite());
}
}