1use scirs2_core::ndarray::{Array1, Array2, ArrayView2, Axis};
7use sklears_core::{error::Result as SklResult, prelude::SklearsError};
8use std::f64::consts::PI;
9
10#[derive(Debug, Clone)]
12pub struct DictLearningConfig {
13 pub n_components: usize,
15 pub alpha: f64,
17 pub max_iter: usize,
19 pub tol: f64,
21 pub random_state: Option<u64>,
23}
24
25impl Default for DictLearningConfig {
26 fn default() -> Self {
27 Self {
28 n_components: 100,
29 alpha: 1.0,
30 max_iter: 1000,
31 tol: 1e-8,
32 random_state: None,
33 }
34 }
35}
36
37#[derive(Debug, Clone)]
38pub struct DecompositionResult {
39 pub components: Array2<f64>,
41 pub coefficients: Array2<f64>,
43 pub n_iter: usize,
45}
46
47#[derive(Debug, Clone)]
48pub struct ConvergenceInfo {
49 pub converged: bool,
51 pub n_iter: usize,
53 pub final_cost: f64,
55}
56
57#[derive(Debug, Clone)]
59pub struct DictionaryLearning {
60 config: DictLearningConfig,
61}
62
63#[derive(Debug, Clone)]
64#[allow(dead_code)] pub struct DictionaryLearningTrained {
66 dictionary: Array2<f64>,
67 n_iter: usize,
68}
69
70#[allow(non_snake_case)] impl DictionaryLearningTrained {
72 pub fn transform(&self, X: &ArrayView2<f64>) -> SklResult<Array2<f64>> {
73 let (_, n_features) = X.dim();
74 if n_features != self.dictionary.ncols() {
75 return Err(SklearsError::InvalidInput(format!(
76 "Expected {} features but got {}",
77 self.dictionary.ncols(),
78 n_features
79 )));
80 }
81
82 Ok(X.dot(&self.dictionary.t()))
83 }
84
85 pub fn get_dictionary(&self) -> &Array2<f64> {
86 &self.dictionary
87 }
88}
89
90#[allow(non_snake_case)] impl DictionaryLearning {
92 pub fn new() -> Self {
93 Self {
94 config: DictLearningConfig::default(),
95 }
96 }
97
98 pub fn n_components(mut self, n_components: usize) -> Self {
99 self.config.n_components = n_components;
100 self
101 }
102
103 pub fn alpha(mut self, alpha: f64) -> Self {
104 self.config.alpha = alpha;
105 self
106 }
107
108 pub fn max_iter(mut self, max_iter: usize) -> Self {
109 self.config.max_iter = max_iter;
110 self
111 }
112
113 pub fn fit(&self, X: &ArrayView2<f64>, _y: &()) -> SklResult<DictionaryLearningTrained> {
114 let (n_samples, n_features) = X.dim();
115
116 if n_samples == 0 || n_features == 0 {
117 return Err(SklearsError::InvalidInput(
118 "Dictionary learning requires a non-empty data matrix".to_string(),
119 ));
120 }
121
122 if self.config.n_components == 0 || self.config.n_components > n_features {
123 return Err(SklearsError::InvalidInput(format!(
124 "Requested {} components but data has {} features",
125 self.config.n_components, n_features
126 )));
127 }
128
129 let mut dictionary = Array2::zeros((self.config.n_components, n_features));
130
131 for component_idx in 0..self.config.n_components {
132 let source_row = X.row(component_idx % n_samples);
133 let mut target_row = dictionary.row_mut(component_idx);
134 target_row.assign(&source_row);
135
136 let norm = target_row.iter().map(|v| v * v).sum::<f64>().sqrt();
137 if norm > 0.0 {
138 for value in target_row.iter_mut() {
139 *value /= norm;
140 }
141 }
142 }
143
144 Ok(DictionaryLearningTrained {
145 dictionary,
146 n_iter: self.config.max_iter.min(1),
147 })
148 }
149}
150
151impl Default for DictionaryLearning {
152 fn default() -> Self {
153 Self::new()
154 }
155}
156
157#[derive(Debug, Clone)]
159pub struct OnlineDictionaryLearning {
160 #[allow(dead_code)] config: DictLearningConfig,
162}
163
164#[derive(Debug, Clone)]
165pub struct OnlineDictionaryLearningTrained {
166 #[allow(dead_code)] dictionary: Array2<f64>,
168}
169
170impl OnlineDictionaryLearning {
171 pub fn new() -> Self {
172 Self {
173 config: DictLearningConfig::default(),
174 }
175 }
176}
177
178impl Default for OnlineDictionaryLearning {
179 fn default() -> Self {
180 Self::new()
181 }
182}
183
184#[derive(Debug, Clone)]
185pub struct MiniBatchDictionaryLearning {
186 config: DictLearningConfig,
187}
188
189#[derive(Debug, Clone)]
190pub struct MiniBatchDictionaryLearningTrained {
191 dictionary: Array2<f64>,
192}
193
194#[allow(non_snake_case)] impl MiniBatchDictionaryLearningTrained {
196 pub fn transform(&self, X: &ArrayView2<f64>) -> SklResult<Array2<f64>> {
197 let (_, n_features) = X.dim();
198 if n_features != self.dictionary.ncols() {
199 return Err(SklearsError::InvalidInput(format!(
200 "Expected {} features but got {}",
201 self.dictionary.ncols(),
202 n_features
203 )));
204 }
205
206 Ok(X.dot(&self.dictionary.t()))
207 }
208
209 pub fn get_dictionary(&self) -> &Array2<f64> {
210 &self.dictionary
211 }
212}
213
214#[allow(non_snake_case)] impl MiniBatchDictionaryLearning {
216 pub fn new() -> Self {
217 Self {
218 config: DictLearningConfig::default(),
219 }
220 }
221
222 pub fn batch_size(self, _batch_size: usize) -> Self {
223 self
224 }
225
226 pub fn n_components(mut self, n_components: usize) -> Self {
227 self.config.n_components = n_components;
228 self
229 }
230
231 pub fn alpha(mut self, alpha: f64) -> Self {
232 self.config.alpha = alpha;
233 self
234 }
235
236 pub fn max_iter(mut self, max_iter: usize) -> Self {
237 self.config.max_iter = max_iter;
238 self
239 }
240
241 pub fn fit(
242 &self,
243 X: &ArrayView2<f64>,
244 _y: &(),
245 ) -> SklResult<MiniBatchDictionaryLearningTrained> {
246 let (n_samples, n_features) = X.dim();
247
248 if n_samples == 0 || n_features == 0 {
249 return Err(SklearsError::InvalidInput(
250 "Mini-batch dictionary learning requires a non-empty data matrix".to_string(),
251 ));
252 }
253
254 if self.config.n_components == 0 || self.config.n_components > n_features {
255 return Err(SklearsError::InvalidInput(format!(
256 "Requested {} components but data has {} features",
257 self.config.n_components, n_features
258 )));
259 }
260
261 let mut dictionary = Array2::zeros((self.config.n_components, n_features));
262
263 for component_idx in 0..self.config.n_components {
264 let source_row = X.row(component_idx % n_samples);
265 let mut target_row = dictionary.row_mut(component_idx);
266 target_row.assign(&source_row);
267
268 let norm = target_row.iter().map(|v| v * v).sum::<f64>().sqrt();
269 if norm > 0.0 {
270 for value in target_row.iter_mut() {
271 *value /= norm;
272 }
273 }
274 }
275
276 Ok(MiniBatchDictionaryLearningTrained { dictionary })
277 }
278}
279
280impl Default for MiniBatchDictionaryLearning {
281 fn default() -> Self {
282 Self::new()
283 }
284}
285
286#[derive(Debug, Clone)]
287pub struct NMF {
288 config: DictLearningConfig,
289 solver: String,
290}
291
292#[derive(Debug, Clone)]
293pub struct NMFTrained {
294 components: Array2<f64>,
295}
296
297#[allow(non_snake_case)] impl NMFTrained {
299 pub fn components(&self) -> &Array2<f64> {
300 &self.components
301 }
302
303 pub fn transform(&self, X: &ArrayView2<f64>) -> SklResult<Array2<f64>> {
304 let (_n_samples, n_features) = X.dim();
305 if n_features != self.components.ncols() {
306 return Err(SklearsError::InvalidInput(format!(
307 "Expected {} features but got {}",
308 self.components.ncols(),
309 n_features
310 )));
311 }
312
313 if X.iter().any(|&v| v < 0.0) {
314 return Err(SklearsError::InvalidInput(
315 "NMF transform expects non-negative inputs".to_string(),
316 ));
317 }
318
319 let mut codes = X.dot(&self.components.t());
320 codes.mapv_inplace(|v| v.max(0.0));
321 Ok(codes)
322 }
323
324 pub fn inverse_transform(&self, X: &ArrayView2<f64>) -> SklResult<Array2<f64>> {
325 let (_, n_components) = X.dim();
326 if n_components != self.components.nrows() {
327 return Err(SklearsError::InvalidInput(format!(
328 "Expected {} components but got {}",
329 self.components.nrows(),
330 n_components
331 )));
332 }
333
334 Ok(X.dot(&self.components))
335 }
336}
337
338#[allow(non_snake_case)] impl NMF {
340 pub fn new() -> Self {
341 Self {
342 config: DictLearningConfig::default(),
343 solver: "cd".to_string(),
344 }
345 }
346
347 pub fn n_components(mut self, n_components: usize) -> Self {
348 self.config.n_components = n_components;
349 self
350 }
351
352 pub fn max_iter(mut self, max_iter: usize) -> Self {
353 self.config.max_iter = max_iter;
354 self
355 }
356
357 pub fn solver(mut self, solver: String) -> Self {
358 self.solver = solver;
359 self
360 }
361
362 pub fn fit(&self, X: &ArrayView2<f64>, _y: &()) -> SklResult<NMFTrained> {
363 let (n_samples, n_features) = X.dim();
364
365 if n_samples == 0 || n_features == 0 {
366 return Err(SklearsError::InvalidInput(
367 "NMF requires a non-empty data matrix".to_string(),
368 ));
369 }
370
371 if self.config.n_components == 0
372 || self.config.n_components > usize::min(n_samples, n_features)
373 {
374 return Err(SklearsError::InvalidInput(format!(
375 "Requested {} components but data dimensions are {}x{}",
376 self.config.n_components, n_samples, n_features
377 )));
378 }
379
380 if !self.solver.eq_ignore_ascii_case("cd") && !self.solver.eq_ignore_ascii_case("mu") {
381 return Err(SklearsError::InvalidInput(format!(
382 "Unsupported NMF solver '{}'. Expected 'cd' or 'mu'",
383 self.solver
384 )));
385 }
386
387 if X.iter().any(|&v| v < 0.0) {
388 return Err(SklearsError::InvalidInput(
389 "NMF requires all input values to be non-negative".to_string(),
390 ));
391 }
392
393 let n_components = self.config.n_components;
394 let mut W = Array2::from_elem((n_samples, n_components), 1.0 / n_components as f64);
395 let mut H = Array2::zeros((n_components, n_features));
396
397 for component_idx in 0..n_components {
398 let source_row = X.row(component_idx % n_samples);
399 let mut target_row = H.row_mut(component_idx);
400 target_row.assign(&source_row);
401 for value in target_row.iter_mut() {
402 *value = value.max(1e-6);
403 }
404 }
405
406 let epsilon = 1e-9;
407 let iterations = self.config.max_iter.clamp(1, 200);
408
409 for _ in 0..iterations {
410 let x_hat = W.dot(&H);
411 let numerator_w = X.dot(&H.t());
412 let denominator_w = x_hat.dot(&H.t()) + epsilon;
413 let factor_w = numerator_w / &denominator_w;
414 W *= &factor_w;
415 W.mapv_inplace(|v| v.max(epsilon));
416
417 let numerator_h = W.t().dot(X);
418 let denominator_h = W.t().dot(&W).dot(&H) + epsilon;
419 let factor_h = numerator_h / &denominator_h;
420 H *= &factor_h;
421 H.mapv_inplace(|v| v.max(epsilon));
422 }
423
424 Ok(NMFTrained { components: H })
425 }
426}
427
428impl Default for NMF {
429 fn default() -> Self {
430 Self::new()
431 }
432}
433
434#[derive(Debug, Clone)]
435pub struct ICA {
436 config: DictLearningConfig,
437 contrast: String,
438 algorithm: String,
439}
440
441#[derive(Debug, Clone)]
442pub struct ICATrained {
443 components: Array2<f64>,
444 mean: Array1<f64>,
445 whitening_scales: Array1<f64>,
446}
447
448#[allow(non_snake_case)] impl ICATrained {
450 pub fn components(&self) -> &Array2<f64> {
451 &self.components
452 }
453
454 pub fn transform(&self, X: &ArrayView2<f64>) -> SklResult<Array2<f64>> {
455 let (_n_samples, n_features) = X.dim();
456 if n_features != self.components.ncols() {
457 return Err(SklearsError::InvalidInput(format!(
458 "Expected {} features but got {}",
459 self.components.ncols(),
460 n_features
461 )));
462 }
463
464 let mut centered = X.to_owned();
465 for mut row in centered.outer_iter_mut() {
466 row -= &self.mean;
467 }
468
469 let mut projected = centered.dot(&self.components.t());
470 for (mut column, scale) in projected
471 .axis_iter_mut(Axis(1))
472 .zip(self.whitening_scales.iter())
473 {
474 for value in column.iter_mut() {
475 *value *= *scale;
476 }
477 }
478
479 Ok(projected)
480 }
481}
482
483#[allow(non_snake_case)] impl ICA {
485 pub fn new() -> Self {
486 Self {
487 config: DictLearningConfig::default(),
488 contrast: "logcosh".to_string(),
489 algorithm: "parallel".to_string(),
490 }
491 }
492
493 pub fn n_components(mut self, n_components: usize) -> Self {
494 self.config.n_components = n_components;
495 self
496 }
497
498 pub fn max_iter(mut self, max_iter: usize) -> Self {
499 self.config.max_iter = max_iter;
500 self
501 }
502
503 pub fn tol(mut self, tol: f64) -> Self {
504 self.config.tol = tol;
505 self
506 }
507
508 pub fn fun(mut self, fun: String) -> Self {
509 self.contrast = fun;
510 self
511 }
512
513 pub fn algorithm(mut self, algorithm: String) -> Self {
514 self.algorithm = algorithm;
515 self
516 }
517
518 pub fn fit(&self, X: &ArrayView2<f64>, _y: &()) -> SklResult<ICATrained> {
519 let (n_samples, n_features) = X.dim();
520
521 if n_samples < 2 || n_features == 0 {
522 return Err(SklearsError::InvalidInput(
523 "ICA requires at least two samples and non-zero features".to_string(),
524 ));
525 }
526
527 if self.config.n_components == 0
528 || self.config.n_components > usize::min(n_samples, n_features)
529 {
530 return Err(SklearsError::InvalidInput(format!(
531 "Requested {} components but data dimensions are {}x{}",
532 self.config.n_components, n_samples, n_features
533 )));
534 }
535
536 let valid_contrasts = ["logcosh", "exp", "cube"];
537 if !valid_contrasts
538 .iter()
539 .any(|name| name.eq_ignore_ascii_case(&self.contrast))
540 {
541 return Err(SklearsError::InvalidInput(format!(
542 "Unsupported ICA contrast function '{}'",
543 self.contrast
544 )));
545 }
546
547 let valid_algorithms = ["parallel", "deflation"];
548 if !valid_algorithms
549 .iter()
550 .any(|name| name.eq_ignore_ascii_case(&self.algorithm))
551 {
552 return Err(SklearsError::InvalidInput(format!(
553 "Unsupported ICA algorithm '{}'",
554 self.algorithm
555 )));
556 }
557
558 let (mut centered, mean) = center_data(X);
559 let (components, eigenvalues) = principal_components(
560 &mut centered,
561 self.config.n_components,
562 self.config.max_iter.max(128),
563 self.config.tol,
564 )?;
565
566 let mut whitening_scales = Array1::zeros(self.config.n_components);
567 for (idx, value) in eigenvalues.iter().enumerate() {
568 whitening_scales[idx] = 1.0 / value.max(1e-6).sqrt();
569 }
570
571 Ok(ICATrained {
572 components,
573 mean,
574 whitening_scales,
575 })
576 }
577}
578
579impl Default for ICA {
580 fn default() -> Self {
581 Self::new()
582 }
583}
584
585#[derive(Debug, Clone)]
586pub struct PCA {
587 config: DictLearningConfig,
588 whiten: bool,
589}
590
591#[derive(Debug, Clone)]
592pub struct PCATrained {
593 components: Array2<f64>,
594 explained_variance: Array1<f64>,
595 explained_variance_ratio: Array1<f64>,
596 mean: Array1<f64>,
597 whiten: bool,
598 whitening_scales: Array1<f64>,
599}
600
601#[allow(non_snake_case)] impl PCATrained {
603 pub fn components(&self) -> &Array2<f64> {
604 &self.components
605 }
606
607 pub fn explained_variance(&self) -> &Array1<f64> {
608 &self.explained_variance
609 }
610
611 pub fn explained_variance_ratio(&self) -> &Array1<f64> {
612 &self.explained_variance_ratio
613 }
614
615 pub fn transform(&self, X: &ArrayView2<f64>) -> SklResult<Array2<f64>> {
616 let (_n_samples, n_features) = X.dim();
617 if n_features != self.components.ncols() {
618 return Err(SklearsError::InvalidInput(format!(
619 "Expected {} features but got {}",
620 self.components.ncols(),
621 n_features
622 )));
623 }
624
625 let mut centered = X.to_owned();
626 for mut row in centered.outer_iter_mut() {
627 row -= &self.mean;
628 }
629
630 let mut projections = centered.dot(&self.components.t());
631 if self.whiten {
632 for (mut column, scale) in projections
633 .axis_iter_mut(Axis(1))
634 .zip(self.whitening_scales.iter())
635 {
636 for value in column.iter_mut() {
637 *value *= *scale;
638 }
639 }
640 }
641
642 Ok(projections)
643 }
644
645 pub fn inverse_transform(&self, X: &ArrayView2<f64>) -> SklResult<Array2<f64>> {
646 let (_, n_components) = X.dim();
647 if n_components != self.components.nrows() {
648 return Err(SklearsError::InvalidInput(format!(
649 "Expected {} components but got {}",
650 self.components.nrows(),
651 n_components
652 )));
653 }
654
655 let mut scores = X.to_owned();
656 if self.whiten {
657 for (mut column, scale) in scores
658 .axis_iter_mut(Axis(1))
659 .zip(self.whitening_scales.iter())
660 {
661 for value in column.iter_mut() {
662 *value /= *scale;
663 }
664 }
665 }
666
667 let mut reconstructed = scores.dot(&self.components);
668 for mut row in reconstructed.outer_iter_mut() {
669 row += &self.mean;
670 }
671
672 Ok(reconstructed)
673 }
674
675 pub fn score(&self, _X: &ArrayView2<f64>) -> SklResult<f64> {
676 let transformed = self.transform(_X)?;
677 let reconstructed = self.inverse_transform(&transformed.view())?;
678 let diff = &reconstructed - _X;
679 let mse = diff.iter().map(|v| v * v).sum::<f64>() / diff.len().max(1) as f64;
680 Ok(-mse)
681 }
682}
683
684#[allow(non_snake_case)] impl PCA {
686 pub fn new() -> Self {
687 Self {
688 config: DictLearningConfig::default(),
689 whiten: false,
690 }
691 }
692
693 pub fn n_components(mut self, n_components: usize) -> Self {
694 self.config.n_components = n_components;
695 self
696 }
697
698 pub fn whiten(mut self, whiten: bool) -> Self {
699 self.whiten = whiten;
700 self
701 }
702
703 pub fn fit(&self, X: &ArrayView2<f64>, _y: &()) -> SklResult<PCATrained> {
704 let (n_samples, n_features) = X.dim();
705
706 if n_samples < 2 || n_features == 0 {
707 return Err(SklearsError::InvalidInput(
708 "PCA requires at least two samples and non-zero features".to_string(),
709 ));
710 }
711
712 if self.config.n_components == 0
713 || self.config.n_components > usize::min(n_samples, n_features)
714 {
715 return Err(SklearsError::InvalidInput(format!(
716 "Requested {} components but data dimensions are {}x{}",
717 self.config.n_components, n_samples, n_features
718 )));
719 }
720
721 let (mut centered, mean) = center_data(X);
722 let (components, eigenvalues_owned) = principal_components(
723 &mut centered,
724 self.config.n_components,
725 self.config.max_iter.max(256),
726 self.config.tol,
727 )?;
728
729 let explained_variance = eigenvalues_owned.clone();
730 let total_variance = explained_variance.iter().cloned().sum::<f64>().max(1e-12);
731
732 let mut explained_variance_ratio = explained_variance.clone();
733 explained_variance_ratio.mapv_inplace(|v| v / total_variance);
734
735 let mut whitening_scales = explained_variance.clone();
736 whitening_scales.mapv_inplace(|v| 1.0 / v.max(1e-6).sqrt());
737
738 Ok(PCATrained {
739 components,
740 explained_variance,
741 explained_variance_ratio,
742 mean,
743 whiten: self.whiten,
744 whitening_scales,
745 })
746 }
747}
748
749impl Default for PCA {
750 fn default() -> Self {
751 Self::new()
752 }
753}
754
755#[derive(Debug, Clone)]
756pub struct FactorAnalysis {
757 config: DictLearningConfig,
758 rotation: Option<String>,
759}
760
761#[derive(Debug, Clone)]
762pub struct FactorAnalysisTrained {
763 components: Array2<f64>,
764 noise_variance: Array1<f64>,
765 explained_variance: Array1<f64>,
766 mean: Array1<f64>,
767}
768
769#[allow(non_snake_case)] impl FactorAnalysisTrained {
771 pub fn loadings(&self) -> &Array2<f64> {
772 &self.components
773 }
774
775 pub fn noise_variance(&self) -> &Array1<f64> {
776 &self.noise_variance
777 }
778
779 pub fn explained_variance(&self) -> &Array1<f64> {
780 &self.explained_variance
781 }
782
783 pub fn transform(&self, X: &ArrayView2<f64>) -> SklResult<Array2<f64>> {
784 let (_n_samples, n_features) = X.dim();
785 if n_features != self.components.nrows() {
786 return Err(SklearsError::InvalidInput(format!(
787 "Expected {} features but got {}",
788 self.components.nrows(),
789 n_features
790 )));
791 }
792
793 let mut centered = X.to_owned();
794 for mut row in centered.outer_iter_mut() {
795 row -= &self.mean;
796 }
797
798 Ok(centered.dot(&self.components))
799 }
800
801 pub fn get_covariance(&self) -> SklResult<Array2<f64>> {
802 let n_features = self.components.nrows();
803 let mut covariance = self.components.dot(&self.components.t());
804 for idx in 0..n_features {
805 covariance[[idx, idx]] += self.noise_variance[idx];
806 }
807 Ok(covariance)
808 }
809
810 pub fn score(&self, _X: &ArrayView2<f64>) -> SklResult<f64> {
811 let factors = self.transform(_X)?;
812 let mut reconstruction = factors.dot(&self.components.t());
813 for mut row in reconstruction.outer_iter_mut() {
814 row += &self.mean;
815 }
816
817 let diff = &reconstruction - _X;
818 let mse = diff.iter().map(|v| v * v).sum::<f64>() / diff.len().max(1) as f64;
819 Ok(-mse)
820 }
821}
822
823#[allow(non_snake_case)] impl FactorAnalysis {
825 pub fn new() -> Self {
826 Self {
827 config: DictLearningConfig::default(),
828 rotation: None,
829 }
830 }
831
832 pub fn n_components(mut self, n_components: usize) -> Self {
833 self.config.n_components = n_components;
834 self
835 }
836
837 pub fn max_iter(mut self, max_iter: usize) -> Self {
838 self.config.max_iter = max_iter;
839 self
840 }
841
842 pub fn tol(mut self, tol: f64) -> Self {
843 self.config.tol = tol;
844 self
845 }
846
847 pub fn rotation(mut self, rotation: &str) -> Self {
848 self.rotation = Some(rotation.to_string());
849 self
850 }
851
852 pub fn fit(&self, X: &ArrayView2<f64>, _y: &()) -> SklResult<FactorAnalysisTrained> {
853 let (n_samples, n_features) = X.dim();
854
855 if n_samples < 2 || n_features == 0 {
856 return Err(SklearsError::InvalidInput(
857 "Factor analysis requires at least two samples and non-zero features".to_string(),
858 ));
859 }
860
861 if self.config.n_components == 0
862 || self.config.n_components > usize::min(n_samples, n_features)
863 {
864 return Err(SklearsError::InvalidInput(format!(
865 "Requested {} factors but data dimensions are {}x{}",
866 self.config.n_components, n_samples, n_features
867 )));
868 }
869
870 if let Some(rotation) = &self.rotation {
871 let allowed = ["varimax", "none"];
872 if !allowed
873 .iter()
874 .any(|name| name.eq_ignore_ascii_case(rotation))
875 {
876 return Err(SklearsError::InvalidInput(format!(
877 "Unsupported factor rotation '{}'",
878 rotation
879 )));
880 }
881 }
882
883 let (mut centered, mean) = center_data(X);
884 let (components, eigenvalues) = principal_components(
885 &mut centered,
886 self.config.n_components,
887 self.config.max_iter.max(256),
888 self.config.tol,
889 )?;
890
891 let mut loadings = Array2::zeros((n_features, self.config.n_components));
892 for factor_idx in 0..self.config.n_components {
893 let scale = eigenvalues[factor_idx].max(1e-6).sqrt();
894 for feature_idx in 0..n_features {
895 loadings[[feature_idx, factor_idx]] = components[[factor_idx, feature_idx]] * scale;
896 }
897 }
898
899 if let Some(rotation) = &self.rotation {
900 if rotation.eq_ignore_ascii_case("varimax") {
901 loadings = varimax(loadings);
902 }
903 }
904
905 let mut noise_variance = Array1::zeros(n_features);
906 for feature_idx in 0..n_features {
907 let communality = (0..self.config.n_components)
908 .map(|k| loadings[[feature_idx, k]].powi(2))
909 .sum::<f64>();
910 let variance = centered
911 .column(feature_idx)
912 .iter()
913 .map(|v| v * v)
914 .sum::<f64>()
915 / (n_samples as f64 - 1.0);
916 noise_variance[feature_idx] = (variance - communality).max(1e-6);
917 }
918
919 Ok(FactorAnalysisTrained {
920 components: loadings,
921 noise_variance,
922 explained_variance: eigenvalues,
923 mean,
924 })
925 }
926}
927
928impl Default for FactorAnalysis {
929 fn default() -> Self {
930 Self::new()
931 }
932}
933
934#[derive(Debug, Clone)]
935pub struct ProbabilisticMatrixFactorization {
936 config: DictLearningConfig,
937 lambda_u: f64,
938 lambda_v: f64,
939 sigma: f64,
940 learning_rate: f64,
941 random_state: Option<u64>,
942}
943
944#[derive(Debug, Clone)]
945pub struct PMFTrained {
946 user_features: Array2<f64>,
947 item_features: Array2<f64>,
948 sigma: f64,
949 training_loss: f64,
950}
951
952#[allow(non_snake_case)] impl PMFTrained {
954 pub fn predict(&self, user_idx: usize, item_idx: usize) -> SklResult<f64> {
955 if user_idx >= self.user_features.nrows() {
956 return Err(SklearsError::InvalidInput(format!(
957 "User index {} out of bounds ({} users)",
958 user_idx,
959 self.user_features.nrows()
960 )));
961 }
962
963 if item_idx >= self.item_features.nrows() {
964 return Err(SklearsError::InvalidInput(format!(
965 "Item index {} out of bounds ({} items)",
966 item_idx,
967 self.item_features.nrows()
968 )));
969 }
970
971 let user = self.user_features.row(user_idx);
972 let item = self.item_features.row(item_idx);
973 Ok(user.dot(&item))
974 }
975
976 pub fn predict_batch(
977 &self,
978 user_indices: &[usize],
979 item_indices: &[usize],
980 ) -> SklResult<Vec<f64>> {
981 if user_indices.len() != item_indices.len() {
982 return Err(SklearsError::InvalidInput(
983 "User and item index lists must have the same length".to_string(),
984 ));
985 }
986
987 user_indices
988 .iter()
989 .zip(item_indices.iter())
990 .map(|(&u, &i)| self.predict(u, i))
991 .collect()
992 }
993
994 pub fn transform(&self, X: &ArrayView2<f64>) -> SklResult<Array2<f64>> {
995 let (n_users, n_items) = X.dim();
996 if n_users != self.user_features.nrows() || n_items != self.item_features.nrows() {
997 return Err(SklearsError::InvalidInput(format!(
998 "Expected matrix of shape ({}, {}) but got ({}, {})",
999 self.user_features.nrows(),
1000 self.item_features.nrows(),
1001 n_users,
1002 n_items
1003 )));
1004 }
1005
1006 Ok(X.dot(&self.item_features))
1007 }
1008
1009 pub fn get_user_features(&self) -> &Array2<f64> {
1010 &self.user_features
1011 }
1012
1013 pub fn get_item_features(&self) -> &Array2<f64> {
1014 &self.item_features
1015 }
1016
1017 pub fn reconstruct(&self) -> Array2<f64> {
1018 self.user_features.dot(&self.item_features.t())
1019 }
1020
1021 pub fn get_training_loss(&self) -> f64 {
1022 self.training_loss
1023 }
1024
1025 pub fn log_probability(&self, user_idx: usize, item_idx: usize, rating: f64) -> SklResult<f64> {
1026 let prediction = self.predict(user_idx, item_idx)?;
1027 let sigma = self.sigma.max(1e-6);
1028 let diff = rating - prediction;
1029 let log_prob = -0.5 * (diff * diff) / (sigma * sigma) - (sigma * (2.0 * PI).sqrt()).ln();
1030 Ok(log_prob)
1031 }
1032}
1033
1034#[allow(non_snake_case)] impl ProbabilisticMatrixFactorization {
1036 pub fn new() -> Self {
1037 Self {
1038 config: DictLearningConfig::default(),
1039 lambda_u: 0.0,
1040 lambda_v: 0.0,
1041 sigma: 1.0,
1042 learning_rate: 0.01,
1043 random_state: None,
1044 }
1045 }
1046
1047 pub fn n_components(mut self, n_components: usize) -> Self {
1048 self.config.n_components = n_components;
1049 self
1050 }
1051
1052 pub fn lambda_u(mut self, lambda_u: f64) -> Self {
1053 self.lambda_u = lambda_u.max(0.0);
1054 self
1055 }
1056
1057 pub fn lambda_v(mut self, lambda_v: f64) -> Self {
1058 self.lambda_v = lambda_v.max(0.0);
1059 self
1060 }
1061
1062 pub fn max_iter(mut self, max_iter: usize) -> Self {
1063 self.config.max_iter = max_iter;
1064 self
1065 }
1066
1067 pub fn sigma(mut self, sigma: f64) -> Self {
1068 self.sigma = sigma.max(1e-6);
1069 self
1070 }
1071
1072 pub fn random_state(mut self, random_state: Option<u64>) -> Self {
1073 self.random_state = random_state;
1074 self
1075 }
1076
1077 pub fn learning_rate(mut self, learning_rate: f64) -> Self {
1078 self.learning_rate = learning_rate.max(1e-6);
1079 self
1080 }
1081
1082 pub fn fit(&self, X: &ArrayView2<f64>, _y: &()) -> SklResult<PMFTrained> {
1083 let (n_users, n_items) = X.dim();
1084
1085 if n_users == 0 || n_items == 0 {
1086 return Err(SklearsError::InvalidInput(
1087 "PMF requires a non-empty rating matrix".to_string(),
1088 ));
1089 }
1090
1091 if self.config.n_components == 0 || self.config.n_components > usize::min(n_users, n_items)
1092 {
1093 return Err(SklearsError::InvalidInput(format!(
1094 "Requested {} latent components but matrix dimensions are {}x{}",
1095 self.config.n_components, n_users, n_items
1096 )));
1097 }
1098
1099 let k = self.config.n_components;
1100 let mut user_features = initialize_features(n_users, k, self.random_state.unwrap_or(0));
1101 let mut item_features = initialize_features(n_items, k, self.random_state.unwrap_or(0) + 1);
1102
1103 let learning_rate = self.learning_rate;
1104 let iterations = self.config.max_iter.clamp(1, 500);
1105
1106 for _ in 0..iterations {
1107 for user_idx in 0..n_users {
1108 for item_idx in 0..n_items {
1109 let rating = X[[user_idx, item_idx]];
1110 if rating == 0.0 {
1111 continue;
1112 }
1113
1114 let prediction = user_features
1115 .row(user_idx)
1116 .dot(&item_features.row(item_idx));
1117 let error = rating - prediction;
1118
1119 for component_idx in 0..k {
1120 let u_val = user_features[[user_idx, component_idx]];
1121 let v_val = item_features[[item_idx, component_idx]];
1122
1123 user_features[[user_idx, component_idx]] +=
1124 learning_rate * (error * v_val - self.lambda_u * u_val);
1125 item_features[[item_idx, component_idx]] +=
1126 learning_rate * (error * u_val - self.lambda_v * v_val);
1127 }
1128 }
1129 }
1130 }
1131
1132 let training_loss = compute_training_loss(
1133 X,
1134 &user_features,
1135 &item_features,
1136 self.lambda_u,
1137 self.lambda_v,
1138 );
1139
1140 Ok(PMFTrained {
1141 user_features,
1142 item_features,
1143 sigma: self.sigma,
1144 training_loss,
1145 })
1146 }
1147}
1148
1149impl Default for ProbabilisticMatrixFactorization {
1150 fn default() -> Self {
1151 Self::new()
1152 }
1153}
1154
1155#[derive(Debug, Clone)]
1156pub struct TensorDecompositionConfig {
1157 pub rank: usize,
1159 pub max_iter: usize,
1161 pub tol: f64,
1163}
1164
1165impl Default for TensorDecompositionConfig {
1166 fn default() -> Self {
1167 Self {
1168 rank: 10,
1169 max_iter: 100,
1170 tol: 1e-6,
1171 }
1172 }
1173}
1174
1175#[derive(Debug, Clone)]
1176pub struct CPDecomposition {
1177 config: TensorDecompositionConfig,
1178}
1179
1180#[derive(Debug, Clone)]
1181pub struct CPResult {
1182 pub factors: Vec<Array2<f64>>,
1184 pub reconstruction_error: f64,
1186}
1187
1188impl CPDecomposition {
1189 pub fn new() -> Self {
1190 Self {
1191 config: TensorDecompositionConfig::default(),
1192 }
1193 }
1194
1195 pub fn rank(mut self, rank: usize) -> Self {
1196 self.config.rank = rank;
1197 self
1198 }
1199}
1200
1201impl Default for CPDecomposition {
1202 fn default() -> Self {
1203 Self::new()
1204 }
1205}
1206
1207#[allow(non_snake_case)] fn center_data(X: &ArrayView2<f64>) -> (Array2<f64>, Array1<f64>) {
1209 let mean = X
1210 .mean_axis(Axis(0))
1211 .unwrap_or_else(|| Array1::zeros(X.ncols()));
1212
1213 let mut centered = X.to_owned();
1214 for mut row in centered.outer_iter_mut() {
1215 row -= &mean;
1216 }
1217
1218 (centered, mean)
1219}
1220
1221fn principal_components(
1222 centered: &mut Array2<f64>,
1223 n_components: usize,
1224 max_iter: usize,
1225 tol: f64,
1226) -> SklResult<(Array2<f64>, Array1<f64>)> {
1227 let (n_samples, n_features) = centered.dim();
1228 if n_samples < 2 || n_features == 0 {
1229 return Err(SklearsError::InvalidInput(
1230 "Principal component analysis requires at least two samples".to_string(),
1231 ));
1232 }
1233
1234 let denom = (n_samples as f64 - 1.0).max(1.0);
1235 let mut covariance = centered.t().dot(centered) / denom;
1236 let mut components = Array2::zeros((n_components, n_features));
1237 let mut eigenvalues = Array1::zeros(n_components);
1238
1239 for component_idx in 0..n_components {
1240 let (vector, eigenvalue) = power_iteration(&covariance, max_iter.max(128), tol.max(1e-9));
1241
1242 components.row_mut(component_idx).assign(&vector.view());
1243 eigenvalues[component_idx] = eigenvalue;
1244
1245 for i in 0..n_features {
1246 for j in 0..n_features {
1247 covariance[[i, j]] -= eigenvalue * vector[i] * vector[j];
1248 }
1249 }
1250 }
1251
1252 Ok((components, eigenvalues))
1253}
1254
1255fn power_iteration(matrix: &Array2<f64>, max_iter: usize, tol: f64) -> (Array1<f64>, f64) {
1256 let n = matrix.ncols();
1257 let mut vector = Array1::from_elem(n, 1.0 / (n as f64).sqrt().max(1e-6));
1258 let iterations = max_iter.max(128);
1259 let tolerance = tol.max(1e-9);
1260
1261 for _ in 0..iterations {
1262 let next = matrix.dot(&vector);
1263 let norm = next.iter().map(|v| v * v).sum::<f64>().sqrt();
1264 if norm == 0.0 {
1265 break;
1266 }
1267 let next_vector = next.mapv(|v| v / norm);
1268 let diff = (&next_vector - &vector)
1269 .iter()
1270 .map(|v| v.abs())
1271 .sum::<f64>();
1272 vector = next_vector;
1273 if diff < tolerance {
1274 break;
1275 }
1276 }
1277
1278 let eigenvalue = vector.dot(&matrix.dot(&vector)).max(0.0);
1279 (vector, eigenvalue)
1280}
1281
1282fn varimax(loadings: Array2<f64>) -> Array2<f64> {
1283 loadings
1285}
1286
1287fn initialize_features(n_rows: usize, n_components: usize, seed: u64) -> Array2<f64> {
1288 let base = (seed as f64 + 1.0).max(1.0);
1289 Array2::from_shape_fn((n_rows, n_components), |(row, comp)| {
1290 let value = (base + (row + 1) as f64 * (comp + 1) as f64).sin();
1291 value.abs() + 0.1
1292 })
1293}
1294
1295fn compute_training_loss(
1296 ratings: &ArrayView2<f64>,
1297 user_features: &Array2<f64>,
1298 item_features: &Array2<f64>,
1299 lambda_u: f64,
1300 lambda_v: f64,
1301) -> f64 {
1302 let (n_users, n_items) = ratings.dim();
1303 let mut loss = 0.0;
1304
1305 for user_idx in 0..n_users {
1306 for item_idx in 0..n_items {
1307 let rating = ratings[[user_idx, item_idx]];
1308 if rating == 0.0 {
1309 continue;
1310 }
1311
1312 let prediction = user_features
1313 .row(user_idx)
1314 .dot(&item_features.row(item_idx));
1315 let diff = rating - prediction;
1316 loss += diff * diff;
1317 }
1318 }
1319
1320 if lambda_u > 0.0 {
1321 loss += lambda_u * user_features.iter().map(|v| v * v).sum::<f64>();
1322 }
1323
1324 if lambda_v > 0.0 {
1325 loss += lambda_v * item_features.iter().map(|v| v * v).sum::<f64>();
1326 }
1327
1328 loss / 2.0
1329}