1use crate::error::GreenersError;
2use crate::linalg::{LinalgEigh as _, LinalgInverse as _, UPLO};
3use ndarray::{s, Array1, Array2, Axis};
4use statrs::distribution::{ContinuousCDF, FisherSnedecor};
5use std::fmt;
6
7#[derive(Debug)]
11pub struct PCAResult {
12 pub components: Array2<f64>,
14 pub explained_variance: Array1<f64>,
16 pub explained_variance_ratio: Array1<f64>,
18 pub loadings: Array2<f64>,
20 pub scores: Array2<f64>,
22 pub mean: Array1<f64>,
24 pub std: Array1<f64>,
26 pub n_obs: usize,
27 pub n_components: usize,
28}
29
30impl PCAResult {
31 pub fn transform(&self, data: &Array2<f64>) -> Array2<f64> {
33 let centered = self.standardize(data);
34 centered.dot(&self.components)
35 }
36
37 pub fn inverse_transform(&self, scores: &Array2<f64>) -> Array2<f64> {
39 let recon = scores.dot(&self.components.t());
40 let mut result = recon;
42 for (j, mut col) in result.axis_iter_mut(Axis(1)).enumerate() {
43 col *= self.std[j];
44 col += self.mean[j];
45 }
46 result
47 }
48
49 fn standardize(&self, data: &Array2<f64>) -> Array2<f64> {
50 let mut centered = data.clone();
51 for (j, mut col) in centered.axis_iter_mut(Axis(1)).enumerate() {
52 col -= self.mean[j];
53 if self.std[j] > 1e-15 {
54 col /= self.std[j];
55 }
56 }
57 centered
58 }
59}
60
61impl fmt::Display for PCAResult {
62 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
63 writeln!(f, "\n{:=^60}", " Principal Component Analysis ")?;
64 writeln!(f, "{:<20} {:>10}", "Observations:", self.n_obs)?;
65 writeln!(f, "{:<20} {:>10}", "Components:", self.n_components)?;
66 writeln!(
67 f,
68 "\n{:<12} {:>12} {:>12}",
69 "Component", "Var Expl", "Cumulative"
70 )?;
71 writeln!(f, "{:-^40}", "")?;
72 let mut cum = 0.0;
73 for i in 0..self.n_components {
74 cum += self.explained_variance_ratio[i];
75 writeln!(
76 f,
77 "PC{:<10} {:>12.4} {:>12.4}",
78 i + 1,
79 self.explained_variance_ratio[i],
80 cum
81 )?;
82 }
83 writeln!(f, "{:=^60}", "")
84 }
85}
86
87pub struct PCA;
89
90impl PCA {
91 pub fn fit(data: &Array2<f64>, n_components: usize) -> Result<PCAResult, GreenersError> {
92 let (n, p) = (data.nrows(), data.ncols());
93 if n < 2 {
94 return Err(GreenersError::InvalidOperation(
95 "Need at least 2 observations for PCA".into(),
96 ));
97 }
98 let nc = n_components.min(p);
99
100 let mut mean = Array1::<f64>::zeros(p);
102 let mut std = Array1::<f64>::zeros(p);
103 for j in 0..p {
104 let col = data.column(j);
105 mean[j] = col.mean().unwrap_or(0.0);
106 let var = col.iter().map(|x| (x - mean[j]).powi(2)).sum::<f64>() / (n - 1) as f64;
107 std[j] = var.sqrt().max(1e-15);
108 }
109
110 let mut z = data.clone();
111 for (j, mut col) in z.axis_iter_mut(Axis(1)).enumerate() {
112 col -= mean[j];
113 col /= std[j];
114 }
115
116 let corr = z.t().dot(&z) / (n - 1) as f64;
118
119 let (eigenvalues, eigenvectors) = corr.eigh(UPLO::Upper)?;
121
122 let total_var: f64 = eigenvalues.iter().sum();
124 let ev: Array1<f64> = eigenvalues.slice(s![..;-1]).to_owned();
125 let evec: Array2<f64> = eigenvectors.slice(s![.., ..;-1]).to_owned();
126
127 let explained_variance = ev.slice(s![..nc]).to_owned();
129 let explained_variance_ratio = explained_variance.mapv(|v| v / total_var.max(1e-15));
130 let components = evec.slice(s![.., ..nc]).to_owned();
131
132 let mut loadings = components.clone();
134 for (j, mut col) in loadings.axis_iter_mut(Axis(1)).enumerate() {
135 col *= explained_variance[j].sqrt();
136 }
137
138 let scores = z.dot(&components);
140
141 Ok(PCAResult {
142 components,
143 explained_variance,
144 explained_variance_ratio,
145 loadings,
146 scores,
147 mean,
148 std,
149 n_obs: n,
150 n_components: nc,
151 })
152 }
153}
154
155#[derive(Debug, Clone)]
159pub enum Rotation {
160 None,
161 Varimax,
162}
163
164#[derive(Debug)]
166pub struct FactorResult {
167 pub loadings: Array2<f64>,
168 pub communalities: Array1<f64>,
169 pub uniquenesses: Array1<f64>,
170 pub eigenvalues: Array1<f64>,
171 pub n_factors: usize,
172 pub n_obs: usize,
173}
174
175impl fmt::Display for FactorResult {
176 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
177 writeln!(f, "\n{:=^60}", " Factor Analysis ")?;
178 writeln!(f, "{:<20} {:>10}", "Observations:", self.n_obs)?;
179 writeln!(f, "{:<20} {:>10}", "Factors:", self.n_factors)?;
180 writeln!(f, "\nCommunalities:")?;
181 for (i, &c) in self.communalities.iter().enumerate() {
182 writeln!(f, " Var{}: {:.4}", i + 1, c)?;
183 }
184 writeln!(f, "{:=^60}", "")
185 }
186}
187
188pub struct FactorAnalysis;
190
191impl FactorAnalysis {
192 pub fn fit(
193 data: &Array2<f64>,
194 n_factors: usize,
195 rotation: Rotation,
196 ) -> Result<FactorResult, GreenersError> {
197 let (n, p) = (data.nrows(), data.ncols());
198 if n < 2 || n_factors > p {
199 return Err(GreenersError::InvalidOperation(
200 "Invalid dimensions for factor analysis".into(),
201 ));
202 }
203
204 let mut z = data.clone();
206 for mut col in z.axis_iter_mut(Axis(1)) {
207 let m = col.mean().unwrap_or(0.0);
208 col -= m;
209 let s = col.iter().map(|x| x * x).sum::<f64>() / (n - 1) as f64;
210 let s = s.sqrt().max(1e-15);
211 col /= s;
212 }
213
214 let corr = z.t().dot(&z) / (n - 1) as f64;
215 let (eigenvalues_all, eigenvectors_all) = corr.eigh(UPLO::Upper)?;
216
217 let eigenvalues: Array1<f64> = eigenvalues_all.slice(s![..;-1]).to_owned();
219 let eigenvectors: Array2<f64> = eigenvectors_all.slice(s![.., ..;-1]).to_owned();
220
221 let mut loadings = Array2::<f64>::zeros((p, n_factors));
223 for j in 0..n_factors {
224 let sqrt_ev = eigenvalues[j].max(0.0).sqrt();
225 for i in 0..p {
226 loadings[[i, j]] = eigenvectors[[i, j]] * sqrt_ev;
227 }
228 }
229
230 if matches!(rotation, Rotation::Varimax) {
232 loadings = varimax_rotation(&loadings, 100);
233 }
234
235 let communalities: Array1<f64> = (0..p)
237 .map(|i| (0..n_factors).map(|j| loadings[[i, j]].powi(2)).sum())
238 .collect::<Vec<_>>()
239 .into();
240
241 let uniquenesses = communalities.mapv(|c| (1.0 - c).max(0.0));
242
243 Ok(FactorResult {
244 loadings,
245 communalities,
246 uniquenesses,
247 eigenvalues: eigenvalues.slice(s![..n_factors]).to_owned(),
248 n_factors,
249 n_obs: n,
250 })
251 }
252}
253
254fn varimax_rotation(loadings: &Array2<f64>, max_iter: usize) -> Array2<f64> {
255 let (p, k) = (loadings.nrows(), loadings.ncols());
256 if k < 2 {
257 return loadings.clone();
258 }
259
260 let mut rotated = loadings.clone();
261
262 for _ in 0..max_iter {
263 let mut changed = false;
264 for i in 0..k {
265 for j in (i + 1)..k {
266 let mut a = 0.0;
268 let mut b = 0.0;
269 let mut c = 0.0;
270 let mut d = 0.0;
271
272 for r in 0..p {
273 let li = rotated[[r, i]];
274 let lj = rotated[[r, j]];
275 let u = li * li - lj * lj;
276 let v = 2.0 * li * lj;
277 a += u;
278 b += v;
279 c += u * u - v * v;
280 d += 2.0 * u * v;
281 }
282
283 let num = d - 2.0 * a * b / p as f64;
284 let den = c - (a * a - b * b) / p as f64;
285 let angle = 0.25 * num.atan2(den);
286
287 if angle.abs() < 1e-10 {
288 continue;
289 }
290 changed = true;
291
292 let cos_a = angle.cos();
293 let sin_a = angle.sin();
294
295 for r in 0..p {
296 let li = rotated[[r, i]];
297 let lj = rotated[[r, j]];
298 rotated[[r, i]] = cos_a * li + sin_a * lj;
299 rotated[[r, j]] = -sin_a * li + cos_a * lj;
300 }
301 }
302 }
303 if !changed {
304 break;
305 }
306 }
307
308 rotated
309}
310
311#[derive(Debug)]
315pub struct ManovaResult {
316 pub wilks_lambda: f64,
318 pub pillai_trace: f64,
320 pub hotelling_lawley: f64,
322 pub roys_largest_root: f64,
324 pub f_values: [f64; 4],
326 pub p_values: [f64; 4],
328 pub n_obs: usize,
329 pub n_groups: usize,
330 pub n_vars: usize,
331}
332
333impl fmt::Display for ManovaResult {
334 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
335 writeln!(f, "\n{:=^70}", " One-Way MANOVA ")?;
336 writeln!(f, "{:<20} {:>10}", "Observations:", self.n_obs)?;
337 writeln!(f, "{:<20} {:>10}", "Groups:", self.n_groups)?;
338 writeln!(f, "{:<20} {:>10}", "Variables:", self.n_vars)?;
339 writeln!(
340 f,
341 "\n{:<24} {:>10} {:>10} {:>10}",
342 "Test", "Statistic", "F", "P-value"
343 )?;
344 writeln!(f, "{:-^60}", "")?;
345 let names = [
346 "Wilks' Lambda",
347 "Pillai's trace",
348 "Hotelling-Lawley",
349 "Roy's largest root",
350 ];
351 let stats = [
352 self.wilks_lambda,
353 self.pillai_trace,
354 self.hotelling_lawley,
355 self.roys_largest_root,
356 ];
357 for i in 0..4 {
358 writeln!(
359 f,
360 "{:<24} {:>10.4} {:>10.4} {:>10.4}",
361 names[i], stats[i], self.f_values[i], self.p_values[i]
362 )?;
363 }
364 writeln!(f, "{:=^70}", "")
365 }
366}
367
368pub struct MANOVA;
370
371impl MANOVA {
372 pub fn fit(
376 y_matrix: &Array2<f64>,
377 groups: &Array1<usize>,
378 ) -> Result<ManovaResult, GreenersError> {
379 let (n, p) = (y_matrix.nrows(), y_matrix.ncols());
380 if n != groups.len() {
381 return Err(GreenersError::ShapeMismatch(
382 "y_matrix rows must match groups length".into(),
383 ));
384 }
385
386 let mut unique_groups: Vec<usize> = groups.iter().cloned().collect();
388 unique_groups.sort();
389 unique_groups.dedup();
390 let g = unique_groups.len();
391
392 if g < 2 {
393 return Err(GreenersError::InvalidOperation(
394 "Need at least 2 groups for MANOVA".into(),
395 ));
396 }
397
398 let grand_mean: Array1<f64> = y_matrix.mean_axis(Axis(0)).ok_or_else(|| {
400 GreenersError::InvalidOperation("Cannot compute grand mean".to_string())
401 })?;
402
403 let mut h_matrix = Array2::<f64>::zeros((p, p));
405 let mut e_matrix = Array2::<f64>::zeros((p, p));
406
407 for &grp in &unique_groups {
408 let idx: Vec<usize> = (0..n).filter(|&i| groups[i] == grp).collect();
410 let ni = idx.len();
411 if ni == 0 {
412 continue;
413 }
414
415 let mut group_mean = Array1::<f64>::zeros(p);
417 for &i in &idx {
418 group_mean = &group_mean + &y_matrix.row(i).to_owned();
419 }
420 group_mean /= ni as f64;
421
422 let diff = &group_mean - &grand_mean;
424 for a in 0..p {
425 for b in 0..p {
426 h_matrix[[a, b]] += ni as f64 * diff[a] * diff[b];
427 }
428 }
429
430 for &i in &idx {
432 let d = &y_matrix.row(i).to_owned() - &group_mean;
433 for a in 0..p {
434 for b in 0..p {
435 e_matrix[[a, b]] += d[a] * d[b];
436 }
437 }
438 }
439 }
440
441 let e_inv = e_matrix.inv()?;
443 let m = e_inv.dot(&h_matrix);
444 let m_sym = (&m + &m.t()) * 0.5;
446 let (eig_vals, _) = m_sym.eigh(UPLO::Upper)?;
447 let mut lambdas: Vec<f64> = eig_vals.iter().cloned().collect();
448 lambdas.sort_by(|a, b| b.total_cmp(a));
449 let s = p.min(g - 1);
450
451 let wilks_lambda: f64 = lambdas.iter().take(s).map(|&l| 1.0 / (1.0 + l)).product();
453 let pillai_trace: f64 = lambdas.iter().take(s).map(|&l| l / (1.0 + l)).sum();
454 let hotelling_lawley: f64 = lambdas.iter().take(s).sum();
455 let roys_largest_root = lambdas.first().cloned().unwrap_or(0.0);
456
457 let df_h = (g - 1) as f64;
459 let df_e = (n - g) as f64;
460 let pf = p as f64;
461
462 let t = if pf * pf + df_h * df_h - 5.0 > 0.0 {
464 ((pf * pf * df_h * df_h - 4.0) / (pf * pf + df_h * df_h - 5.0)).sqrt()
465 } else {
466 1.0
467 };
468 let df1_wilks = pf * df_h;
469 let df2_wilks = (df_e + df_h - 0.5 * (pf + df_h + 1.0)) * t - 0.5 * (df1_wilks) + 1.0;
470 let lambda_t = if t > 0.0 {
471 wilks_lambda.powf(1.0 / t)
472 } else {
473 wilks_lambda
474 };
475 let f_wilks = if lambda_t < 1.0 {
476 ((1.0 - lambda_t) / lambda_t) * (df2_wilks / df1_wilks)
477 } else {
478 0.0
479 };
480
481 let s_f = s as f64;
483 let f_pillai = (pillai_trace / s_f)
484 * ((df_e + s_f - pf + s_f * df_h) / ((s_f.max(1.0)) * (s_f * df_h)));
485 let df1_pillai = s_f * pf * df_h / s_f.max(1.0);
486 let df2_pillai = s_f * (df_e + s_f - pf);
487
488 let f_hl = hotelling_lawley * df_e / (s_f * df_h * pf);
490 let df1_hl = s_f * pf * df_h / s_f.max(1.0);
491 let df2_hl = s_f * df_e;
492
493 let f_roy = roys_largest_root * df_e / pf.max(df_h);
495 let df1_roy = pf.max(df_h);
496 let df2_roy = df_e;
497
498 let f_values = [f_wilks, f_pillai.max(0.0), f_hl.max(0.0), f_roy.max(0.0)];
499
500 let p_values = [
502 f_pvalue(f_wilks, df1_wilks, df2_wilks),
503 f_pvalue(f_pillai, df1_pillai, df2_pillai),
504 f_pvalue(f_hl, df1_hl, df2_hl),
505 f_pvalue(f_roy, df1_roy, df2_roy),
506 ];
507
508 Ok(ManovaResult {
509 wilks_lambda,
510 pillai_trace,
511 hotelling_lawley,
512 roys_largest_root,
513 f_values,
514 p_values,
515 n_obs: n,
516 n_groups: g,
517 n_vars: p,
518 })
519 }
520}
521
522#[derive(Debug)]
526pub struct CanCorrResult {
527 pub cancorr: Array1<f64>,
529 pub x_weights: Array2<f64>,
531 pub y_weights: Array2<f64>,
533 pub wilks_lambda: f64,
535 pub f_stat: f64,
537 pub p_value: f64,
539 pub n_obs: usize,
540}
541
542impl fmt::Display for CanCorrResult {
543 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
544 writeln!(f, "\n{:=^60}", " Canonical Correlation Analysis ")?;
545 writeln!(f, "{:<20} {:>10}", "Observations:", self.n_obs)?;
546 writeln!(f, "{:<20} {:>10.4}", "Wilks' Lambda:", self.wilks_lambda)?;
547 writeln!(f, "{:<20} {:>10.4}", "F-statistic:", self.f_stat)?;
548 writeln!(f, "{:<20} {:>10.4}", "P-value:", self.p_value)?;
549 writeln!(f, "\nCanonical Correlations:")?;
550 for (i, &c) in self.cancorr.iter().enumerate() {
551 writeln!(f, " CC{}: {:.4}", i + 1, c)?;
552 }
553 writeln!(f, "{:=^60}", "")
554 }
555}
556
557pub struct CanCorr;
559
560impl CanCorr {
561 pub fn fit(x: &Array2<f64>, y: &Array2<f64>) -> Result<CanCorrResult, GreenersError> {
566 let (n, p) = (x.nrows(), x.ncols());
567 let q = y.ncols();
568
569 if n != y.nrows() {
570 return Err(GreenersError::ShapeMismatch(
571 "x and y must have same number of rows".into(),
572 ));
573 }
574 if n < p + q + 1 {
575 return Err(GreenersError::ShapeMismatch(
576 "Not enough observations for CCA".into(),
577 ));
578 }
579
580 let s = p.min(q);
581
582 let x_mean = x
584 .mean_axis(Axis(0))
585 .ok_or_else(|| GreenersError::InvalidOperation("Cannot compute X mean".to_string()))?;
586 let y_mean = y
587 .mean_axis(Axis(0))
588 .ok_or_else(|| GreenersError::InvalidOperation("Cannot compute Y mean".to_string()))?;
589 let mut xc = x.clone();
590 let mut yc = y.clone();
591 for (j, mut col) in xc.axis_iter_mut(Axis(1)).enumerate() {
592 col -= x_mean[j];
593 }
594 for (j, mut col) in yc.axis_iter_mut(Axis(1)).enumerate() {
595 col -= y_mean[j];
596 }
597
598 let nf = (n - 1) as f64;
599 let sxx = xc.t().dot(&xc) / nf;
600 let syy = yc.t().dot(&yc) / nf;
601 let sxy = xc.t().dot(&yc) / nf;
602
603 let sxx_inv = sxx.inv()?;
605 let syy_inv = syy.inv()?;
606
607 let m = sxx_inv.dot(&sxy).dot(&syy_inv).dot(&sxy.t());
609 let m_sym = (&m + &m.t()) * 0.5;
610 let (eig_vals, eig_vecs) = m_sym.eigh(UPLO::Upper)?;
611
612 let mut idx: Vec<usize> = (0..p).collect();
614 idx.sort_by(|&a, &b| eig_vals[b].total_cmp(&eig_vals[a]));
615
616 let cancorr: Array1<f64> = idx
617 .iter()
618 .take(s)
619 .map(|&i| eig_vals[i].max(0.0).sqrt().min(1.0))
620 .collect();
621
622 let mut x_weights = Array2::<f64>::zeros((p, s));
623 for (new_col, &old_col) in idx.iter().take(s).enumerate() {
624 x_weights
625 .column_mut(new_col)
626 .assign(&eig_vecs.column(old_col));
627 }
628
629 let y_weights_raw = syy_inv.dot(&sxy.t()).dot(&x_weights);
631 let mut y_weights = Array2::<f64>::zeros((q, s));
632 for j in 0..s {
633 let col = y_weights_raw.column(j);
634 let norm = col.dot(&col).sqrt().max(1e-15);
635 y_weights.column_mut(j).assign(&(&col / norm));
636 }
637
638 let wilks_lambda: f64 = cancorr.iter().map(|&r| 1.0 - r * r).product();
640
641 let pf = p as f64;
643 let qf = q as f64;
644 let nf_obs = n as f64;
645 let t = if pf * pf * qf * qf - 4.0 > 0.0 {
646 ((pf * pf * qf * qf - 4.0) / (pf * pf + qf * qf - 5.0)).sqrt()
647 } else {
648 1.0
649 };
650 let df1 = pf * qf;
651 let df2 = ((nf_obs - 1.0 - 0.5 * (pf + qf + 1.0)) * t - 0.5 * df1 + 1.0).max(1.0);
652 let lambda_t = if t > 0.0 {
653 wilks_lambda.powf(1.0 / t)
654 } else {
655 wilks_lambda
656 };
657 let f_stat = if lambda_t < 1.0 {
658 ((1.0 - lambda_t) / lambda_t) * (df2 / df1)
659 } else {
660 0.0
661 };
662
663 let p_value = f_pvalue(f_stat, df1, df2);
664
665 Ok(CanCorrResult {
666 cancorr,
667 x_weights,
668 y_weights,
669 wilks_lambda,
670 f_stat,
671 p_value,
672 n_obs: n,
673 })
674 }
675}
676
677fn f_pvalue(f: f64, df1: f64, df2: f64) -> f64 {
678 if df1 <= 0.0 || df2 <= 0.0 || !f.is_finite() || f <= 0.0 {
679 return 1.0;
680 }
681 match FisherSnedecor::new(df1, df2) {
682 Ok(dist) => 1.0 - dist.cdf(f),
683 Err(_) => 1.0,
684 }
685}