1use ndarray::{Array1, Array2};
17use solow_core::error::{Error, Result};
18use solow_distributions::f_sf;
19use solow_linalg::{pinv, qr};
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum AnovaType {
24 I,
26 II,
28 III,
30}
31
32#[derive(Debug, Clone)]
35pub struct Term {
36 pub name: String,
39 pub start: usize,
41 pub stop: usize,
43}
44
45impl Term {
46 pub fn new(name: impl Into<String>, start: usize, stop: usize) -> Self {
48 Term {
49 name: name.into(),
50 start,
51 stop,
52 }
53 }
54
55 fn factors(&self) -> Vec<&str> {
57 self.name.split(':').collect()
58 }
59}
60
61#[derive(Debug, Clone)]
63pub struct AnovaRow {
64 pub name: String,
66 pub df: f64,
68 pub sum_sq: f64,
70 pub mean_sq: f64,
72 pub f: Option<f64>,
74 pub pr: Option<f64>,
76}
77
78#[derive(Debug, Clone)]
80pub struct AnovaTable {
81 pub rows: Vec<AnovaRow>,
83}
84
85impl AnovaTable {
86 pub fn row(&self, name: &str) -> Option<&AnovaRow> {
88 self.rows.iter().find(|r| r.name == name)
89 }
90}
91
92fn is_intercept(name: &str) -> bool {
94 name == "Intercept"
95}
96
97pub fn anova_lm(
104 endog: &Array1<f64>,
105 exog: &Array2<f64>,
106 terms: &[Term],
107 typ: AnovaType,
108) -> Result<AnovaTable> {
109 let (nobs, kcols) = exog.dim();
110
111 let (pinv_x, sv) = pinv(exog)?;
113 let beta = pinv_x.dot(endog);
114 let fitted = exog.dot(&beta);
115 let resid = endog - &fitted;
116 let ssr = resid.dot(&resid);
117
118 let smax = sv.iter().cloned().fold(0.0_f64, f64::max);
120 let tol = smax * (sv.len() as f64) * f64::EPSILON;
121 let rank = sv.iter().filter(|&&s| s > tol).count();
122 let df_resid = nobs as f64 - rank as f64;
123 let scale = ssr / df_resid;
124
125 let rows = match typ {
126 AnovaType::I => anova_type1(endog, exog, terms, ssr, df_resid),
127 AnovaType::II | AnovaType::III => {
128 let xtx = exog.t().dot(exog);
130 let (xtx_pinv, _) = pinv(&xtx)?;
131 let cov = &xtx_pinv * scale;
132 if typ == AnovaType::III {
133 anova_type3(&beta, &cov, terms, kcols, ssr, df_resid)?
134 } else {
135 anova_type2(&beta, &cov, terms, kcols, ssr, df_resid)?
136 }
137 }
138 };
139
140 Ok(AnovaTable { rows })
141}
142
143fn make_row(name: &str, sum_sq: f64, df: f64, scale: f64, df_resid: f64) -> AnovaRow {
145 let mean_sq = sum_sq / df;
146 let f = mean_sq / scale;
147 let pr = f_sf(f, df, df_resid);
148 AnovaRow {
149 name: name.to_string(),
150 df,
151 sum_sq,
152 mean_sq,
153 f: Some(f),
154 pr: Some(pr),
155 }
156}
157
158fn residual_row(ssr: f64, df_resid: f64) -> AnovaRow {
160 AnovaRow {
161 name: "Residual".to_string(),
162 df: df_resid,
163 sum_sq: ssr,
164 mean_sq: ssr / df_resid,
165 f: None,
166 pr: None,
167 }
168}
169
170fn anova_type1(
172 endog: &Array1<f64>,
173 exog: &Array2<f64>,
174 terms: &[Term],
175 ssr: f64,
176 df_resid: f64,
177) -> Vec<AnovaRow> {
178 let scale = ssr / df_resid;
179 let (q, _r) = qr(exog).expect("qr of design");
181 let effects = q.t().dot(endog);
182
183 let mut rows = Vec::new();
184 for t in terms {
185 if is_intercept(&t.name) {
186 continue;
187 }
188 let mut ss = 0.0;
189 for c in t.start..t.stop {
190 ss += effects[c] * effects[c];
191 }
192 let df = (t.stop - t.start) as f64;
193 rows.push(make_row(&t.name, ss, df, scale, df_resid));
194 }
195 rows.push(residual_row(ssr, df_resid));
196 rows
197}
198
199fn anova_type3(
202 beta: &Array1<f64>,
203 cov: &Array2<f64>,
204 terms: &[Term],
205 kcols: usize,
206 ssr: f64,
207 df_resid: f64,
208) -> Result<Vec<AnovaRow>> {
209 let scale = ssr / df_resid;
210 let mut rows = Vec::new();
211 for t in terms {
212 let l = identity_rows(kcols, &(t.start..t.stop).collect::<Vec<_>>());
213 let (fstat, r) = f_test(&l, beta, cov)?;
214 let df = r as f64;
215 let ss = fstat * df * scale;
216 rows.push(make_row(&t.name, ss, df, scale, df_resid));
217 }
218 rows.push(residual_row(ssr, df_resid));
219 Ok(rows)
220}
221
222fn anova_type2(
225 beta: &Array1<f64>,
226 cov: &Array2<f64>,
227 terms: &[Term],
228 kcols: usize,
229 ssr: f64,
230 df_resid: f64,
231) -> Result<Vec<AnovaRow>> {
232 let scale = ssr / df_resid;
233 let model_terms: Vec<&Term> = terms.iter().filter(|t| !is_intercept(&t.name)).collect();
235
236 let mut rows = Vec::new();
237 for term in &model_terms {
238 let mut l1_cols: Vec<usize> = (term.start..term.stop).collect();
239 let mut l2_cols: Vec<usize> = Vec::new();
240 let term_set: Vec<&str> = term.factors();
241 for t in &model_terms {
242 let other: Vec<&str> = t.factors();
243 if is_strict_subset(&term_set, &other) {
245 l1_cols.extend(t.start..t.stop);
246 l2_cols.extend(t.start..t.stop);
247 }
248 }
249 let l1 = identity_rows(kcols, &l1_cols);
250 let (l12, r) = if !l2_cols.is_empty() {
251 let l2 = identity_rows(kcols, &l2_cols);
252 let lvl = l1.dot(cov).dot(&l2.t());
254 let rr = l1.nrows() - l2.nrows();
255 let orth_compl = full_q(&lvl)?;
256 let ncolq = orth_compl.ncols();
257 let comp = orth_compl.slice(ndarray::s![.., (ncolq - rr)..]).to_owned();
258 (comp.t().dot(&l1), rr)
259 } else {
260 (l1.clone(), l1.nrows())
261 };
262
263 let (fstat, _jr) = f_test(&l12, beta, cov)?;
264 let df = r as f64;
265 let ss = fstat * df * scale;
266 rows.push(make_row(&term.name, ss, df, scale, df_resid));
267 }
268 rows.push(residual_row(ssr, df_resid));
269 Ok(rows)
270}
271
272fn is_strict_subset(a: &[&str], b: &[&str]) -> bool {
274 if a.len() >= b.len() {
275 return false;
276 }
277 a.iter().all(|x| b.contains(x))
278}
279
280fn identity_rows(kcols: usize, cols: &[usize]) -> Array2<f64> {
283 let mut l = Array2::<f64>::zeros((cols.len(), kcols));
284 for (i, &c) in cols.iter().enumerate() {
285 l[[i, c]] = 1.0;
286 }
287 l
288}
289
290fn f_test(l: &Array2<f64>, beta: &Array1<f64>, cov: &Array2<f64>) -> Result<(f64, usize)> {
295 let rb = l.dot(beta);
296 let cov_l = l.dot(cov).dot(&l.t());
297 let (cov_l_pinv, _) = pinv(&cov_l)?;
298 let quad = rb.dot(&cov_l_pinv.dot(&rb));
299 let j = l.nrows();
300 Ok((quad / j as f64, j))
301}
302
303fn full_q(a: &Array2<f64>) -> Result<Array2<f64>> {
308 let (m, n) = a.dim();
309 if m < n {
310 return Err(Error::Shape("full_q requires rows >= cols".into()));
311 }
312 let mut r = a.clone();
313 let mut q = Array2::<f64>::eye(m);
314 for k in 0..n {
315 let mut norm = 0.0;
316 for i in k..m {
317 norm += r[[i, k]] * r[[i, k]];
318 }
319 let norm = norm.sqrt();
320 if norm == 0.0 {
321 continue;
322 }
323 let alpha = if r[[k, k]] >= 0.0 { -norm } else { norm };
324 let mut v = vec![0.0; m];
325 v[k] = r[[k, k]] - alpha;
326 for (i, vi) in v.iter_mut().enumerate().skip(k + 1) {
327 *vi = r[[i, k]];
328 }
329 let mut vnorm2 = 0.0;
330 for vi in v.iter().skip(k) {
331 vnorm2 += vi * vi;
332 }
333 if vnorm2 == 0.0 {
334 continue;
335 }
336 for j in k..n {
338 let mut dot = 0.0;
339 for i in k..m {
340 dot += v[i] * r[[i, j]];
341 }
342 let b = 2.0 * dot / vnorm2;
343 for i in k..m {
344 r[[i, j]] -= b * v[i];
345 }
346 }
347 for j in 0..m {
349 let mut dot = 0.0;
350 for i in k..m {
351 dot += v[i] * q[[j, i]];
352 }
353 let b = 2.0 * dot / vnorm2;
354 for i in k..m {
355 q[[j, i]] -= b * v[i];
356 }
357 }
358 }
359 Ok(q)
360}
361
362#[cfg(test)]
363mod tests {
364 use super::*;
365 use ndarray::array;
366
367 #[test]
368 fn simple_regression_type1() {
369 let exog = array![[1.0, 0.0], [1.0, 1.0], [1.0, 2.0], [1.0, 3.0]];
371 let endog = array![1.0, 3.0, 5.0, 7.0];
372 let terms = vec![Term::new("Intercept", 0, 1), Term::new("x", 1, 2)];
373 let tab = anova_lm(&endog, &exog, &terms, AnovaType::I).unwrap();
374 let x = tab.row("x").unwrap();
375 assert!(x.sum_sq > 0.0);
376 let res = tab.row("Residual").unwrap();
377 assert!(res.sum_sq.abs() < 1e-18);
378 }
379
380 #[test]
381 fn strict_subset_logic() {
382 assert!(is_strict_subset(&["a"], &["a", "b"]));
383 assert!(!is_strict_subset(&["a", "b"], &["a", "b"]));
384 assert!(!is_strict_subset(&["c"], &["a", "b"]));
385 }
386}