1use scirs2_core::ndarray::{Array1, Array2, ArrayView2, Axis};
9use sklears_core::{
10 error::{Result as SklResult, SklearsError},
11 traits::{Estimator, Fit, Predict, Untrained},
12 types::Float,
13};
14use std::collections::HashMap;
15use std::sync::{Arc, Mutex};
16use std::thread;
17
18#[derive(Debug, Clone)]
36pub struct MultiOutputClassifier<S = Untrained> {
37 state: S,
38 n_jobs: Option<i32>,
39}
40
41impl MultiOutputClassifier<Untrained> {
42 pub fn new() -> Self {
44 Self {
45 state: Untrained,
46 n_jobs: None,
47 }
48 }
49
50 pub fn n_jobs(mut self, n_jobs: Option<i32>) -> Self {
52 self.n_jobs = n_jobs;
53 self
54 }
55}
56
57impl Default for MultiOutputClassifier<Untrained> {
58 fn default() -> Self {
59 Self::new()
60 }
61}
62
63impl Estimator for MultiOutputClassifier<Untrained> {
64 type Config = ();
65 type Error = SklearsError;
66 type Float = Float;
67
68 fn config(&self) -> &Self::Config {
69 &()
70 }
71}
72
73impl Fit<ArrayView2<'_, Float>, Array2<i32>> for MultiOutputClassifier<Untrained> {
74 type Fitted = MultiOutputClassifier<MultiOutputClassifierTrained>;
75
76 #[allow(non_snake_case)]
77 fn fit(self, X: &ArrayView2<'_, Float>, y: &Array2<i32>) -> SklResult<Self::Fitted> {
78 let X = X.to_owned();
79 let (n_samples, n_features) = X.dim();
80
81 if n_samples != y.nrows() {
82 return Err(SklearsError::InvalidInput(
83 "X and y must have the same number of samples".to_string(),
84 ));
85 }
86
87 let n_targets = y.ncols();
88 if n_targets == 0 {
89 return Err(SklearsError::InvalidInput(
90 "y must have at least one target".to_string(),
91 ));
92 }
93
94 let mut classes_per_target = Vec::new();
95 let mut target_models = HashMap::new();
96
97 for target_idx in 0..n_targets {
99 let y_target = y.column(target_idx);
100
101 let mut target_classes: Vec<i32> = y_target
103 .iter()
104 .cloned()
105 .collect::<std::collections::HashSet<_>>()
106 .into_iter()
107 .collect();
108 target_classes.sort();
109
110 let mut class_centroids = HashMap::new();
112 for &class_label in &target_classes {
113 let mut centroid = Array1::<Float>::zeros(n_features);
114 let mut count = 0;
115
116 for (sample_idx, &sample_class) in y_target.iter().enumerate() {
117 if sample_class == class_label {
118 for feature_idx in 0..n_features {
119 centroid[feature_idx] += X[[sample_idx, feature_idx]];
120 }
121 count += 1;
122 }
123 }
124
125 if count > 0 {
126 centroid /= count as f64;
127 }
128 class_centroids.insert(class_label, centroid);
129 }
130
131 target_models.insert(target_idx, class_centroids);
132 classes_per_target.push(target_classes);
133 }
134
135 if let Some(n_jobs) = self.n_jobs {
137 if n_jobs > 1 && n_targets > 1 {
138 return self.fit_parallel(X, y, n_jobs as usize);
139 }
140 }
141
142 Ok(MultiOutputClassifier {
143 state: MultiOutputClassifierTrained {
144 classes_per_target,
145 target_models,
146 n_targets,
147 n_features,
148 },
149 n_jobs: self.n_jobs,
150 })
151 }
152}
153
154impl MultiOutputClassifier<Untrained> {
155 #[allow(non_snake_case)]
157 fn fit_parallel(
158 self,
159 X: Array2<Float>,
160 y: &Array2<i32>,
161 n_jobs: usize,
162 ) -> SklResult<MultiOutputClassifier<MultiOutputClassifierTrained>> {
163 let (_n_samples, n_features) = X.dim();
164 let n_targets = y.ncols();
165
166 let X_arc = Arc::new(X);
168 let y_arc = Arc::new(y.clone());
169 let classes_per_target = Arc::new(Mutex::new(Vec::with_capacity(n_targets)));
170 let target_models = Arc::new(Mutex::new(HashMap::new()));
171
172 let chunk_size = n_targets.div_ceil(n_jobs);
174 let mut handles = vec![];
175
176 for worker_id in 0..n_jobs {
178 let start_target = worker_id * chunk_size;
179 let end_target = std::cmp::min(start_target + chunk_size, n_targets);
180
181 if start_target >= n_targets {
182 break; }
184
185 let X_thread = Arc::clone(&X_arc);
186 let y_thread = Arc::clone(&y_arc);
187 let classes_thread = Arc::clone(&classes_per_target);
188 let models_thread = Arc::clone(&target_models);
189
190 let handle = thread::spawn(move || -> SklResult<()> {
191 let mut local_classes = Vec::new();
192 let mut local_models = HashMap::new();
193
194 for target_idx in start_target..end_target {
195 let y_target = y_thread.column(target_idx);
196
197 let mut target_classes: Vec<i32> = y_target
199 .iter()
200 .cloned()
201 .collect::<std::collections::HashSet<_>>()
202 .into_iter()
203 .collect();
204 target_classes.sort();
205
206 let mut class_centroids = HashMap::new();
208 for &class_label in &target_classes {
209 let mut centroid = Array1::<Float>::zeros(n_features);
210 let mut count = 0;
211
212 for (sample_idx, &sample_class) in y_target.iter().enumerate() {
213 if sample_class == class_label {
214 for feature_idx in 0..n_features {
215 centroid[feature_idx] += X_thread[[sample_idx, feature_idx]];
216 }
217 count += 1;
218 }
219 }
220
221 if count > 0 {
222 centroid /= count as f64;
223 }
224 class_centroids.insert(class_label, centroid);
225 }
226
227 local_models.insert(target_idx, class_centroids);
228 local_classes.push((target_idx, target_classes));
229 }
230
231 {
233 let mut classes_guard =
234 classes_thread.lock().expect("lock should not be poisoned");
235 let mut models_guard =
236 models_thread.lock().expect("lock should not be poisoned");
237
238 local_classes.sort_by_key(|(idx, _)| *idx);
240 for (target_idx, target_classes) in local_classes {
241 while classes_guard.len() <= target_idx {
243 classes_guard.push(vec![]);
244 }
245 classes_guard[target_idx] = target_classes;
246 }
247
248 for (target_idx, class_centroids) in local_models {
249 models_guard.insert(target_idx, class_centroids);
250 }
251 }
252
253 Ok(())
254 });
255
256 handles.push(handle);
257 }
258
259 for handle in handles {
261 handle.join().map_err(|_| {
262 SklearsError::InvalidInput("Thread panicked during parallel training".to_string())
263 })??;
264 }
265
266 let final_classes = Arc::try_unwrap(classes_per_target)
268 .map_err(|_| SklearsError::InvalidInput("Failed to extract classes".to_string()))?
269 .into_inner()
270 .expect("operation should succeed");
271
272 let final_models = Arc::try_unwrap(target_models)
273 .map_err(|_| SklearsError::InvalidInput("Failed to extract models".to_string()))?
274 .into_inner()
275 .expect("operation should succeed");
276
277 Ok(MultiOutputClassifier {
278 state: MultiOutputClassifierTrained {
279 classes_per_target: final_classes,
280 target_models: final_models,
281 n_targets,
282 n_features,
283 },
284 n_jobs: Some(n_jobs as i32),
285 })
286 }
287}
288
289impl MultiOutputClassifier<MultiOutputClassifierTrained> {
290 pub fn classes(&self) -> &[Vec<i32>] {
292 &self.state.classes_per_target
293 }
294
295 pub fn n_targets(&self) -> usize {
297 self.state.n_targets
298 }
299}
300
301impl Predict<ArrayView2<'_, Float>, Array2<i32>>
302 for MultiOutputClassifier<MultiOutputClassifierTrained>
303{
304 #[allow(non_snake_case)]
305 fn predict(&self, X: &ArrayView2<'_, Float>) -> SklResult<Array2<i32>> {
306 let X = X.to_owned();
307 let (n_samples, n_features) = X.dim();
308
309 if n_features != self.state.n_features {
310 return Err(SklearsError::InvalidInput(
311 "Number of features doesn't match training data".to_string(),
312 ));
313 }
314
315 let mut predictions = Array2::<i32>::zeros((n_samples, self.state.n_targets));
316
317 for target_idx in 0..self.state.n_targets {
319 if let Some(class_centroids) = self.state.target_models.get(&target_idx) {
320 for (sample_idx, sample) in X.axis_iter(Axis(0)).enumerate() {
321 let mut min_distance = f64::INFINITY;
322 let mut best_class = 0;
323
324 for (&class_label, centroid) in class_centroids {
326 let mut distance = 0.0;
327 for feature_idx in 0..n_features {
328 let diff = sample[feature_idx] - centroid[feature_idx];
329 distance += diff * diff;
330 }
331 distance = distance.sqrt();
332
333 if distance < min_distance {
334 min_distance = distance;
335 best_class = class_label;
336 }
337 }
338
339 predictions[[sample_idx, target_idx]] = best_class;
340 }
341 }
342 }
343
344 Ok(predictions)
345 }
346}
347
348#[derive(Debug, Clone)]
366pub struct MultiOutputRegressor<S = Untrained> {
367 state: S,
368 n_jobs: Option<i32>,
369}
370
371impl MultiOutputRegressor<Untrained> {
372 pub fn new() -> Self {
374 Self {
375 state: Untrained,
376 n_jobs: None,
377 }
378 }
379
380 pub fn n_jobs(mut self, n_jobs: Option<i32>) -> Self {
382 self.n_jobs = n_jobs;
383 self
384 }
385}
386
387impl Default for MultiOutputRegressor<Untrained> {
388 fn default() -> Self {
389 Self::new()
390 }
391}
392
393impl Estimator for MultiOutputRegressor<Untrained> {
394 type Config = ();
395 type Error = SklearsError;
396 type Float = Float;
397
398 fn config(&self) -> &Self::Config {
399 &()
400 }
401}
402
403impl Fit<ArrayView2<'_, Float>, Array2<f64>> for MultiOutputRegressor<Untrained> {
404 type Fitted = MultiOutputRegressor<MultiOutputRegressorTrained>;
405
406 #[allow(non_snake_case)]
407 fn fit(self, X: &ArrayView2<'_, Float>, y: &Array2<f64>) -> SklResult<Self::Fitted> {
408 let X = X.to_owned();
409 let (n_samples, n_features) = X.dim();
410
411 if n_samples != y.nrows() {
412 return Err(SklearsError::InvalidInput(
413 "X and y must have the same number of samples".to_string(),
414 ));
415 }
416
417 let n_targets = y.ncols();
418 if n_targets == 0 {
419 return Err(SklearsError::InvalidInput(
420 "y must have at least one target".to_string(),
421 ));
422 }
423
424 let mut target_models = HashMap::new();
425
426 for target_idx in 0..n_targets {
428 let y_target = y.column(target_idx);
429
430 let mut weights = Array1::<Float>::zeros(n_features);
433
434 let y_mean = y_target
436 .mean()
437 .expect("array should have elements for mean computation");
438 let bias = y_mean;
439
440 for feature_idx in 0..n_features {
442 let mut correlation = 0.0;
443 let mut x_mean = 0.0;
444
445 for sample_idx in 0..n_samples {
447 x_mean += X[[sample_idx, feature_idx]];
448 }
449 x_mean /= n_samples as f64;
450
451 let mut numerator = 0.0;
453 let mut x_var = 0.0;
454 let mut y_var = 0.0;
455
456 for sample_idx in 0..n_samples {
457 let x_diff = X[[sample_idx, feature_idx]] - x_mean;
458 let y_diff = y_target[sample_idx] - y_mean;
459 numerator += x_diff * y_diff;
460 x_var += x_diff * x_diff;
461 y_var += y_diff * y_diff;
462 }
463
464 if x_var > 1e-10 && y_var > 1e-10 {
465 correlation = numerator / (x_var.sqrt() * y_var.sqrt());
466 }
467
468 weights[feature_idx] = correlation * 0.1; }
470
471 target_models.insert(target_idx, (weights, bias));
472 }
473
474 if let Some(n_jobs) = self.n_jobs {
476 if n_jobs > 1 && n_targets > 1 {
477 return self.fit_parallel(X, y, n_jobs as usize);
478 }
479 }
480
481 Ok(MultiOutputRegressor {
482 state: MultiOutputRegressorTrained {
483 target_models,
484 n_targets,
485 n_features,
486 },
487 n_jobs: self.n_jobs,
488 })
489 }
490}
491
492impl MultiOutputRegressor<Untrained> {
493 #[allow(non_snake_case)]
495 fn fit_parallel(
496 self,
497 X: Array2<Float>,
498 y: &Array2<f64>,
499 n_jobs: usize,
500 ) -> SklResult<MultiOutputRegressor<MultiOutputRegressorTrained>> {
501 let (n_samples, n_features) = X.dim();
502 let n_targets = y.ncols();
503
504 let X_arc = Arc::new(X);
506 let y_arc = Arc::new(y.clone());
507 let target_models = Arc::new(Mutex::new(HashMap::new()));
508
509 let chunk_size = n_targets.div_ceil(n_jobs);
511 let mut handles = vec![];
512
513 for worker_id in 0..n_jobs {
515 let start_target = worker_id * chunk_size;
516 let end_target = std::cmp::min(start_target + chunk_size, n_targets);
517
518 if start_target >= n_targets {
519 break; }
521
522 let X_thread = Arc::clone(&X_arc);
523 let y_thread = Arc::clone(&y_arc);
524 let models_thread = Arc::clone(&target_models);
525
526 let handle = thread::spawn(move || -> SklResult<()> {
527 let mut local_models = HashMap::new();
528
529 for target_idx in start_target..end_target {
530 let y_target = y_thread.column(target_idx);
531 let mut weights = Array1::<f64>::zeros(n_features);
532
533 let y_mean = y_target
535 .mean()
536 .expect("array should have elements for mean computation");
537 let bias: f64 = y_mean;
538
539 for feature_idx in 0..n_features {
541 let mut correlation = 0.0;
542 let mut x_mean = 0.0;
543
544 for sample_idx in 0..n_samples {
546 x_mean += X_thread[[sample_idx, feature_idx]];
547 }
548 x_mean /= n_samples as f64;
549
550 let mut numerator = 0.0;
552 let mut x_var = 0.0;
553 let mut y_var = 0.0;
554
555 for sample_idx in 0..n_samples {
556 let x_diff = X_thread[[sample_idx, feature_idx]] - x_mean;
557 let y_diff = y_target[sample_idx] - y_mean;
558 numerator += x_diff * y_diff;
559 x_var += x_diff * x_diff;
560 y_var += y_diff * y_diff;
561 }
562
563 if x_var > 1e-10 && y_var > 1e-10 {
564 correlation = numerator / (x_var.sqrt() * y_var.sqrt());
565 }
566
567 weights[feature_idx] = correlation * 0.1; }
569
570 local_models.insert(target_idx, (weights, bias));
571 }
572
573 {
575 let mut models_guard =
576 models_thread.lock().expect("lock should not be poisoned");
577 for (target_idx, model) in local_models {
578 models_guard.insert(target_idx, model);
579 }
580 }
581
582 Ok(())
583 });
584
585 handles.push(handle);
586 }
587
588 for handle in handles {
590 handle.join().map_err(|_| {
591 SklearsError::InvalidInput("Thread panicked during parallel training".to_string())
592 })??;
593 }
594
595 let final_models = Arc::try_unwrap(target_models)
597 .map_err(|_| SklearsError::InvalidInput("Failed to extract models".to_string()))?
598 .into_inner()
599 .expect("operation should succeed");
600
601 Ok(MultiOutputRegressor {
602 state: MultiOutputRegressorTrained {
603 target_models: final_models,
604 n_targets,
605 n_features,
606 },
607 n_jobs: Some(n_jobs as i32),
608 })
609 }
610}
611
612impl MultiOutputRegressor<MultiOutputRegressorTrained> {
613 pub fn n_targets(&self) -> usize {
615 self.state.n_targets
616 }
617}
618
619impl Predict<ArrayView2<'_, Float>, Array2<f64>>
620 for MultiOutputRegressor<MultiOutputRegressorTrained>
621{
622 #[allow(non_snake_case)]
623 fn predict(&self, X: &ArrayView2<'_, Float>) -> SklResult<Array2<f64>> {
624 let X = X.to_owned();
625 let (n_samples, n_features) = X.dim();
626
627 if n_features != self.state.n_features {
628 return Err(SklearsError::InvalidInput(
629 "Number of features doesn't match training data".to_string(),
630 ));
631 }
632
633 let mut predictions = Array2::<Float>::zeros((n_samples, self.state.n_targets));
634
635 for target_idx in 0..self.state.n_targets {
637 if let Some((weights, bias)) = self.state.target_models.get(&target_idx) {
638 for (sample_idx, sample) in X.axis_iter(Axis(0)).enumerate() {
639 let prediction: f64 = sample
641 .iter()
642 .zip(weights.iter())
643 .map(|(&x, &w)| x * w)
644 .sum::<f64>()
645 + bias;
646
647 predictions[[sample_idx, target_idx]] = prediction;
648 }
649 }
650 }
651
652 Ok(predictions)
653 }
654}
655
656#[derive(Debug, Clone)]
658pub struct MultiOutputClassifierTrained {
659 pub classes_per_target: Vec<Vec<i32>>,
661 pub target_models: HashMap<usize, HashMap<i32, Array1<f64>>>,
663 pub n_targets: usize,
665 pub n_features: usize,
667}
668
669#[derive(Debug, Clone)]
671pub struct MultiOutputRegressorTrained {
672 pub target_models: HashMap<usize, (Array1<f64>, f64)>,
674 pub n_targets: usize,
676 pub n_features: usize,
678}
679
680#[allow(non_snake_case)]
681#[cfg(test)]
682mod tests {
683 use super::*;
684 use approx::assert_abs_diff_eq;
685 use scirs2_core::ndarray::array;
687 use std::time::Instant;
688
689 #[test]
690 #[allow(non_snake_case)]
691 fn test_parallel_multi_output_classifier() {
692 let X = array![
693 [1.0, 2.0, 3.0],
694 [2.0, 3.0, 4.0],
695 [3.0, 4.0, 5.0],
696 [4.0, 5.0, 6.0],
697 [5.0, 6.0, 7.0],
698 [6.0, 7.0, 8.0]
699 ];
700 let y = array![
701 [0, 1, 0],
702 [1, 0, 1],
703 [0, 1, 0],
704 [1, 0, 1],
705 [0, 1, 0],
706 [1, 0, 1]
707 ];
708
709 let classifier_parallel = MultiOutputClassifier::new().n_jobs(Some(2));
711 let trained_parallel = classifier_parallel
712 .fit(&X.view(), &y)
713 .expect("model fitting should succeed");
714
715 let classifier_sequential = MultiOutputClassifier::new().n_jobs(Some(1));
717 let trained_sequential = classifier_sequential
718 .fit(&X.view(), &y)
719 .expect("model fitting should succeed");
720
721 assert_eq!(trained_parallel.n_targets(), trained_sequential.n_targets());
723 assert_eq!(
724 trained_parallel.classes().len(),
725 trained_sequential.classes().len()
726 );
727
728 let pred_parallel = trained_parallel
730 .predict(&X.view())
731 .expect("prediction should succeed");
732 let pred_sequential = trained_sequential
733 .predict(&X.view())
734 .expect("prediction should succeed");
735
736 assert_eq!(pred_parallel.shape(), pred_sequential.shape());
737 assert_eq!(pred_parallel.shape(), &[6, 3]);
738 }
739
740 #[test]
741 #[allow(non_snake_case)]
742 fn test_parallel_multi_output_regressor() {
743 let X = array![
744 [1.0, 2.0, 3.0],
745 [2.0, 3.0, 4.0],
746 [3.0, 4.0, 5.0],
747 [4.0, 5.0, 6.0],
748 [5.0, 6.0, 7.0],
749 [6.0, 7.0, 8.0]
750 ];
751 let y = array![
752 [1.5, 2.5, 3.5],
753 [2.5, 3.5, 4.5],
754 [3.5, 4.5, 5.5],
755 [4.5, 5.5, 6.5],
756 [5.5, 6.5, 7.5],
757 [6.5, 7.5, 8.5]
758 ];
759
760 let regressor_parallel = MultiOutputRegressor::new().n_jobs(Some(2));
762 let trained_parallel = regressor_parallel
763 .fit(&X.view(), &y)
764 .expect("model fitting should succeed");
765
766 let regressor_sequential = MultiOutputRegressor::new().n_jobs(Some(1));
768 let trained_sequential = regressor_sequential
769 .fit(&X.view(), &y)
770 .expect("model fitting should succeed");
771
772 assert_eq!(trained_parallel.n_targets(), trained_sequential.n_targets());
774
775 let pred_parallel = trained_parallel
777 .predict(&X.view())
778 .expect("prediction should succeed");
779 let pred_sequential = trained_sequential
780 .predict(&X.view())
781 .expect("prediction should succeed");
782
783 assert_eq!(pred_parallel.shape(), pred_sequential.shape());
784 assert_eq!(pred_parallel.shape(), &[6, 3]);
785
786 for i in 0..pred_parallel.nrows() {
788 for j in 0..pred_parallel.ncols() {
789 assert_abs_diff_eq!(
790 pred_parallel[[i, j]],
791 pred_sequential[[i, j]],
792 epsilon = 1e-10
793 );
794 }
795 }
796 }
797
798 #[test]
799 fn test_parallel_training_performance_classifier() {
800 let n_samples = 1000;
802 let n_features = 50;
803 let n_targets = 20;
804
805 let mut X = Array2::<Float>::zeros((n_samples, n_features));
806 let mut y = Array2::<i32>::zeros((n_samples, n_targets));
807
808 for i in 0..n_samples {
810 for j in 0..n_features {
811 X[[i, j]] = (i * j) as Float * 0.01;
812 }
813 for j in 0..n_targets {
814 y[[i, j]] = ((i + j) % 2) as i32;
815 }
816 }
817
818 let start_sequential = Instant::now();
820 let classifier_sequential = MultiOutputClassifier::new().n_jobs(Some(1));
821 let trained_sequential = classifier_sequential
822 .fit(&X.view(), &y)
823 .expect("model fitting should succeed");
824 let sequential_time = start_sequential.elapsed();
825
826 let start_parallel = Instant::now();
828 let classifier_parallel = MultiOutputClassifier::new().n_jobs(Some(4));
829 let trained_parallel = classifier_parallel
830 .fit(&X.view(), &y)
831 .expect("model fitting should succeed");
832 let parallel_time = start_parallel.elapsed();
833
834 assert_eq!(trained_parallel.n_targets(), n_targets);
836 assert_eq!(trained_sequential.n_targets(), n_targets);
837
838 let pred_parallel = trained_parallel
840 .predict(&X.view())
841 .expect("prediction should succeed");
842 let pred_sequential = trained_sequential
843 .predict(&X.view())
844 .expect("prediction should succeed");
845 assert_eq!(pred_parallel.shape(), pred_sequential.shape());
846
847 println!(
848 "Sequential time: {:?}, Parallel time: {:?}",
849 sequential_time, parallel_time
850 );
851 }
852
853 #[test]
854 fn test_parallel_training_performance_regressor() {
855 let n_samples = 1000;
857 let n_features = 50;
858 let n_targets = 20;
859
860 let mut X = Array2::<Float>::zeros((n_samples, n_features));
861 let mut y = Array2::<f64>::zeros((n_samples, n_targets));
862
863 for i in 0..n_samples {
865 for j in 0..n_features {
866 X[[i, j]] = (i * j) as Float * 0.01;
867 }
868 for j in 0..n_targets {
869 y[[i, j]] = (i + j) as f64 * 0.1;
870 }
871 }
872
873 let start_sequential = Instant::now();
875 let regressor_sequential = MultiOutputRegressor::new().n_jobs(Some(1));
876 let trained_sequential = regressor_sequential
877 .fit(&X.view(), &y)
878 .expect("model fitting should succeed");
879 let sequential_time = start_sequential.elapsed();
880
881 let start_parallel = Instant::now();
883 let regressor_parallel = MultiOutputRegressor::new().n_jobs(Some(4));
884 let trained_parallel = regressor_parallel
885 .fit(&X.view(), &y)
886 .expect("model fitting should succeed");
887 let parallel_time = start_parallel.elapsed();
888
889 assert_eq!(trained_parallel.n_targets(), n_targets);
891 assert_eq!(trained_sequential.n_targets(), n_targets);
892
893 let pred_parallel = trained_parallel
895 .predict(&X.view())
896 .expect("prediction should succeed");
897 let pred_sequential = trained_sequential
898 .predict(&X.view())
899 .expect("prediction should succeed");
900 assert_eq!(pred_parallel.shape(), pred_sequential.shape());
901
902 println!(
903 "Sequential time: {:?}, Parallel time: {:?}",
904 sequential_time, parallel_time
905 );
906 }
907
908 #[test]
909 #[allow(non_snake_case)]
910 fn test_parallel_training_thread_safety() {
911 let X = array![[1.0, 2.0], [2.0, 3.0], [3.0, 4.0], [4.0, 5.0]];
912 let y_class = array![[0, 1], [1, 0], [0, 1], [1, 0]];
913 let y_reg = array![[1.0, 2.0], [2.0, 3.0], [3.0, 4.0], [4.0, 5.0]];
914
915 for _ in 0..10 {
917 let classifier = MultiOutputClassifier::new().n_jobs(Some(2));
918 let trained = classifier
919 .fit(&X.view(), &y_class)
920 .expect("model fitting should succeed");
921 let predictions = trained
922 .predict(&X.view())
923 .expect("prediction should succeed");
924 assert_eq!(predictions.shape(), &[4, 2]);
925
926 let regressor = MultiOutputRegressor::new().n_jobs(Some(2));
927 let trained = regressor
928 .fit(&X.view(), &y_reg)
929 .expect("model fitting should succeed");
930 let predictions = trained
931 .predict(&X.view())
932 .expect("prediction should succeed");
933 assert_eq!(predictions.shape(), &[4, 2]);
934 }
935 }
936
937 #[test]
938 #[allow(non_snake_case)]
939 fn test_parallel_training_edge_cases() {
940 let X = array![[1.0, 2.0], [2.0, 3.0]];
941 let y_class = array![[0, 1], [1, 0]];
942 let y_reg = array![[1.0, 2.0], [2.0, 3.0]];
943
944 let classifier = MultiOutputClassifier::new().n_jobs(Some(10));
946 let trained = classifier
947 .fit(&X.view(), &y_class)
948 .expect("model fitting should succeed");
949 assert_eq!(trained.n_targets(), 2);
950
951 let regressor = MultiOutputRegressor::new().n_jobs(Some(10));
952 let trained = regressor
953 .fit(&X.view(), &y_reg)
954 .expect("model fitting should succeed");
955 assert_eq!(trained.n_targets(), 2);
956
957 let y_single = array![[0], [1]];
959 let classifier_single = MultiOutputClassifier::new().n_jobs(Some(4));
960 let trained_single = classifier_single
961 .fit(&X.view(), &y_single)
962 .expect("model fitting should succeed");
963 assert_eq!(trained_single.n_targets(), 1);
964 }
965
966 #[test]
967 #[allow(non_snake_case)]
968 fn test_parallel_training_error_handling() {
969 let X = array![[1.0, 2.0], [2.0, 3.0]];
970 let y_mismatch = array![[0, 1, 0], [1, 0, 1], [0, 1, 0]]; let classifier = MultiOutputClassifier::new().n_jobs(Some(2));
974 let result = classifier.fit(&X.view(), &y_mismatch);
975 assert!(result.is_err());
976
977 let regressor = MultiOutputRegressor::new().n_jobs(Some(2));
978 let y_reg_mismatch = array![[1.0, 2.0, 3.0], [2.0, 3.0, 4.0], [3.0, 4.0, 5.0]];
979 let result = regressor.fit(&X.view(), &y_reg_mismatch);
980 assert!(result.is_err());
981 }
982}