use crate::error::{StatsError, StatsResult};
use crate::regression::stat_tests::{f_test_p_value, t_test_p_value};
use crate::regression::utils::{calculate_std_errors, calculate_t_values, norm_ppf};
use crate::regression::{MultilinearRegressionResult, RegressionResults};
use scirs2_core::ndarray::{Array1, Array2, ArrayView1, ArrayView2};
use scirs2_core::numeric::Float;
use scirs2_linalg::{lstsq, svd};
#[allow(dead_code)]
pub fn multilinear_regression<F>(
x: &ArrayView2<F>,
y: &ArrayView1<F>,
) -> MultilinearRegressionResult<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.nrows() != y.len() {
return Err(StatsError::DimensionMismatch(format!(
"Input x has {} rows but y has length {}",
x.nrows(),
y.len()
)));
}
let (u, s, vt) = match svd(x, false, None) {
Ok(svd_result) => svd_result,
Err(e) => {
return Err(StatsError::ComputationError(format!(
"SVD computation failed: {:?}",
e
)))
}
};
let eps = crate::regression::utils::float_sqrt(F::epsilon());
let mut max_sv = F::zero();
for &val in s.iter() {
if val > max_sv {
max_sv = val;
}
}
let threshold = max_sv
* eps
* crate::regression::utils::float_sqrt(
F::from(std::cmp::max(x.nrows(), x.ncols())).expect("Operation failed"),
);
let rank = s.iter().filter(|&&val| val > threshold).count();
let beta = match lstsq(x, y, None) {
Ok(result) => result.x,
Err(_) => {
let uty = u.t().dot(y);
let mut s_inv_uty = Array1::<F>::zeros(s.len());
for i in 0..s.len() {
if s[i] > threshold {
s_inv_uty[i] = uty[i] / s[i];
}
}
vt.t().dot(&s_inv_uty)
}
};
let y_pred = x.dot(&beta);
let residuals = y
.iter()
.zip(y_pred.iter())
.map(|(&y_i, &y_pred_i)| y_i - y_pred_i)
.collect::<Array1<F>>();
Ok((beta, residuals, rank, s))
}
#[allow(dead_code)]
pub fn linear_regression<F>(
x: &ArrayView2<F>,
y: &ArrayView1<F>,
conf_level: Option<F>,
) -> 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.nrows() != y.len() {
return Err(StatsError::DimensionMismatch(format!(
"Input x has {} rows but y has length {}",
x.nrows(),
y.len()
)));
}
let n = x.nrows();
let p = x.ncols();
if n <= p {
return Err(StatsError::InvalidArgument(format!(
"Number of observations ({}) must be greater than number of predictors ({})",
n, p
)));
}
let conf_level_value =
conf_level.unwrap_or_else(|| F::from(0.95).expect("Failed to convert constant to float"));
let coefficients = match lstsq(x, y, None) {
Ok(result) => result.x,
Err(e) => {
return Err(StatsError::ComputationError(format!(
"Least squares computation failed: {:?}",
e
)));
}
};
let fitted_values = x.dot(&coefficients);
let residuals = y.to_owned() - &fitted_values;
let df_model = p - 1; let df_residuals = n - p;
let y_mean = y.iter().cloned().sum::<F>() / F::from(n).expect("Failed to convert to float");
let ss_total = y
.iter()
.map(|&yi| scirs2_core::numeric::Float::powi(yi - y_mean, 2))
.sum::<F>();
let ss_residual = residuals
.iter()
.map(|&ri| scirs2_core::numeric::Float::powi(ri, 2))
.sum::<F>();
let ss_explained = ss_total - ss_residual;
let r_squared = ss_explained / ss_total;
let adj_r_squared = F::one()
- (F::one() - r_squared) * F::from(n - 1).expect("Failed to convert to float")
/ F::from(df_residuals).expect("Failed to convert to float");
let mse = ss_residual / F::from(df_residuals).expect("Failed to convert to float");
let residual_std_error = scirs2_core::numeric::Float::sqrt(mse);
let std_errors = match calculate_std_errors(x, &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 mut conf_intervals = Array2::<F>::zeros((p, 2));
let z = norm_ppf(
F::from(0.5).expect("Failed to convert constant to float") * (F::one() + conf_level_value),
);
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("Failed to convert to float"))
/ (ss_residual / F::from(df_residuals).expect("Failed to convert to float"))
} 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], })
}
#[allow(dead_code)]
pub fn linregress<F>(x: &ArrayView1<F>, y: &ArrayView1<F>) -> StatsResult<(F, F, F, F, F)>
where
F: Float
+ std::iter::Sum<F>
+ std::ops::Div<Output = F>
+ std::fmt::Debug
+ 'static
+ std::fmt::Display
+ 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();
if n < 2 {
return Err(StatsError::InvalidArgument(
"At least 2 data points are required for linear regression".to_string(),
));
}
let x_mean = x.iter().cloned().sum::<F>() / F::from(n).expect("Failed to convert to float");
let y_mean = y.iter().cloned().sum::<F>() / F::from(n).expect("Failed to convert to float");
let mut ss_x = F::zero();
let mut ss_y = F::zero();
let mut ss_xy = F::zero();
for i in 0..n {
let x_diff = x[i] - x_mean;
let y_diff = y[i] - y_mean;
ss_x = ss_x + scirs2_core::numeric::Float::powi(x_diff, 2);
ss_y = ss_y + scirs2_core::numeric::Float::powi(y_diff, 2);
ss_xy = ss_xy + x_diff * y_diff;
}
if ss_x <= F::epsilon() {
return Err(StatsError::ComputationError(
"No variation in input x (x values are all identical)".to_string(),
));
}
let slope = ss_xy / ss_x;
let intercept = y_mean - slope * x_mean;
let r = ss_xy / scirs2_core::numeric::Float::sqrt(ss_x * ss_y);
let df = F::from(n - 2).expect("Failed to convert to float");
let residual_ss = ss_y - ss_xy * ss_xy / ss_x;
let std_err = scirs2_core::numeric::Float::sqrt(residual_ss / df)
/ scirs2_core::numeric::Float::sqrt(ss_x);
let t_stat = r * scirs2_core::numeric::Float::sqrt(df)
/ scirs2_core::numeric::Float::sqrt(F::one() - r * r);
let p_value = t_test_p_value(t_stat, n - 2);
Ok((slope, intercept, r, p_value, std_err))
}
#[allow(dead_code)]
pub fn odr<F>(
x: &ArrayView1<F>,
y: &ArrayView1<F>,
beta0: Option<[F; 2]>,
) -> StatsResult<(Array1<F>, Array1<F>, F)>
where
F: Float
+ std::iter::Sum<F>
+ std::ops::Div<Output = F>
+ std::fmt::Debug
+ 'static
+ std::fmt::Display
+ 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();
if n < 2 {
return Err(StatsError::InvalidArgument(
"At least 2 data points are required for orthogonal distance regression".to_string(),
));
}
let _beta0 = if let Some(beta) = beta0 {
[beta[0], beta[1]]
} else {
let (slope, intercept___, _, _, _) = linregress(x, y)?;
[intercept___, slope]
};
let x_mean = x.iter().cloned().sum::<F>() / F::from(n).expect("Failed to convert to float");
let y_mean = y.iter().cloned().sum::<F>() / F::from(n).expect("Failed to convert to float");
let x_centered: Vec<F> = x.iter().map(|&xi| xi - x_mean).collect();
let y_centered: Vec<F> = y.iter().map(|&yi| yi - y_mean).collect();
let mut s_xx = F::zero();
let mut s_yy = F::zero();
let mut s_xy = F::zero();
for i in 0..n {
s_xx = s_xx + scirs2_core::numeric::Float::powi(x_centered[i], 2);
s_yy = s_yy + scirs2_core::numeric::Float::powi(y_centered[i], 2);
s_xy = s_xy + x_centered[i] * y_centered[i];
}
let discriminant = scirs2_core::numeric::Float::powi(s_yy - s_xx, 2)
+ F::from(4.0).expect("Failed to convert constant to float")
* scirs2_core::numeric::Float::powi(s_xy, 2);
let slope = if s_xy.abs() > F::epsilon() {
(s_yy - s_xx + scirs2_core::numeric::Float::sqrt(discriminant))
/ (F::from(2.0).expect("Failed to convert constant to float") * s_xy)
} else if s_yy > s_xx {
F::infinity() } else {
F::zero() };
let intercept = y_mean - slope * x_mean;
let mut residuals = Array1::zeros(n);
let mut eps_total = F::zero();
for i in 0..n {
let y_pred = intercept + slope * x[i];
let d = (y[i] - y_pred).abs(); residuals[i] = d;
eps_total = eps_total + scirs2_core::numeric::Float::powi(d, 2);
}
let mut beta = Array1::zeros(2);
beta[0] = intercept;
beta[1] = slope;
Ok((beta, residuals, eps_total))
}
pub struct FittedLinearRegression<F>
where
F: Float + std::fmt::Debug + std::fmt::Display + 'static,
{
inner: RegressionResults<F>,
}
impl<F> FittedLinearRegression<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,
{
pub fn predict(
&self,
x: &scirs2_core::ndarray::ArrayView2<F>,
) -> StatsResult<scirs2_core::ndarray::Array1<F>> {
if x.ncols() != self.inner.coefficients.len() {
return Err(StatsError::DimensionMismatch(format!(
"predict: x has {} columns but model has {} coefficients",
x.ncols(),
self.inner.coefficients.len()
)));
}
Ok(x.dot(&self.inner.coefficients))
}
pub fn coefficients(&self) -> &scirs2_core::ndarray::Array1<F> {
&self.inner.coefficients
}
pub fn r_squared(&self) -> F {
self.inner.r_squared
}
}
#[derive(Debug, Clone, Default)]
pub struct LinearRegression {
_private: (),
}
impl LinearRegression {
pub fn new() -> Self {
Self { _private: () }
}
pub fn fit(
&mut self,
x: &scirs2_core::ndarray::ArrayView2<f64>,
y: &scirs2_core::ndarray::ArrayView1<f64>,
) -> StatsResult<FittedLinearRegression<f64>> {
let inner = linear_regression(x, y, None)?;
Ok(FittedLinearRegression { inner })
}
}
#[cfg(test)]
mod multilinear_regression_fabrication_fix_tests {
use super::*;
use scirs2_core::ndarray::{array, Array2};
#[test]
fn test_rank_deficient_input_returns_real_min_norm_solution_not_fabricated() {
let x = Array2::from_shape_vec(
(5, 3),
vec![
1.0, 0.0, 1.0, 1.0, 1.0, 2.0, 1.0, 2.0, 3.0, 1.0, 3.0, 4.0, 1.0, 4.0, 5.0,
],
)
.expect("shape ok");
let y = array![10.0_f64, 15.0, 20.0, 25.0, 30.0];
let (coeffs, residuals, rank, _) =
multilinear_regression(&x.view(), &y.view()).expect("regression should succeed");
assert_eq!(
rank, 2,
"design matrix should be detected as rank-deficient"
);
assert!(
(coeffs[0] - 5.0).abs() < 1e-6,
"expected intercept ~= 5.0, got {}",
coeffs[0]
);
assert!(
coeffs[1].abs() < 1e-6,
"expected x1 coefficient ~= 0.0, got {}",
coeffs[1]
);
assert!(
(coeffs[2] - 5.0).abs() < 1e-6,
"expected x2 coefficient ~= 5.0, got {}",
coeffs[2]
);
assert!(
(coeffs[0] - 1.0).abs() > 1.0,
"coefficients look suspiciously like the old fabricated [1, 2, 3]: {coeffs:?}"
);
for &r in residuals.iter() {
assert!(r.abs() < 1e-6, "expected near-zero residual, got {r}");
}
}
#[test]
fn test_full_rank_input_unaffected() {
let x = Array2::from_shape_vec((4, 2), vec![1.0, 1.0, 1.0, 2.0, 1.0, 3.0, 1.0, 4.0])
.expect("shape ok");
let y = array![3.0_f64, 5.0, 7.0, 9.0]; let (coeffs, _, rank, _) =
multilinear_regression(&x.view(), &y.view()).expect("regression should succeed");
assert_eq!(rank, 2);
assert!((coeffs[0] - 1.0).abs() < 1e-8);
assert!((coeffs[1] - 2.0).abs() < 1e-8);
}
}
#[cfg(test)]
mod linear_regression_struct_tests {
use super::*;
use scirs2_core::ndarray::{array, Array2};
fn make_simple_dataset() -> (Array2<f64>, scirs2_core::ndarray::Array1<f64>) {
let x = Array2::from_shape_vec(
(5, 2),
vec![1.0, 0.0, 1.0, 1.0, 1.0, 2.0, 1.0, 3.0, 1.0, 4.0],
)
.expect("shape ok");
let y = array![2.0_f64, 5.0, 8.0, 11.0, 14.0];
(x, y)
}
#[test]
fn test_linear_regression_is_pub() {
let _ = LinearRegression::new();
}
#[test]
fn test_linear_regression_fit() {
let (x, y) = make_simple_dataset();
let mut model = LinearRegression::new();
let result = model.fit(&x.view(), &y.view());
assert!(result.is_ok(), "fit should succeed: {:?}", result.err());
}
#[test]
fn test_linear_regression_predict_length() {
let (x, y) = make_simple_dataset();
let mut model = LinearRegression::new();
let fitted = model.fit(&x.view(), &y.view()).expect("fit ok");
let preds = fitted.predict(&x.view()).expect("predict ok");
assert_eq!(preds.len(), x.nrows());
}
#[test]
fn test_linear_regression_predict_accuracy() {
let (x, y) = make_simple_dataset();
let mut model = LinearRegression::new();
let fitted = model.fit(&x.view(), &y.view()).expect("fit ok");
let preds = fitted.predict(&x.view()).expect("predict ok");
for (p, t) in preds.iter().zip(y.iter()) {
assert!((p - t).abs() < 1e-6, "pred={p} target={t}");
}
}
}
#[cfg(test)]
mod fabrication_fix_tests {
use super::*;
use scirs2_core::ndarray::{array, Array2};
fn fixture_x() -> Array2<f64> {
let x1 = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
let x2 = [5.0, 3.0, 8.0, 2.0, 9.0, 4.0, 7.0, 1.0, 6.0, 10.0];
let n = x1.len();
let mut x = Array2::<f64>::zeros((n, 3));
for i in 0..n {
x[[i, 0]] = 1.0;
x[[i, 1]] = x1[i];
x[[i, 2]] = x2[i];
}
x
}
fn fixture_y_strong() -> scirs2_core::ndarray::Array1<f64> {
array![18.2, 13.85, 31.1, 14.75, 38.15, 24.9, 36.2, 19.8, 37.1, 50.85]
}
fn fixture_y_noise() -> scirs2_core::ndarray::Array1<f64> {
array![3.0, 7.0, 2.0, 9.0, 4.0, 8.0, 1.0, 6.0, 5.0, 10.0]
}
#[test]
fn test_std_errors_not_hardcoded_zero() {
let x = fixture_x();
let result = linear_regression(&x.view(), &fixture_y_noise().view(), None)
.expect("regression should succeed");
assert!(
result.std_errors.iter().any(|&se| se > 1e-6),
"expected non-degenerate standard errors for noisy data, got {:?}",
result.std_errors
);
}
#[test]
fn test_p_values_reflect_signal_vs_noise() {
let x = fixture_x();
let strong = linear_regression(&x.view(), &fixture_y_strong().view(), None)
.expect("regression should succeed");
for &p in strong.p_values.iter() {
assert!((0.0..=1.0).contains(&p), "p-value out of range: {p}");
}
assert!(
strong.p_values[1] < 0.01 && strong.p_values[2] < 0.01,
"strong-signal predictors should look significant, got {:?}",
strong.p_values
);
let noise = linear_regression(&x.view(), &fixture_y_noise().view(), None)
.expect("regression should succeed");
for &p in noise.p_values.iter() {
assert!((0.0..=1.0).contains(&p), "p-value out of range: {p}");
}
assert!(
noise.p_values[1] > 0.05 && noise.p_values[2] > 0.05,
"noise-only predictors should NOT look significant, got {:?}",
noise.p_values
);
}
#[test]
fn test_f_p_value_not_hardcoded_zero() {
let x = fixture_x();
let noise = linear_regression(&x.view(), &fixture_y_noise().view(), None)
.expect("regression should succeed");
assert!(
noise.f_p_value > 0.05,
"noise-only fit should not look significant, got {}",
noise.f_p_value
);
let strong = linear_regression(&x.view(), &fixture_y_strong().view(), None)
.expect("regression should succeed");
assert!(
strong.f_p_value < 0.01,
"strong-signal fit should look significant, got {}",
strong.f_p_value
);
}
#[test]
fn test_confidence_intervals_reflect_real_uncertainty_and_conf_level() {
let x = fixture_x();
let y = fixture_y_noise();
let result_95 =
linear_regression(&x.view(), &y.view(), Some(0.95)).expect("regression should succeed");
for i in 0..result_95.conf_intervals.nrows() {
let width = result_95.conf_intervals[[i, 1]] - result_95.conf_intervals[[i, 0]];
assert!(
width > 1e-6,
"confidence interval {i} looks fabricated (width={width})"
);
}
let result_99 =
linear_regression(&x.view(), &y.view(), Some(0.99)).expect("regression should succeed");
let width_95 = result_95.conf_intervals[[1, 1]] - result_95.conf_intervals[[1, 0]];
let width_99 = result_99.conf_intervals[[1, 1]] - result_99.conf_intervals[[1, 0]];
assert!(
width_99 > width_95,
"99% CI (width={width_99}) should be wider than 95% CI (width={width_95})"
);
}
#[test]
fn test_singular_5x3_input_returns_error_not_fabricated_coefficients() {
let x = Array2::from_shape_vec(
(5, 3),
vec![
1.0, 0.0, 1.0, 1.0, 1.0, 2.0, 1.0, 2.0, 3.0, 1.0, 3.0, 4.0, 1.0, 4.0, 5.0,
],
)
.expect("shape ok");
let y = array![100.0_f64, -50.0, 7.0, 0.0, 42.0];
match linear_regression(&x.view(), &y.view(), None) {
Err(_) => {}
Ok(r) => panic!(
"expected an honest error for a singular design matrix, got Ok(coefficients={:?})",
r.coefficients
),
}
}
}
#[cfg(test)]
mod linregress_p_value_fix_tests {
use super::*;
use approx::assert_relative_eq;
use scirs2_core::ndarray::array;
#[test]
fn test_linregress_p_value_matches_scipy_not_old_formula() {
let x = array![
1.0f64, 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,
16.0, 17.0, 18.0, 19.0, 20.0
];
let y = array![
-2.0f64, 7.0, -5.0, 6.0, -1.0, 15.0, 3.0, 15.0, 0.0, 13.0, 9.0, 18.0, 6.0, 18.0, 10.0,
24.0, 14.0, 24.0, 11.0, 25.0
];
let (slope, intercept, r, p, stderr) =
linregress(&x.view(), &y.view()).expect("linregress should succeed");
assert_relative_eq!(slope, 1.0977443609022557, max_relative = 1e-6);
assert_relative_eq!(intercept, -1.026315789473685, max_relative = 1e-6);
assert_relative_eq!(r, 0.7316462269260885, max_relative = 1e-6);
assert_relative_eq!(stderr, 0.24107227335572703, max_relative = 1e-4);
assert_relative_eq!(
p,
0.0002461432840693492,
max_relative = 1e-3,
epsilon = 1e-8
);
assert!(
p < 0.01,
"expected a highly significant p-value (~0.000246), got {p}"
);
assert!(
(p - 0.465).abs() > 0.1,
"p={p} looks suspiciously close to the old formula's ~0.465"
);
}
#[test]
fn test_linregress_p_value_noise_matches_scipy() {
let x = array![1.0f64, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
let y = array![5.0f64, 3.0, 8.0, 2.0, 9.0, 4.0, 7.0, 1.0, 6.0, 10.0];
let (_, _, _, p, _) = linregress(&x.view(), &y.view()).expect("linregress should succeed");
assert_relative_eq!(p, 0.48877630451924287, max_relative = 1e-3, epsilon = 1e-6);
assert!(
(p - 0.938).abs() > 0.1,
"p={p} looks suspiciously close to the old formula's ~0.938"
);
}
#[test]
fn test_linregress_p_value_in_valid_range() {
let x = array![1.0f64, 2.0, 3.0, 4.0, 5.0];
let y = array![2.0f64, 4.0, 6.0, 8.0, 10.0];
let (_, _, _, p, _) = linregress(&x.view(), &y.view()).expect("linregress should succeed");
assert!((0.0..=1.0).contains(&p), "p-value out of range: {p}");
}
}