qualia_core_db/solvers/learning/splines/
gam.rs1use crate::solvers::learning::splines::RegressionSpline;
7use crate::solvers::learning::LearningError;
8use crate::solvers::statistics::descriptive::mean;
9
10#[derive(Debug, Clone)]
12pub struct Gam {
13 pub intercept: f64,
14 terms: Vec<(RegressionSpline, f64)>,
17 p: usize,
18}
19
20impl Gam {
21 pub fn fit(
24 x: &[f64],
25 y: &[f64],
26 n: usize,
27 p: usize,
28 degree: usize,
29 knots_per_feature: &[Vec<f64>],
30 max_iter: usize,
31 ) -> Result<Self, LearningError> {
32 if n == 0 || p == 0 || x.len() != n * p || y.len() != n || knots_per_feature.len() != p {
33 return Err(LearningError::InvalidDimension);
34 }
35 let intercept = mean(y).ok_or(LearningError::InsufficientData)?;
36
37 let cols: Vec<Vec<f64>> = (0..p)
39 .map(|j| (0..n).map(|i| x[i * p + j]).collect::<Vec<f64>>())
40 .collect();
41
42 let mut term_vals = vec![0.0; n * p];
44 let mut terms: Vec<(RegressionSpline, f64)> = Vec::with_capacity(p);
46 for j in 0..p {
47 let spline =
48 RegressionSpline::fit(&cols[j], &vec![0.0; n], n, degree, &knots_per_feature[j])?;
49 terms.push((spline, 0.0));
50 }
51
52 for _ in 0..max_iter.max(1) {
53 for j in 0..p {
54 let mut resid = vec![0.0; n];
56 for i in 0..n {
57 let mut s = y[i] - intercept;
58 for k in 0..p {
59 if k != j {
60 s -= term_vals[i * p + k];
61 }
62 }
63 resid[i] = s;
64 }
65 let spline =
67 RegressionSpline::fit(&cols[j], &resid, n, degree, &knots_per_feature[j])?;
68 let raw: Vec<f64> = cols[j].iter().map(|&xi| spline.predict_one(xi)).collect();
69 let offset = mean(&raw).unwrap_or(0.0);
70 for i in 0..n {
71 term_vals[i * p + j] = raw[i] - offset;
72 }
73 terms[j] = (spline, offset);
74 }
75 }
76
77 Ok(Self {
78 intercept,
79 terms,
80 p,
81 })
82 }
83
84 pub fn predict_row(&self, x_row: &[f64]) -> f64 {
86 let mut s = self.intercept;
87 for (j, (spline, offset)) in self.terms.iter().enumerate() {
88 s += spline.predict_one(x_row[j]) - offset;
89 }
90 s
91 }
92
93 pub fn predict(&self, x: &[f64], n: usize) -> Vec<f64> {
94 (0..n)
95 .map(|i| self.predict_row(&x[i * self.p..(i + 1) * self.p]))
96 .collect()
97 }
98}
99
100#[cfg(test)]
101mod tests {
102 use super::*;
103 use crate::solvers::learning::metrics::regression::r2_score;
104
105 #[test]
106 fn fits_an_additive_nonlinear_surface() {
107 let n = 40;
109 let mut x = vec![0.0; n * 2];
110 let mut y = vec![0.0; n];
111 for i in 0..n {
112 let x0 = (i as f64) * 0.15;
113 let x1 = ((i * 7) % 40) as f64 * 0.1;
114 x[i * 2] = x0;
115 x[i * 2 + 1] = x1;
116 y[i] = x0.sin() + 0.1 * x1 * x1;
117 }
118 let knots = vec![vec![1.5, 3.0, 4.5], vec![1.0, 2.0, 3.0]];
119 let gam = Gam::fit(&x, &y, n, 2, 3, &knots, 10).unwrap();
120 let preds = gam.predict(&x, n);
121 assert!(
122 r2_score(&y, &preds).unwrap() > 0.95,
123 "GAM should fit the additive surface"
124 );
125 }
126
127 #[test]
128 fn recovers_a_linear_additive_model() {
129 let n = 25;
131 let mut x = vec![0.0; n * 2];
132 let mut y = vec![0.0; n];
133 for i in 0..n {
134 let x0 = i as f64;
135 let x1 = (i % 5) as f64;
136 x[i * 2] = x0;
137 x[i * 2 + 1] = x1;
138 y[i] = 2.0 * x0 - 3.0 * x1 + 1.0;
139 }
140 let knots = vec![vec![], vec![]]; let gam = Gam::fit(&x, &y, n, 2, 1, &knots, 20).unwrap();
142 let preds = gam.predict(&x, n);
143 assert!(r2_score(&y, &preds).unwrap() > 0.999);
144 }
145
146 #[test]
147 fn guards() {
148 assert_eq!(
149 Gam::fit(&[1.0, 2.0], &[1.0], 2, 1, 3, &[vec![]], 5).unwrap_err(),
150 LearningError::InvalidDimension
151 );
152 }
153}