use crate::error::{StatsError, StatsResult};
use crate::regression::stat_tests::{f_test_p_value, t_test_p_value};
use crate::regression::utils::*;
use crate::regression::RegressionResults;
use scirs2_core::ndarray::{Array1, Array2, ArrayView1};
use scirs2_core::numeric::Float;
use scirs2_linalg::lstsq;
#[allow(dead_code)]
pub fn polyfit<F>(
x: &ArrayView1<F>,
y: &ArrayView1<F>,
deg: usize,
) -> StatsResult<RegressionResults<F>>
where
F: Float
+ std::iter::Sum<F>
+ std::ops::Div<Output = F>
+ std::fmt::Debug
+ std::fmt::Display
+ 'static
+ scirs2_core::numeric::NumAssign
+ scirs2_core::numeric::One
+ scirs2_core::ndarray::ScalarOperand
+ Send
+ Sync,
{
if x.len() != y.len() {
return Err(StatsError::DimensionMismatch(format!(
"Input x has length {} but y has length {}",
x.len(),
y.len()
)));
}
let n = x.len();
let p = deg + 1;
if n <= deg {
return Err(StatsError::InvalidArgument(format!(
"Number of data points ({}) must be greater than polynomial degree ({})",
n, deg
)));
}
let mut vandermonde = Array2::<F>::zeros((n, p));
for i in 0..n {
vandermonde[[i, 0]] = F::one();
for j in 1..=deg {
vandermonde[[i, j]] = scirs2_core::numeric::Float::powi(x[i], j as i32);
}
}
let coefficients = match lstsq(&vandermonde.view(), y, None) {
Ok(result) => result.x,
Err(e) => {
return Err(StatsError::ComputationError(format!(
"Least squares computation failed: {:?}",
e
)));
}
};
let fitted_values = vandermonde.dot(&coefficients);
let residuals = y.to_owned() - &fitted_values;
let df_model = p - 1;
let df_residuals = n - p;
let (_y_mean, ss_total, ss_residual, ss_explained) =
calculate_sum_of_squares(y, &residuals.view());
let r_squared = ss_explained / ss_total;
let adj_r_squared = F::one()
- (F::one() - r_squared) * F::from(n - 1).expect("Operation failed")
/ F::from(df_residuals).expect("Operation failed");
let mse = ss_residual / F::from(df_residuals).expect("Operation failed");
let residual_std_error = scirs2_core::numeric::Float::sqrt(mse);
let std_errors =
match calculate_std_errors(&vandermonde.view(), &residuals.view(), df_residuals) {
Ok(se) => se,
Err(_) => Array1::<F>::zeros(p),
};
let t_values = calculate_t_values(&coefficients, &std_errors);
let p_values = t_values.mapv(|t| t_test_p_value(t, df_residuals));
let z = norm_ppf(F::from(0.975).expect("Operation failed"));
let mut conf_intervals = Array2::<F>::zeros((p, 2));
for i in 0..p {
let margin = std_errors[i] * z;
conf_intervals[[i, 0]] = coefficients[i] - margin;
conf_intervals[[i, 1]] = coefficients[i] + margin;
}
let f_statistic = if df_model > 0 && df_residuals > 0 {
(ss_explained / F::from(df_model).expect("Operation failed"))
/ (ss_residual / F::from(df_residuals).expect("Operation failed"))
} else {
F::infinity()
};
let f_p_value = f_test_p_value(f_statistic, df_model, df_residuals);
Ok(RegressionResults {
coefficients,
std_errors,
t_values,
p_values,
conf_intervals,
r_squared,
adj_r_squared,
f_statistic,
f_p_value,
residual_std_error,
df_residuals,
residuals,
fitted_values,
inlier_mask: vec![true; n], })
}
#[cfg(test)]
mod f_p_value_and_p_values_fix_tests {
use super::*;
use approx::assert_relative_eq;
use scirs2_core::ndarray::array;
#[test]
fn test_polyfit_degree1_strong_signal_f_p_value_and_p_values_match_scipy() {
let x = array![0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0];
let y = array![2.1, 3.9, 5.8, 8.2, 9.7, 12.3, 13.5, 16.4, 17.9, 20.5];
let result = polyfit(&x.view(), &y.view(), 1).expect("polyfit should succeed");
assert_relative_eq!(result.f_statistic, 3157.3741037536925, max_relative = 1e-4);
assert_relative_eq!(
result.f_p_value,
1.1167535971635081e-11,
max_relative = 1e-3,
epsilon = 1e-14
);
assert!((0.0..=1.0).contains(&result.f_p_value));
assert_relative_eq!(
result.p_values[0],
9.868959309924845e-06,
max_relative = 1e-3
);
assert_relative_eq!(
result.p_values[1],
1.1167535971635009e-11,
max_relative = 1e-3,
epsilon = 1e-14
);
for &p in result.p_values.iter() {
assert!((0.0..=1.0).contains(&p), "p-value out of range: {p}");
}
}
#[test]
fn test_polyfit_degree1_flat_data_distinguishes_signal_from_noise() {
let x = array![
0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0
];
let y =
array![5.1, 4.8, 5.3, 4.9, 5.2, 5.0, 4.7, 5.4, 5.1, 4.9, 5.0, 5.2, 4.8, 5.3, 5.0, 4.9];
let result = polyfit(&x.view(), &y.view(), 1).expect("polyfit should succeed");
assert_relative_eq!(
result.f_statistic,
0.016690510252749525,
max_relative = 1e-3
);
assert_relative_eq!(
result.f_p_value,
0.8990436185686632,
max_relative = 1e-3,
epsilon = 1e-6
);
assert!(
result.f_p_value > 0.05,
"expected a large, non-significant p-value, got {}",
result.f_p_value
);
assert!(result.p_values[0] < 1e-9);
assert_relative_eq!(
result.p_values[1],
0.8990436185686801,
max_relative = 1e-3,
epsilon = 1e-6
);
assert!(
result.p_values[1] > 0.05,
"expected a non-significant p-value for the (noise-only) slope, got {}",
result.p_values[1]
);
for &p in result.p_values.iter() {
assert!((0.0..=1.0).contains(&p), "p-value out of range: {p}");
}
}
#[test]
fn test_polyfit_degree2_f_p_value_and_p_values_match_scipy() {
let x = array![0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0];
let y = array![1.2, 2.8, 6.5, 11.7, 18.9, 27.6, 38.4, 50.8, 65.3, 80.9, 99.1, 119.0];
let result = polyfit(&x.view(), &y.view(), 2).expect("polyfit should succeed");
assert_relative_eq!(result.f_statistic, 362654.7065548911, max_relative = 1e-3);
assert!(result.f_p_value < 1e-9);
assert!((0.0..=1.0).contains(&result.f_p_value));
assert_relative_eq!(
result.p_values[0],
2.094418202299678e-06,
max_relative = 1e-3
);
assert_relative_eq!(
result.p_values[1],
5.6845212254180934e-08,
max_relative = 1e-3
);
assert!(result.p_values[2] < 1e-9);
for &p in result.p_values.iter() {
assert!((0.0..=1.0).contains(&p), "p-value out of range: {p}");
}
}
#[test]
fn test_polyfit_conf_intervals_use_95pct_normal_margin_not_bare_std_error() {
let x = array![0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0];
let y = array![2.1, 3.9, 5.8, 8.2, 9.7, 12.3, 13.5, 16.4, 17.9, 20.5];
let result = polyfit(&x.view(), &y.view(), 1).expect("polyfit should succeed");
assert_relative_eq!(
result.conf_intervals[[0, 0]],
1.5126464415195235,
max_relative = 1e-6
);
assert_relative_eq!(
result.conf_intervals[[0, 1]],
2.2691717402986518,
max_relative = 1e-6
);
assert_relative_eq!(
result.conf_intervals[[1, 0]],
1.9600540048151793,
max_relative = 1e-6
);
assert_relative_eq!(
result.conf_intervals[[1, 1]],
2.1017641770030058,
max_relative = 1e-6
);
let half_width_1 = result.conf_intervals[[1, 1]] - result.coefficients[1];
assert_relative_eq!(
half_width_1 / result.std_errors[1],
1.9603949169253396,
max_relative = 1e-6
);
}
}