mod linalg;
use linalg::{centered, column_means, mean, solve};
#[derive(Debug, Clone, PartialEq)]
pub struct LinearModel {
intercept: f64,
coefficients: Vec<f64>,
}
impl LinearModel {
#[must_use]
pub const fn intercept(&self) -> f64 {
self.intercept
}
#[must_use]
pub fn coefficients(&self) -> &[f64] {
&self.coefficients
}
#[must_use]
pub fn predict(&self, row: &[f64]) -> f64 {
self.coefficients
.iter()
.zip(row)
.fold(self.intercept, |acc, (&beta, &x)| beta.mul_add(x, acc))
}
#[must_use]
pub fn r_squared(&self, x: &[Vec<f64>], y: &[f64]) -> f64 {
if y.is_empty() {
return 0.0;
}
let y_mean = mean(y);
let mut ss_res = 0.0_f64;
let mut ss_tot = 0.0_f64;
for (row, &yi) in x.iter().zip(y) {
let resid = yi - self.predict(row);
ss_res = resid.mul_add(resid, ss_res);
let dev = yi - y_mean;
ss_tot = dev.mul_add(dev, ss_tot);
}
if ss_tot <= 0.0 {
return if ss_res <= 0.0 { 1.0 } else { 0.0 };
}
1.0 - ss_res / ss_tot
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RegressionError {
RowMismatch {
rows_x: usize,
rows_y: usize,
},
EmptyInput,
RaggedRows,
Singular,
InvalidPenalty,
}
impl std::fmt::Display for RegressionError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::RowMismatch { rows_x, rows_y } => {
write!(f, "row count mismatch: x has {rows_x}, y has {rows_y}")
}
Self::EmptyInput => write!(f, "design matrix has no observations"),
Self::RaggedRows => write!(f, "design matrix rows have unequal length"),
Self::Singular => write!(f, "normal-equations matrix is singular"),
Self::InvalidPenalty => {
write!(f, "ridge penalty must be finite and non-negative")
}
}
}
}
impl std::error::Error for RegressionError {}
pub fn ols(x: &[Vec<f64>], y: &[f64]) -> Result<LinearModel, RegressionError> {
ridge(x, y, 0.0)
}
pub fn ridge(x: &[Vec<f64>], y: &[f64], lambda: f64) -> Result<LinearModel, RegressionError> {
if !lambda.is_finite() || lambda < 0.0 {
return Err(RegressionError::InvalidPenalty);
}
let n_rows = x.len();
if n_rows == 0 {
return Err(RegressionError::EmptyInput);
}
if y.len() != n_rows {
return Err(RegressionError::RowMismatch {
rows_x: n_rows,
rows_y: y.len(),
});
}
let n_cols = x.first().map_or(0, Vec::len);
if x.iter().any(|row| row.len() != n_cols) {
return Err(RegressionError::RaggedRows);
}
if n_cols == 0 {
return Ok(LinearModel {
intercept: mean(y),
coefficients: Vec::new(),
});
}
let col_means = column_means(x, n_cols);
let y_mean = mean(y);
let mut a = vec![vec![0.0_f64; n_cols]; n_cols];
let mut b = vec![0.0_f64; n_cols];
for (row, &yi) in x.iter().zip(y) {
let yc = yi - y_mean;
for j in 0..n_cols {
let xj = centered(row, &col_means, j);
if let Some(bj) = b.get_mut(j) {
*bj += xj * yc;
}
if let Some(a_row) = a.get_mut(j) {
for k in 0..n_cols {
let xk = centered(row, &col_means, k);
if let Some(a_jk) = a_row.get_mut(k) {
*a_jk += xj * xk;
}
}
}
}
}
for (j, a_row) in a.iter_mut().enumerate() {
if let Some(a_jj) = a_row.get_mut(j) {
*a_jj += lambda;
}
}
let coefficients = solve(a, b)?;
let slope_dot_mean: f64 = coefficients
.iter()
.zip(&col_means)
.map(|(&beta, &mean_j)| beta * mean_j)
.sum();
Ok(LinearModel {
intercept: y_mean - slope_dot_mean,
coefficients,
})
}
#[cfg(test)]
#[path = "tests.rs"]
mod tests;