1use modewise_method::MethodError;
4use ndarray::{Array1, Array2, ArrayView1, ArrayView2, Axis};
5
6#[derive(Debug, Clone)]
8pub struct Glm {
9 design: Array2<f64>,
10 tested: usize,
11 pinv: Array2<f64>,
13 scale: f64,
15 nuisance: Option<Array2<f64>>,
17}
18
19impl Glm {
20 pub fn new(design: Array2<f64>, tested: usize) -> Result<Self, MethodError> {
21 let (n, p) = design.dim();
22 if tested >= p {
23 return Err(MethodError::invalid(
24 "tested",
25 format!("regressor {tested} does not exist; there are {p}"),
26 ));
27 }
28 if n <= p {
29 return Err(MethodError::invalid(
30 "design",
31 format!("{n} observations cannot fit {p} regressors with error left over"),
32 ));
33 }
34 let inverse = invert(&design.t().dot(&design))
35 .ok_or_else(|| MethodError::invalid("design", "regressors are collinear"))?;
36 let nuisance_columns: Vec<usize> = (0..p).filter(|&c| c != tested).collect();
37 let nuisance = if nuisance_columns.is_empty() {
38 None
39 } else {
40 let z = design.select(Axis(1), &nuisance_columns);
41 let zz = invert(&z.t().dot(&z)).ok_or_else(|| {
42 MethodError::invalid("design", "nuisance regressors are collinear")
43 })?;
44 Some(z.dot(&zz).dot(&z.t()))
45 };
46 Ok(Self {
47 pinv: inverse.dot(&design.t()),
48 scale: inverse[[tested, tested]],
49 design,
50 tested,
51 nuisance,
52 })
53 }
54
55 pub fn n_observations(&self) -> usize {
56 self.design.nrows()
57 }
58
59 pub fn degrees_of_freedom(&self) -> f64 {
60 (self.design.nrows() - self.design.ncols()) as f64
61 }
62
63 pub fn t(&self, y: ArrayView2<f64>) -> Array1<f64> {
65 let beta = self.pinv.dot(&y);
66 let residuals = &y - &self.design.dot(&beta);
67 let df = self.degrees_of_freedom();
68 let rss = residuals.map_axis(Axis(0), |r| r.dot(&r));
69 Array1::from_shape_fn(y.ncols(), |f| {
70 beta[[self.tested, f]] / (rss[f] / df * self.scale).sqrt()
71 })
72 }
73
74 pub fn nuisance_residuals(&self, y: ArrayView2<f64>) -> Array2<f64> {
76 match &self.nuisance {
77 Some(h) => &y - &h.dot(&y),
78 None => y.to_owned(),
79 }
80 }
81}
82
83fn group(column: ArrayView1<f64>, in_a: &[bool], member: bool) -> (f64, f64, f64) {
85 let values: Vec<f64> = column
86 .iter()
87 .zip(in_a)
88 .filter(|(_, g)| **g == member)
89 .map(|(v, _)| *v)
90 .collect();
91 let n = values.len() as f64;
92 let mean = values.iter().sum::<f64>() / n;
93 let var = values.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / (n - 1.0);
94 (mean, var / n, n)
95}
96
97pub fn welch_t(y: ArrayView2<f64>, in_a: &[bool]) -> Array1<f64> {
99 Array1::from_iter(y.columns().into_iter().map(|c| {
100 let ((ma, va, _), (mb, vb, _)) = (group(c, in_a, true), group(c, in_a, false));
101 (ma - mb) / (va + vb).sqrt()
102 }))
103}
104
105pub fn welch_df(y: ArrayView2<f64>, in_a: &[bool]) -> Array1<f64> {
107 Array1::from_iter(y.columns().into_iter().map(|c| {
108 let ((_, a, na), (_, b, nb)) = (group(c, in_a, true), group(c, in_a, false));
109 (a + b).powi(2) / (a * a / (na - 1.0) + b * b / (nb - 1.0))
110 }))
111}
112
113fn invert(m: &Array2<f64>) -> Option<Array2<f64>> {
116 let n = m.nrows();
117 let mut a = m.clone();
118 let mut inv = Array2::eye(n);
119 let scale = m.iter().fold(0.0f64, |s, v| s.max(v.abs()));
120 for col in 0..n {
121 let pivot = (col..n).max_by(|&i, &j| a[[i, col]].abs().total_cmp(&a[[j, col]].abs()))?;
122 if a[[pivot, col]].abs() <= 1e-12 * scale {
123 return None;
124 }
125 for k in 0..n {
126 a.swap([pivot, k], [col, k]);
127 inv.swap([pivot, k], [col, k]);
128 }
129 let d = a[[col, col]];
130 for k in 0..n {
131 a[[col, k]] /= d;
132 inv[[col, k]] /= d;
133 }
134 for row in 0..n {
135 if row != col {
136 let f = a[[row, col]];
137 for k in 0..n {
138 a[[row, k]] -= f * a[[col, k]];
139 inv[[row, k]] -= f * inv[[col, k]];
140 }
141 }
142 }
143 }
144 Some(inv)
145}
146
147#[cfg(test)]
148mod tests {
149 use ndarray::array;
150
151 use super::*;
152
153 #[test]
154 fn inversion_and_rejections() {
155 let inv = invert(&array![[4.0, 2.0], [2.0, 3.0]]).unwrap();
156 assert!(
157 (&inv.dot(&array![[4.0, 2.0], [2.0, 3.0]]) - &Array2::<f64>::eye(2))
158 .iter()
159 .all(|v| v.abs() < 1e-14)
160 );
161 assert!(invert(&array![[1.0, 2.0], [2.0, 4.0]]).is_none());
162 let collinear = array![[1.0, 2.0], [1.0, 2.0], [1.0, 2.0], [1.0, 2.0]];
163 assert!(Glm::new(collinear, 1).is_err());
164 assert!(
165 Glm::new(Array2::ones((2, 2)), 0).is_err(),
166 "no error degrees of freedom"
167 );
168 assert!(Glm::new(Array2::ones((4, 1)), 1).is_err());
169 let with_nuisance = array![
170 [1.0, 0.0, 1.0],
171 [1.0, 1.0, 1.0],
172 [1.0, 0.0, 1.0],
173 [1.0, 1.0, 1.0],
174 [1.0, 0.5, 1.0]
175 ];
176 assert!(
177 Glm::new(with_nuisance, 1).is_err(),
178 "intercept duplicated in the nuisance"
179 );
180 }
181
182 #[test]
183 fn one_sample_t_by_hand() {
184 let y = array![[1.0], [2.0], [3.0], [6.0]];
185 let glm = Glm::new(Array2::ones((4, 1)), 0).unwrap();
186 let expected = 3.0 / ((14.0f64 / 3.0).sqrt() / 2.0);
188 assert!((glm.t(y.view())[0] - expected).abs() < 1e-12);
189 assert_eq!(glm.nuisance_residuals(y.view()), y);
190 assert_eq!((glm.n_observations(), glm.degrees_of_freedom()), (4, 3.0));
191 }
192}