Skip to main content

ferrolearn_linear/
lib.rs

1//! # ferrolearn-linear
2//!
3//! Linear models for the ferrolearn machine learning framework.
4//!
5//! This crate provides implementations of the most common linear models
6//! for both regression and classification tasks:
7//!
8//! - **[`LinearRegression`]** — Ordinary Least Squares via QR decomposition
9//! - **[`Ridge`]** — L2-regularized regression via Cholesky decomposition
10//! - **[`RidgeCV`]** — Ridge with built-in cross-validated alpha selection
11//! - **[`Lasso`]** — L1-regularized regression via coordinate descent
12//! - **[`LassoCV`]** — Lasso with built-in cross-validated alpha selection
13//! - **[`ElasticNet`]** — Combined L1/L2 regularization via coordinate descent
14//! - **[`ElasticNetCV`]** — ElasticNet with cross-validated (alpha, l1_ratio) selection
15//! - **[`BayesianRidge`]** — Bayesian Ridge with automatic regularization tuning
16//! - **[`HuberRegressor`]** — Robust regression via IRLS with Huber loss
17//! - **[`LogisticRegression`]** — Binary and multiclass classification via L-BFGS
18//!
19//! All models implement the [`ferrolearn_core::Fit`] and [`ferrolearn_core::Predict`]
20//! traits, and produce fitted types that implement [`ferrolearn_core::introspection::HasCoefficients`].
21//!
22//! # Design
23//!
24//! Each model follows the compile-time safety pattern:
25//!
26//! - The unfitted struct (e.g., `LinearRegression<F>`) holds hyperparameters
27//!   and implements [`Fit`](ferrolearn_core::Fit).
28//! - Calling `fit()` produces a new fitted type (e.g., `FittedLinearRegression<F>`)
29//!   that implements [`Predict`](ferrolearn_core::Predict).
30//! - Calling `predict()` on an unfitted model is a compile-time error.
31//!
32//! # Pipeline Integration
33//!
34//! All models implement [`PipelineEstimator`](ferrolearn_core::pipeline::PipelineEstimator)
35//! for `f64`, allowing them to be used as the final step in a
36//! [`Pipeline`](ferrolearn_core::pipeline::Pipeline).
37//!
38//! # Float Generics
39//!
40//! All models are generic over `F: num_traits::Float + Send + Sync + 'static`,
41//! supporting both `f32` and `f64`.
42
43pub mod ard;
44pub mod bayesian_ridge;
45pub mod elastic_net;
46pub mod elastic_net_cv;
47pub mod glm;
48pub mod huber_regressor;
49pub mod isotonic;
50pub mod lars;
51pub mod lasso;
52pub mod lasso_cv;
53pub mod lda;
54mod linalg;
55pub mod linear_regression;
56pub mod linear_svc;
57pub mod linear_svr;
58pub mod logistic_regression;
59pub mod logistic_regression_cv;
60pub mod nu_svm;
61pub mod omp;
62pub mod one_class_svm;
63mod optim;
64pub mod qda;
65pub mod quantile_regressor;
66pub mod ransac;
67pub mod ridge;
68pub mod ridge_classifier;
69pub mod ridge_cv;
70pub mod sgd;
71pub mod svm;
72
73// Re-export the main types at the crate root.
74pub use ard::{ARDRegression, FittedARDRegression};
75pub use bayesian_ridge::{BayesianRidge, FittedBayesianRidge};
76pub use elastic_net::{ElasticNet, FittedElasticNet};
77pub use elastic_net_cv::{ElasticNetCV, FittedElasticNetCV};
78pub use glm::{
79    FittedGLMRegressor, GLMFamily, GLMRegressor, GammaRegressor, PoissonRegressor,
80    TweedieRegressor,
81};
82pub use huber_regressor::{FittedHuberRegressor, HuberRegressor};
83pub use isotonic::{FittedIsotonicRegression, IsotonicRegression};
84pub use lars::{FittedLars, FittedLassoLars, Lars, LassoLars};
85pub use lasso::{FittedLasso, Lasso};
86pub use lasso_cv::{FittedLassoCV, LassoCV};
87pub use lda::{FittedLDA, LDA};
88pub use linear_regression::{FittedLinearRegression, LinearRegression};
89pub use linear_svc::{FittedLinearSVC, LinearSVC, LinearSVCLoss};
90pub use linear_svr::{FittedLinearSVR, LinearSVR, LinearSVRLoss};
91pub use logistic_regression::{FittedLogisticRegression, LogisticRegression};
92pub use logistic_regression_cv::{FittedLogisticRegressionCV, LogisticRegressionCV};
93pub use nu_svm::{FittedNuSVC, FittedNuSVR, NuSVC, NuSVR};
94pub use omp::{FittedOMP, OrthogonalMatchingPursuit};
95pub use one_class_svm::{FittedOneClassSVM, OneClassSVM};
96pub use qda::{FittedQDA, QDA};
97pub use quantile_regressor::{FittedQuantileRegressor, QuantileRegressor};
98pub use ransac::{FittedRANSACRegressor, RANSACRegressor};
99pub use ridge::{FittedRidge, Ridge};
100pub use ridge_classifier::{FittedRidgeClassifier, RidgeClassifier};
101pub use ridge_cv::{FittedRidgeCV, RidgeCV};
102pub use sgd::{FittedSGDClassifier, FittedSGDRegressor, SGDClassifier, SGDRegressor};
103pub use svm::{
104    FittedSVC, FittedSVR, Kernel, LinearKernel, PolynomialKernel, RbfKernel, SVC, SVR,
105    SigmoidKernel,
106};
107
108use ferrolearn_core::error::FerroError;
109use ferrolearn_core::traits::Predict;
110use ndarray::{Array1, Array2};
111use num_traits::Float;
112
113/// Mean-accuracy `score(x, y)` exposed on every fitted classifier in this
114/// crate via a blanket impl over [`Predict<Array2<F>, Output=Array1<usize>>`].
115///
116/// Users just `use ferrolearn_linear::ClassifierScore;` to call
117/// `fitted.score(&x, &y)` and get the same result as sklearn's
118/// `ClassifierMixin.score`.
119pub trait ClassifierScore<F: Float> {
120    /// Mean accuracy on the given test data and labels.
121    ///
122    /// # Errors
123    ///
124    /// Returns [`FerroError::ShapeMismatch`] if `x.nrows() != y.len()`,
125    /// or any error forwarded from the inner `predict`.
126    fn score(&self, x: &Array2<F>, y: &Array1<usize>) -> Result<F, FerroError>;
127}
128
129impl<T, F> ClassifierScore<F> for T
130where
131    T: Predict<Array2<F>, Output = Array1<usize>, Error = FerroError>,
132    F: Float,
133{
134    fn score(&self, x: &Array2<F>, y: &Array1<usize>) -> Result<F, FerroError> {
135        if x.nrows() != y.len() {
136            return Err(FerroError::ShapeMismatch {
137                expected: vec![x.nrows()],
138                actual: vec![y.len()],
139                context: "y length must match number of samples in X".into(),
140            });
141        }
142        let preds = self.predict(x)?;
143        Ok(mean_accuracy(&preds, y))
144    }
145}
146
147/// R² `score(x, y)` exposed on every fitted regressor in this crate via
148/// a blanket impl over [`Predict<Array2<F>, Output=Array1<F>>`].
149///
150/// Users just `use ferrolearn_linear::RegressorScore;` to call
151/// `fitted.score(&x, &y)`.
152pub trait RegressorScore<F: Float> {
153    /// R² coefficient of determination on the given test data and targets.
154    ///
155    /// # Errors
156    ///
157    /// Returns [`FerroError::ShapeMismatch`] if `x.nrows() != y.len()`,
158    /// or any error forwarded from the inner `predict`.
159    fn score(&self, x: &Array2<F>, y: &Array1<F>) -> Result<F, FerroError>;
160}
161
162impl<T, F> RegressorScore<F> for T
163where
164    T: Predict<Array2<F>, Output = Array1<F>, Error = FerroError>,
165    F: Float,
166{
167    fn score(&self, x: &Array2<F>, y: &Array1<F>) -> Result<F, FerroError> {
168        if x.nrows() != y.len() {
169            return Err(FerroError::ShapeMismatch {
170                expected: vec![x.nrows()],
171                actual: vec![y.len()],
172                context: "y length must match number of samples in X".into(),
173            });
174        }
175        let preds = self.predict(x)?;
176        Ok(r2_score(&preds, y))
177    }
178}
179
180/// Mean accuracy: `(sum(predictions == targets)) / n`.
181///
182/// Used as the body of every classifier `score(&self, x, y)` method in
183/// this crate to mirror sklearn's `ClassifierMixin.score`.
184pub(crate) fn mean_accuracy<F: Float>(predictions: &Array1<usize>, targets: &Array1<usize>) -> F {
185    let n = targets.len();
186    if n == 0 {
187        return F::zero();
188    }
189    let correct = predictions
190        .iter()
191        .zip(targets.iter())
192        .filter(|(p, t)| p == t)
193        .count();
194    F::from(correct).unwrap() / F::from(n).unwrap()
195}
196
197/// R² coefficient of determination: `1 - SSres / SStot`. Used as the
198/// body of every regressor `score(&self, x, y)` method to mirror
199/// sklearn's `RegressorMixin.score`. Constant-y returns `1.0` if
200/// predictions are also constant-perfect, else `F::neg_infinity()`.
201pub(crate) fn r2_score<F: Float>(y_pred: &Array1<F>, y_true: &Array1<F>) -> F {
202    let n = y_true.len();
203    if n == 0 {
204        return F::zero();
205    }
206    let mean = y_true.iter().copied().fold(F::zero(), |a, b| a + b) / F::from(n).unwrap();
207    let mut ss_res = F::zero();
208    let mut ss_tot = F::zero();
209    for i in 0..n {
210        let r = y_true[i] - y_pred[i];
211        let t = y_true[i] - mean;
212        ss_res = ss_res + r * r;
213        ss_tot = ss_tot + t * t;
214    }
215    if ss_tot == F::zero() {
216        if ss_res == F::zero() {
217            F::one()
218        } else {
219            F::neg_infinity()
220        }
221    } else {
222        F::one() - ss_res / ss_tot
223    }
224}
225
226/// Element-wise log of a probability matrix, used as the body of every
227/// classifier `predict_log_proba` method in this crate. Clamps values
228/// below `1e-300` to avoid `-inf` / `NaN`.
229pub(crate) fn log_proba<F: Float>(proba: &Array2<F>) -> Array2<F> {
230    let eps = F::from(1e-300).unwrap();
231    proba.mapv(|p| if p > eps { p.ln() } else { eps.ln() })
232}