1use ndarray::{Array1, ArrayView1, ArrayView2};
2
3use super::{center, Centered, Regressor};
4use crate::error::{DmlError, Result};
5use crate::linalg::cholesky_solve;
6
7#[derive(Debug, Clone, Default)]
12pub struct Ols {
13 ridge: f64,
14 coef: Option<Array1<f64>>,
15 intercept: f64,
16}
17
18impl Ols {
19 pub fn new() -> Self {
20 Self::default()
21 }
22
23 pub fn with_ridge(ridge: f64) -> Self {
24 Self {
25 ridge: ridge.max(0.0),
26 ..Self::default()
27 }
28 }
29
30 pub fn coef(&self) -> Option<&Array1<f64>> {
31 self.coef.as_ref()
32 }
33
34 pub fn intercept(&self) -> f64 {
35 self.intercept
36 }
37}
38
39impl Ols {
40 fn fit_impl(
41 &mut self,
42 x: ArrayView2<'_, f64>,
43 y: ArrayView1<'_, f64>,
44 w: Option<ArrayView1<'_, f64>>,
45 ) -> Result<()> {
46 let p = x.ncols();
47 let Centered {
49 x_mean,
50 y_mean,
51 xc,
52 yc,
53 } = center(x, y, w, false)?;
54 let mut xtx = xc.t().dot(&xc);
55 for j in 0..p {
56 xtx[[j, j]] += self.ridge;
57 }
58 let xty = xc.t().dot(&yc);
59 let coef = if p == 0 {
60 Array1::zeros(0)
61 } else {
62 cholesky_solve(&xtx, &xty)?
63 };
64 self.intercept = y_mean - x_mean.dot(&coef);
65 self.coef = Some(coef);
66 Ok(())
67 }
68}
69
70impl Regressor for Ols {
71 fn fit(&mut self, x: ArrayView2<'_, f64>, y: ArrayView1<'_, f64>) -> Result<()> {
72 self.fit_impl(x, y, None)
73 }
74
75 fn fit_weighted(
77 &mut self,
78 x: ArrayView2<'_, f64>,
79 y: ArrayView1<'_, f64>,
80 w: ArrayView1<'_, f64>,
81 ) -> Result<()> {
82 self.fit_impl(x, y, Some(w))
83 }
84
85 fn predict(&self, x: ArrayView2<'_, f64>) -> Result<Array1<f64>> {
86 let coef = self.coef.as_ref().ok_or(DmlError::NotFitted)?;
87 if x.ncols() != coef.len() {
88 return Err(DmlError::DimensionMismatch(
89 "Ols::predict feature count".into(),
90 ));
91 }
92 Ok(x.dot(coef) + self.intercept)
93 }
94
95 fn fresh(&self) -> Box<dyn Regressor> {
96 Box::new(Ols::with_ridge(self.ridge))
97 }
98}
99
100#[cfg(test)]
101mod tests {
102 use super::*;
103 use ndarray::array;
104
105 #[test]
106 fn recovers_exact_linear_fit() {
107 let x = array![[1.0, 0.0], [0.0, 1.0], [1.0, 1.0], [2.0, 1.0], [0.5, 3.0]];
108 let y: Array1<f64> = x
109 .rows()
110 .into_iter()
111 .map(|r| 1.5 + 2.0 * r[0] - 0.5 * r[1])
112 .collect();
113 let mut m = Ols::new();
114 m.fit(x.view(), y.view()).unwrap();
115 let c = m.coef().unwrap();
116 assert!((c[0] - 2.0).abs() < 1e-10 && (c[1] + 0.5).abs() < 1e-10);
117 assert!((m.intercept() - 1.5).abs() < 1e-10);
118 }
119
120 #[test]
121 fn predict_before_fit_errors() {
122 assert_eq!(
123 Ols::new().predict(array![[1.0]].view()).unwrap_err(),
124 DmlError::NotFitted
125 );
126 }
127
128 #[test]
129 fn integer_weights_equal_row_duplication() {
130 let x = array![[0.0], [1.0], [2.0], [3.0]];
131 let y = array![1.0, 0.5, 2.5, 2.0];
132 let mut weighted = Ols::new();
133 weighted
134 .fit_weighted(x.view(), y.view(), array![1.0, 2.0, 1.0, 3.0].view())
135 .unwrap();
136 let xd = array![[0.0], [1.0], [1.0], [2.0], [3.0], [3.0], [3.0]];
137 let yd = array![1.0, 0.5, 0.5, 2.5, 2.0, 2.0, 2.0];
138 let mut dup = Ols::new();
139 dup.fit(xd.view(), yd.view()).unwrap();
140 assert!((weighted.coef().unwrap()[0] - dup.coef().unwrap()[0]).abs() < 1e-12);
141 assert!((weighted.intercept() - dup.intercept()).abs() < 1e-12);
142 }
143
144 #[test]
145 fn ridge_handles_collinearity() {
146 let x = array![[1.0, 2.0], [2.0, 4.0], [3.0, 6.0]];
147 let y = array![1.0, 2.0, 3.0];
148 assert!(Ols::new().fit(x.view(), y.view()).is_err());
149 assert!(Ols::with_ridge(1e-6).fit(x.view(), y.view()).is_ok());
150 }
151}