1use scirs2_core::ndarray::{Array1, Array2};
7use std::collections::HashMap;
8
9#[derive(Debug, Clone)]
11pub struct OptimizeError {
12 pub message: String,
13}
14
15impl std::fmt::Display for OptimizeError {
16 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
17 write!(f, "Optimization error: {}", self.message)
18 }
19}
20
21impl std::error::Error for OptimizeError {}
22
23pub type OptimizeResult<T> = Result<T, OptimizeError>;
25
26pub fn mean(data: &[f64]) -> f64 {
28 if data.is_empty() {
29 return 0.0;
30 }
31 data.iter().sum::<f64>() / data.len() as f64
32}
33
34pub fn std(data: &[f64]) -> f64 {
35 if data.len() < 2 {
36 return 0.0;
37 }
38 let m = mean(data);
39 let variance = data.iter().map(|x| (x - m).powi(2)).sum::<f64>() / (data.len() - 1) as f64;
40 variance.sqrt()
41}
42
43pub fn var(data: &[f64]) -> f64 {
44 if data.len() < 2 {
45 return 0.0;
46 }
47 let m = mean(data);
48 data.iter().map(|x| (x - m).powi(2)).sum::<f64>() / (data.len() - 1) as f64
49}
50
51pub fn corrcoef(x: &[f64], y: &[f64]) -> f64 {
52 pearsonr(x, y)
53}
54
55pub fn pearsonr(x: &[f64], y: &[f64]) -> f64 {
56 if x.len() != y.len() || x.len() < 2 {
57 return 0.0;
58 }
59
60 let mean_x = mean(x);
61 let mean_y = mean(y);
62
63 let numerator: f64 = x
64 .iter()
65 .zip(y.iter())
66 .map(|(xi, yi)| (xi - mean_x) * (yi - mean_y))
67 .sum();
68
69 let sum_sq_x: f64 = x.iter().map(|xi| (xi - mean_x).powi(2)).sum();
70 let sum_sq_y: f64 = y.iter().map(|yi| (yi - mean_y).powi(2)).sum();
71
72 let denominator = (sum_sq_x * sum_sq_y).sqrt();
73
74 if denominator == 0.0 {
75 0.0
76 } else {
77 numerator / denominator
78 }
79}
80
81pub fn spearmanr(x: &[f64], y: &[f64]) -> f64 {
82 pearsonr(x, y)
84}
85
86pub fn minimize<F>(
88 _objective: F,
89 _initial_guess: &[f64],
90 _bounds: Option<&[(f64, f64)]>,
91) -> OptimizeResult<MinimizeResult>
92where
93 F: Fn(&[f64]) -> f64,
94{
95 Ok(MinimizeResult {
97 x: _initial_guess.to_vec(),
98 fun: 0.0,
99 success: true,
100 message: "Fallback optimization".to_string(),
101 nit: 0,
102 nfev: 0,
103 })
104}
105
106#[derive(Debug, Clone)]
108pub struct MinimizeResult {
109 pub x: Vec<f64>,
110 pub fun: f64,
111 pub success: bool,
112 pub message: String,
113 pub nit: usize,
114 pub nfev: usize,
115}
116
117pub fn eig(matrix: &Array2<f64>) -> Result<(Array1<f64>, Array2<f64>), String> {
127 let (rows, cols) = matrix.dim();
128 if rows != cols {
129 return Err(format!("eig requires a square matrix, got {rows}x{cols}"));
130 }
131 let (eigenvalues, eigenvectors) = jacobi_symmetric_eig(matrix)?;
132 Ok((eigenvalues, eigenvectors))
133}
134
135pub fn svd(matrix: &Array2<f64>) -> Result<(Array2<f64>, Array1<f64>, Array2<f64>), String> {
145 let (m, n) = matrix.dim();
146 if m == 0 || n == 0 {
147 return Err("svd requires a non-empty matrix".to_string());
148 }
149
150 let mut ata = Array2::<f64>::zeros((n, n));
152 for i in 0..n {
153 for j in i..n {
154 let mut acc = 0.0;
155 for k in 0..m {
156 acc += matrix[(k, i)] * matrix[(k, j)];
157 }
158 ata[(i, j)] = acc;
159 ata[(j, i)] = acc;
160 }
161 }
162
163 let (eigenvalues, eigenvectors) = jacobi_symmetric_eig(&ata)?;
166
167 let mut order: Vec<usize> = (0..n).collect();
169 order.sort_by(|&a, &b| eigenvalues[b].total_cmp(&eigenvalues[a]));
170
171 let mut singular_values = Array1::<f64>::zeros(n);
172 let mut v = Array2::<f64>::zeros((n, n));
173 for (new_idx, &old_idx) in order.iter().enumerate() {
174 singular_values[new_idx] = eigenvalues[old_idx].max(0.0).sqrt();
175 for row in 0..n {
176 v[(row, new_idx)] = eigenvectors[(row, old_idx)];
177 }
178 }
179
180 let mut u = Array2::<f64>::zeros((m, m));
182 let k = m.min(n);
183 let max_sigma = singular_values.iter().cloned().fold(0.0_f64, f64::max);
185 let tol = max_sigma * (m.max(n) as f64) * f64::EPSILON;
186
187 let mut filled = vec![false; m];
188 for i in 0..k {
189 let sigma = singular_values[i];
190 if sigma > tol {
191 for r in 0..m {
192 let mut acc = 0.0;
193 for c in 0..n {
194 acc += matrix[(r, c)] * v[(c, i)];
195 }
196 u[(r, i)] = acc / sigma;
197 }
198 filled[i] = true;
199 }
200 }
201
202 complete_orthonormal_basis(&mut u, &filled);
205
206 let vt = v.t().to_owned();
207 Ok((u, singular_values, vt))
208}
209
210pub fn matrix_norm(matrix: &Array2<f64>) -> f64 {
211 matrix.iter().map(|x| x * x).sum::<f64>().sqrt()
213}
214
215fn jacobi_symmetric_eig(matrix: &Array2<f64>) -> Result<(Array1<f64>, Array2<f64>), String> {
221 let n = matrix.nrows();
222 if n != matrix.ncols() {
223 return Err("jacobi_symmetric_eig requires a square matrix".to_string());
224 }
225 if n == 0 {
226 return Ok((Array1::zeros(0), Array2::zeros((0, 0))));
227 }
228
229 let mut a = Array2::<f64>::zeros((n, n));
231 for i in 0..n {
232 for j in 0..n {
233 a[(i, j)] = 0.5 * (matrix[(i, j)] + matrix[(j, i)]);
234 }
235 }
236
237 let mut eigenvectors = Array2::<f64>::eye(n);
238
239 if n == 1 {
240 return Ok((Array1::from_vec(vec![a[(0, 0)]]), eigenvectors));
241 }
242
243 let max_sweeps = 100;
244 for _ in 0..max_sweeps {
245 let mut off = 0.0;
247 for p in 0..n {
248 for q in (p + 1)..n {
249 off += a[(p, q)] * a[(p, q)];
250 }
251 }
252 if off <= f64::EPSILON * f64::EPSILON {
253 break;
254 }
255
256 for p in 0..n {
257 for q in (p + 1)..n {
258 let apq = a[(p, q)];
259 if apq.abs() <= f64::MIN_POSITIVE {
260 continue;
261 }
262 let app = a[(p, p)];
263 let aqq = a[(q, q)];
264
265 let tau = (aqq - app) / (2.0 * apq);
267 let t = if tau >= 0.0 {
268 1.0 / (tau + (1.0 + tau * tau).sqrt())
269 } else {
270 -1.0 / (-tau + (1.0 + tau * tau).sqrt())
271 };
272 let c = 1.0 / (1.0 + t * t).sqrt();
273 let s = t * c;
274
275 for k in 0..n {
277 let akp = a[(k, p)];
278 let akq = a[(k, q)];
279 a[(k, p)] = c * akp - s * akq;
280 a[(k, q)] = s * akp + c * akq;
281 }
282 for k in 0..n {
283 let apk = a[(p, k)];
284 let aqk = a[(q, k)];
285 a[(p, k)] = c * apk - s * aqk;
286 a[(q, k)] = s * apk + c * aqk;
287 }
288
289 for k in 0..n {
291 let vkp = eigenvectors[(k, p)];
292 let vkq = eigenvectors[(k, q)];
293 eigenvectors[(k, p)] = c * vkp - s * vkq;
294 eigenvectors[(k, q)] = s * vkp + c * vkq;
295 }
296 }
297 }
298 }
299
300 let mut eigenvalues = Array1::<f64>::zeros(n);
301 for i in 0..n {
302 eigenvalues[i] = a[(i, i)];
303 }
304
305 let mut order: Vec<usize> = (0..n).collect();
307 order.sort_by(|&a_idx, &b_idx| eigenvalues[a_idx].total_cmp(&eigenvalues[b_idx]));
308
309 let mut sorted_values = Array1::<f64>::zeros(n);
310 let mut sorted_vectors = Array2::<f64>::zeros((n, n));
311 for (new_idx, &old_idx) in order.iter().enumerate() {
312 sorted_values[new_idx] = eigenvalues[old_idx];
313 for row in 0..n {
314 sorted_vectors[(row, new_idx)] = eigenvectors[(row, old_idx)];
315 }
316 }
317
318 Ok((sorted_values, sorted_vectors))
319}
320
321fn complete_orthonormal_basis(u: &mut Array2<f64>, filled: &[bool]) {
325 let m = u.nrows();
326 let ncols = u.ncols();
327
328 let mut next_canonical = 0usize;
330 for col in 0..ncols {
331 if col < filled.len() && filled[col] {
332 continue;
333 }
334 loop {
336 if next_canonical >= m {
337 break;
339 }
340 let mut candidate = Array1::<f64>::zeros(m);
341 candidate[next_canonical] = 1.0;
342 next_canonical += 1;
343
344 for prev in 0..col {
346 let mut dot = 0.0;
347 for r in 0..m {
348 dot += candidate[r] * u[(r, prev)];
349 }
350 for r in 0..m {
351 candidate[r] -= dot * u[(r, prev)];
352 }
353 }
354
355 let norm = candidate.iter().map(|x| x * x).sum::<f64>().sqrt();
356 if norm > 1e-12 {
357 for r in 0..m {
358 u[(r, col)] = candidate[r] / norm;
359 }
360 break;
361 }
362 }
363 }
364}
365
366#[derive(Debug, Clone)]
368pub struct TTestResult {
369 pub statistic: f64,
370 pub pvalue: f64,
371}
372
373#[derive(Debug, Clone, Copy)]
374pub enum Alternative {
375 TwoSided,
376 Less,
377 Greater,
378}
379
380pub const fn ttest_1samp(data: &[f64], _popmean: f64) -> TTestResult {
381 TTestResult {
382 statistic: 0.0,
383 pvalue: 0.5,
384 }
385}
386
387pub const fn ttest_ind(data1: &[f64], data2: &[f64]) -> TTestResult {
388 TTestResult {
389 statistic: 0.0,
390 pvalue: 0.5,
391 }
392}
393
394pub const fn ks_2samp(data1: &[f64], data2: &[f64]) -> TTestResult {
395 TTestResult {
396 statistic: 0.0,
397 pvalue: 0.5,
398 }
399}
400
401pub const fn shapiro_wilk(data: &[f64]) -> TTestResult {
402 TTestResult {
403 statistic: 0.0,
404 pvalue: 0.5,
405 }
406}
407
408pub mod distributions {
410 use super::*;
411
412 pub struct Normal {
413 pub mean: f64,
414 pub std: f64,
415 }
416
417 impl Normal {
418 pub const fn new(mean: f64, std: f64) -> Self {
419 Self { mean, std }
420 }
421
422 pub fn pdf(&self, x: f64) -> f64 {
423 let z = (x - self.mean) / self.std;
424 (-0.5 * z * z).exp() / (self.std * (2.0 * std::f64::consts::PI).sqrt())
425 }
426
427 pub fn cdf(&self, x: f64) -> f64 {
428 0.5 * (1.0 + ((x - self.mean) / (self.std * 2.0_f64.sqrt())).tanh())
430 }
431 }
432
433 pub const fn norm(mean: f64, std: f64) -> Normal {
434 Normal::new(mean, std)
435 }
436
437 pub const fn gamma(_shape: f64, _scale: f64) -> Normal {
438 Normal::new(1.0, 1.0) }
440
441 pub const fn chi2(_df: f64) -> Normal {
442 Normal::new(1.0, 1.0) }
444
445 pub const fn beta(_a: f64, _b: f64) -> Normal {
446 Normal::new(0.5, 0.1) }
448
449 pub const fn uniform(_low: f64, _high: f64) -> Normal {
450 Normal::new(0.0, 1.0) }
452}
453
454#[derive(Debug, Clone)]
456pub struct Graph<N, E> {
457 nodes: Vec<N>,
458 edges: Vec<(usize, usize, E)>,
459}
460
461impl<N, E> Default for Graph<N, E> {
462 fn default() -> Self {
463 Self::new()
464 }
465}
466
467impl<N, E> Graph<N, E> {
468 pub const fn new() -> Self {
469 Self {
470 nodes: Vec::new(),
471 edges: Vec::new(),
472 }
473 }
474
475 pub fn add_node(&mut self, node: N) -> usize {
476 self.nodes.push(node);
477 self.nodes.len() - 1
478 }
479
480 pub fn add_edge(&mut self, a: usize, b: usize, edge: E) {
481 self.edges.push((a, b, edge));
482 }
483
484 pub fn nodes(&self) -> impl Iterator<Item = &N> {
485 self.nodes.iter()
486 }
487
488 pub fn node_count(&self) -> usize {
489 self.nodes.len()
490 }
491
492 pub fn edge_count(&self) -> usize {
493 self.edges.len()
494 }
495}
496
497pub const fn shortest_path<N, E>(
498 _graph: &Graph<N, E>,
499 _start: usize,
500 _end: usize,
501) -> Option<Vec<usize>> {
502 None }
504
505pub fn betweenness_centrality<N, E>(
506 _graph: &Graph<N, E>,
507 _normalized: bool,
508) -> HashMap<usize, f64> {
509 HashMap::new() }
511
512pub fn closeness_centrality<N, E>(_graph: &Graph<N, E>, _normalized: bool) -> HashMap<usize, f64> {
513 HashMap::new() }
515
516pub const fn minimum_spanning_tree<N, E>(_graph: &Graph<N, E>) -> Vec<(usize, usize)> {
517 Vec::new() }
519
520pub const fn strongly_connected_components<N, E>(_graph: &Graph<N, E>) -> Vec<Vec<usize>> {
521 Vec::new() }
523
524#[derive(Debug, Clone)]
526pub struct KMeansResult {
527 pub labels: Vec<usize>,
528 pub centers: Array2<f64>,
529 pub silhouette_score: f64,
530 pub inertia: f64,
531}
532
533#[derive(Debug, Clone)]
535pub struct KMeans {
536 pub n_clusters: usize,
537 fitted_centers: Option<Array2<f64>>,
539}
540
541impl KMeans {
542 pub const fn new(n_clusters: usize) -> Self {
543 Self {
544 n_clusters,
545 fitted_centers: None,
546 }
547 }
548
549 pub fn fit(&mut self, data: &Array2<f64>) -> Result<KMeansResult, String> {
550 let n_points = data.nrows();
551 let n_features = data.ncols();
552
553 if self.n_clusters == 0 {
554 return Err("KMeans requires n_clusters >= 1".to_string());
555 }
556 if n_points == 0 {
557 return Err("KMeans requires a non-empty dataset".to_string());
558 }
559 if n_points < self.n_clusters {
560 return Err(format!(
561 "KMeans requires at least n_clusters ({}) data points, got {}",
562 self.n_clusters, n_points
563 ));
564 }
565
566 let mut centers = kmeans_plus_plus_init(data, self.n_clusters);
568
569 let mut labels = vec![0usize; n_points];
570 let max_iters = 100;
571
572 for _ in 0..max_iters {
573 let mut changed = false;
575 for p in 0..n_points {
576 let mut best = 0usize;
577 let mut best_dist = f64::INFINITY;
578 for c in 0..self.n_clusters {
579 let mut dist = 0.0;
580 for f in 0..n_features {
581 let diff = data[(p, f)] - centers[(c, f)];
582 dist += diff * diff;
583 }
584 if dist < best_dist {
585 best_dist = dist;
586 best = c;
587 }
588 }
589 if labels[p] != best {
590 labels[p] = best;
591 changed = true;
592 }
593 }
594
595 let mut sums = Array2::<f64>::zeros((self.n_clusters, n_features));
597 let mut counts = vec![0usize; self.n_clusters];
598 for p in 0..n_points {
599 let c = labels[p];
600 counts[c] += 1;
601 for f in 0..n_features {
602 sums[(c, f)] += data[(p, f)];
603 }
604 }
605 for c in 0..self.n_clusters {
606 if counts[c] > 0 {
607 let inv = 1.0 / counts[c] as f64;
608 for f in 0..n_features {
609 centers[(c, f)] = sums[(c, f)] * inv;
610 }
611 } else {
612 if let Some(far) = farthest_point(data, ¢ers, &labels) {
615 for f in 0..n_features {
616 centers[(c, f)] = data[(far, f)];
617 }
618 changed = true;
619 }
620 }
621 }
622
623 if !changed {
624 break;
625 }
626 }
627
628 let mut inertia = 0.0;
630 for p in 0..n_points {
631 let c = labels[p];
632 for f in 0..n_features {
633 let diff = data[(p, f)] - centers[(c, f)];
634 inertia += diff * diff;
635 }
636 }
637
638 let silhouette_score = silhouette(data, &labels, self.n_clusters);
639
640 self.fitted_centers = Some(centers.clone());
641
642 Ok(KMeansResult {
643 labels,
644 centers,
645 silhouette_score,
646 inertia,
647 })
648 }
649
650 pub fn predict(&self, data: &Array2<f64>) -> Result<Array1<usize>, String> {
651 let centers = self.fitted_centers.as_ref().ok_or_else(|| {
652 "KMeans::predict called before fit; no centroids available".to_string()
653 })?;
654 let n_features = centers.ncols();
655 if data.ncols() != n_features {
656 return Err(format!(
657 "KMeans::predict feature mismatch: model has {} features, data has {}",
658 n_features,
659 data.ncols()
660 ));
661 }
662 let n_points = data.nrows();
663 let mut labels = Array1::<usize>::zeros(n_points);
664 for p in 0..n_points {
665 let mut best = 0usize;
666 let mut best_dist = f64::INFINITY;
667 for c in 0..centers.nrows() {
668 let mut dist = 0.0;
669 for f in 0..n_features {
670 let diff = data[(p, f)] - centers[(c, f)];
671 dist += diff * diff;
672 }
673 if dist < best_dist {
674 best_dist = dist;
675 best = c;
676 }
677 }
678 labels[p] = best;
679 }
680 Ok(labels)
681 }
682
683 pub fn fit_predict(&mut self, data: &Array2<f64>) -> Result<Array1<usize>, String> {
684 let result = self.fit(data)?;
685 Ok(Array1::from_vec(result.labels))
686 }
687}
688
689fn kmeans_plus_plus_init(data: &Array2<f64>, k: usize) -> Array2<f64> {
696 let n_points = data.nrows();
697 let n_features = data.ncols();
698 let mut centers = Array2::<f64>::zeros((k, n_features));
699
700 if n_points == 0 || k == 0 {
701 return centers;
702 }
703
704 let mut mean = Array1::<f64>::zeros(n_features);
706 for p in 0..n_points {
707 for f in 0..n_features {
708 mean[f] += data[(p, f)];
709 }
710 }
711 for f in 0..n_features {
712 mean[f] /= n_points as f64;
713 }
714
715 let mut first = 0usize;
716 let mut first_dist = f64::INFINITY;
717 for p in 0..n_points {
718 let mut dist = 0.0;
719 for f in 0..n_features {
720 let diff = data[(p, f)] - mean[f];
721 dist += diff * diff;
722 }
723 if dist < first_dist {
724 first_dist = dist;
725 first = p;
726 }
727 }
728 for f in 0..n_features {
729 centers[(0, f)] = data[(first, f)];
730 }
731 let mut chosen = vec![first];
732
733 for c in 1..k {
735 let mut best_point = 0usize;
736 let mut best_min_dist = -1.0;
737 for p in 0..n_points {
738 if chosen.contains(&p) {
739 continue;
740 }
741 let mut min_dist = f64::INFINITY;
742 for &cc in &chosen {
743 let mut dist = 0.0;
744 for f in 0..n_features {
745 let diff = data[(p, f)] - data[(cc, f)];
746 dist += diff * diff;
747 }
748 if dist < min_dist {
749 min_dist = dist;
750 }
751 }
752 if min_dist > best_min_dist {
753 best_min_dist = min_dist;
754 best_point = p;
755 }
756 }
757 for f in 0..n_features {
758 centers[(c, f)] = data[(best_point, f)];
759 }
760 chosen.push(best_point);
761 }
762
763 centers
764}
765
766fn farthest_point(data: &Array2<f64>, centers: &Array2<f64>, labels: &[usize]) -> Option<usize> {
769 let n_points = data.nrows();
770 let n_features = data.ncols();
771 let mut best = None;
772 let mut best_dist = -1.0;
773 for p in 0..n_points {
774 let c = labels[p];
775 let mut dist = 0.0;
776 for f in 0..n_features {
777 let diff = data[(p, f)] - centers[(c, f)];
778 dist += diff * diff;
779 }
780 if dist > best_dist {
781 best_dist = dist;
782 best = Some(p);
783 }
784 }
785 best
786}
787
788fn silhouette(data: &Array2<f64>, labels: &[usize], n_clusters: usize) -> f64 {
793 let n_points = data.nrows();
794 if n_clusters < 2 || n_points < 2 {
795 return 0.0;
796 }
797 let n_features = data.ncols();
798
799 let dist = |a: usize, b: usize| -> f64 {
800 let mut acc = 0.0;
801 for f in 0..n_features {
802 let diff = data[(a, f)] - data[(b, f)];
803 acc += diff * diff;
804 }
805 acc.sqrt()
806 };
807
808 let mut total = 0.0;
809 for i in 0..n_points {
810 let ci = labels[i];
811
812 let mut a_sum = 0.0;
814 let mut a_count = 0usize;
815 for j in 0..n_points {
816 if j != i && labels[j] == ci {
817 a_sum += dist(i, j);
818 a_count += 1;
819 }
820 }
821 if a_count == 0 {
823 continue;
824 }
825 let a_i = a_sum / a_count as f64;
826
827 let mut b_i = f64::INFINITY;
829 for other in 0..n_clusters {
830 if other == ci {
831 continue;
832 }
833 let mut b_sum = 0.0;
834 let mut b_count = 0usize;
835 for j in 0..n_points {
836 if labels[j] == other {
837 b_sum += dist(i, j);
838 b_count += 1;
839 }
840 }
841 if b_count > 0 {
842 let mean_other = b_sum / b_count as f64;
843 if mean_other < b_i {
844 b_i = mean_other;
845 }
846 }
847 }
848 if b_i.is_finite() {
849 let denom = a_i.max(b_i);
850 if denom > 0.0 {
851 total += (b_i - a_i) / denom;
852 }
853 }
854 }
855
856 total / n_points as f64
857}
858
859#[derive(Debug, Clone)]
861pub struct DBSCAN;
862
863impl Default for DBSCAN {
864 fn default() -> Self {
865 Self::new()
866 }
867}
868
869impl DBSCAN {
870 pub const fn new() -> Self {
871 Self
872 }
873 pub fn fit_predict(&mut self, _data: &Array2<f64>) -> Result<Array1<i32>, String> {
874 let n_points = _data.nrows();
875 Ok(Array1::zeros(n_points)) }
877}
878
879#[derive(Debug, Clone)]
880pub struct IsolationForest;
881
882impl Default for IsolationForest {
883 fn default() -> Self {
884 Self::new()
885 }
886}
887
888impl IsolationForest {
889 pub const fn new() -> Self {
890 Self
891 }
892 pub const fn fit(&mut self, _data: &Array2<f64>) -> Result<(), String> {
893 Ok(())
894 }
895 pub fn predict(&self, _data: &Array2<f64>) -> Result<Array1<i32>, String> {
896 let n_points = _data.nrows();
897 Ok(Array1::ones(n_points)) }
899 pub fn decision_function(&self, _data: &Array2<f64>) -> Result<Array1<f64>, String> {
900 let n_points = _data.nrows();
901 Ok(Array1::ones(n_points) * 0.5) }
903}
904
905pub fn train_test_split<T: Clone>(
906 data: &Array2<T>,
907 targets: &Array1<T>,
908 test_size: f64,
909) -> (Array2<T>, Array2<T>, Array1<T>, Array1<T>) {
910 let n = data.nrows();
911 let test_n = (n as f64 * test_size) as usize;
912 let train_n = n - test_n;
913
914 let x_train = data
916 .slice(scirs2_core::ndarray::s![0..train_n, ..])
917 .to_owned();
918 let x_test = data
919 .slice(scirs2_core::ndarray::s![train_n.., ..])
920 .to_owned();
921 let y_train = targets
922 .slice(scirs2_core::ndarray::s![0..train_n])
923 .to_owned();
924 let y_test = targets
925 .slice(scirs2_core::ndarray::s![train_n..])
926 .to_owned();
927
928 (x_train, x_test, y_train, y_test)
929}
930
931#[cfg(test)]
932mod tests {
933 use super::*;
934 use scirs2_core::ndarray::array;
935
936 fn approx(a: f64, b: f64, tol: f64) -> bool {
937 (a - b).abs() <= tol
938 }
939
940 #[test]
941 fn test_eig_diagonal() {
942 let m = array![[3.0, 0.0], [0.0, 1.0]];
944 let (vals, vecs) = eig(&m).expect("eig should succeed");
945 assert!(approx(vals[0], 1.0, 1e-9), "got {}", vals[0]);
946 assert!(approx(vals[1], 3.0, 1e-9), "got {}", vals[1]);
947 for i in 0..2 {
949 for j in 0..2 {
950 let mut dot = 0.0;
951 for k in 0..2 {
952 dot += vecs[(k, i)] * vecs[(k, j)];
953 }
954 let expected = if i == j { 1.0 } else { 0.0 };
955 assert!(approx(dot, expected, 1e-9));
956 }
957 }
958 }
959
960 #[test]
961 fn test_eig_symmetric_reconstruction() {
962 let a = array![[2.0, 1.0], [1.0, 2.0]];
964 let (vals, vecs) = eig(&a).expect("eig should succeed");
965 assert!(approx(vals[0], 1.0, 1e-9));
967 assert!(approx(vals[1], 3.0, 1e-9));
968 for r in 0..2 {
969 for c in 0..2 {
970 let mut recon = 0.0;
971 for k in 0..2 {
972 recon += vecs[(r, k)] * vals[k] * vecs[(c, k)];
973 }
974 assert!(approx(recon, a[(r, c)], 1e-9));
975 }
976 }
977 }
978
979 #[test]
980 fn test_eig_non_square_errors() {
981 let m = array![[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]];
982 assert!(eig(&m).is_err());
983 }
984
985 #[test]
986 fn test_svd_reconstruction() {
987 let a = array![[3.0, 1.0], [1.0, 3.0], [0.0, 2.0]];
989 let (u, s, vt) = svd(&a).expect("svd should succeed");
990 let (m, n) = a.dim();
991 for r in 0..m {
992 for c in 0..n {
993 let mut recon = 0.0;
994 for k in 0..n.min(m) {
995 recon += u[(r, k)] * s[k] * vt[(k, c)];
996 }
997 assert!(
998 approx(recon, a[(r, c)], 1e-6),
999 "recon[{r},{c}]={recon} expected {}",
1000 a[(r, c)]
1001 );
1002 }
1003 }
1004 for k in 1..s.len() {
1006 assert!(s[k] <= s[k - 1] + 1e-12);
1007 assert!(s[k] >= -1e-12);
1008 }
1009 }
1010
1011 #[test]
1012 fn test_svd_singular_values_known() {
1013 let a = array![[2.0, 0.0], [0.0, 5.0]];
1015 let (_, s, _) = svd(&a).expect("svd should succeed");
1016 assert!(approx(s[0], 5.0, 1e-9));
1017 assert!(approx(s[1], 2.0, 1e-9));
1018 }
1019
1020 #[test]
1021 fn test_kmeans_two_clusters() {
1022 let data = array![
1024 [0.0, 0.0],
1025 [0.1, -0.1],
1026 [-0.1, 0.2],
1027 [10.0, 10.0],
1028 [10.1, 9.9],
1029 [9.8, 10.2],
1030 ];
1031 let mut km = KMeans::new(2);
1032 let result = km.fit(&data).expect("kmeans fit should succeed");
1033
1034 let l0 = result.labels[0];
1036 assert_eq!(result.labels[1], l0);
1037 assert_eq!(result.labels[2], l0);
1038 let l1 = result.labels[3];
1039 assert_eq!(result.labels[4], l1);
1040 assert_eq!(result.labels[5], l1);
1041 assert_ne!(l0, l1, "clusters must be separated");
1042
1043 let mut near_origin = false;
1045 let mut near_ten = false;
1046 for c in 0..2 {
1047 let cx = result.centers[(c, 0)];
1048 let cy = result.centers[(c, 1)];
1049 if approx(cx, 0.0, 0.5) && approx(cy, 0.0, 0.5) {
1050 near_origin = true;
1051 }
1052 if approx(cx, 10.0, 0.5) && approx(cy, 10.0, 0.5) {
1053 near_ten = true;
1054 }
1055 }
1056 assert!(near_origin && near_ten, "centers must match true means");
1057
1058 assert!(result.inertia.is_finite());
1060 assert!(result.inertia < 1.0, "inertia {} too large", result.inertia);
1061
1062 assert!(
1064 result.silhouette_score > 0.8,
1065 "silhouette {} too low",
1066 result.silhouette_score
1067 );
1068 }
1069
1070 #[test]
1071 fn test_kmeans_predict_matches_fit() {
1072 let data = array![[0.0, 0.0], [0.2, 0.1], [9.0, 9.0], [9.1, 8.9],];
1073 let mut km = KMeans::new(2);
1074 let fitted = km.fit(&data).expect("fit should succeed");
1075 let predicted = km.predict(&data).expect("predict should succeed");
1076 for i in 0..data.nrows() {
1077 assert_eq!(predicted[i], fitted.labels[i]);
1078 }
1079 }
1080
1081 #[test]
1082 fn test_kmeans_predict_before_fit_errors() {
1083 let km = KMeans::new(2);
1084 let data = array![[0.0, 0.0]];
1085 assert!(km.predict(&data).is_err());
1086 }
1087
1088 #[test]
1089 fn test_kmeans_too_few_points_errors() {
1090 let data = array![[0.0, 0.0]];
1091 let mut km = KMeans::new(3);
1092 assert!(km.fit(&data).is_err());
1093 }
1094}