1use crate::distance::Distance;
33use crate::NeighborsError;
34use scirs2_core::ndarray::{Array1, Array2, ArrayView1};
35use sklears_core::error::SklearsError;
36use sklears_core::traits::{Fit, Predict, PredictProba};
37use sklears_core::types::Float;
38
39#[derive(Debug, Clone)]
41pub struct ViewConfig {
42 pub distance: Distance,
44 pub weight: Float,
46 pub learn_metric: bool,
48}
49
50impl ViewConfig {
51 pub fn new(distance: Distance, weight: Float) -> Self {
53 Self {
54 distance,
55 weight,
56 learn_metric: false,
57 }
58 }
59
60 pub fn with_metric_learning(mut self) -> Self {
62 self.learn_metric = true;
63 self
64 }
65}
66
67#[derive(Debug, Clone, Copy)]
69pub enum FusionStrategy {
70 WeightedAverage,
72 Product,
74 MajorityVoting,
76 Stacking,
78}
79
80#[derive(Debug, Clone)]
85pub struct MultiViewKNeighborsClassifier<S> {
86 k: usize,
87 view_configs: Vec<ViewConfig>,
88 fusion_strategy: FusionStrategy,
89 consensus_threshold: Float,
90 state: S,
91}
92
93#[derive(Debug, Clone)]
95pub struct Untrained;
96
97#[derive(Debug, Clone)]
99pub struct Trained {
100 x_train_views: Vec<Array2<Float>>,
102 y_train: Array1<i32>,
104 classes: Array1<i32>,
106 view_weights: Array1<Float>,
108}
109
110impl MultiViewKNeighborsClassifier<Untrained> {
111 pub fn new(k: usize, view_configs: Vec<ViewConfig>) -> Self {
118 Self {
119 k,
120 view_configs,
121 fusion_strategy: FusionStrategy::WeightedAverage,
122 consensus_threshold: 0.5,
123 state: Untrained,
124 }
125 }
126
127 pub fn with_fusion_strategy(mut self, strategy: FusionStrategy) -> Self {
129 self.fusion_strategy = strategy;
130 self
131 }
132
133 pub fn with_consensus_threshold(mut self, threshold: Float) -> Self {
135 self.consensus_threshold = threshold;
136 self
137 }
138}
139
140impl Fit<Vec<Array2<Float>>, Array1<i32>, MultiViewKNeighborsClassifier<Trained>>
141 for MultiViewKNeighborsClassifier<Untrained>
142{
143 type Fitted = MultiViewKNeighborsClassifier<Trained>;
144
145 fn fit(
146 self,
147 x_views: &Vec<Array2<Float>>,
148 y: &Array1<i32>,
149 ) -> Result<Self::Fitted, SklearsError> {
150 if x_views.is_empty() {
152 return Err(NeighborsError::InvalidInput("No views provided".to_string()).into());
153 }
154
155 if x_views.len() != self.view_configs.len() {
156 return Err(NeighborsError::InvalidInput(format!(
157 "Number of views ({}) doesn't match number of view configs ({})",
158 x_views.len(),
159 self.view_configs.len()
160 ))
161 .into());
162 }
163
164 let n_samples = x_views[0].nrows();
165
166 for view in x_views.iter() {
168 if view.nrows() != n_samples {
169 return Err(NeighborsError::ShapeMismatch {
170 expected: vec![n_samples],
171 actual: vec![view.nrows()],
172 }
173 .into());
174 }
175 }
176
177 if y.len() != n_samples {
178 return Err(NeighborsError::ShapeMismatch {
179 expected: vec![n_samples],
180 actual: vec![y.len()],
181 }
182 .into());
183 }
184
185 if self.k >= n_samples {
186 return Err(NeighborsError::InvalidInput(format!(
187 "k={} should be less than n_samples={}",
188 self.k, n_samples
189 ))
190 .into());
191 }
192
193 let mut unique_classes: Vec<i32> = y.iter().cloned().collect();
195 unique_classes.sort_unstable();
196 unique_classes.dedup();
197 let classes = Array1::from_vec(unique_classes);
198
199 let view_weights = Array1::from_vec(
201 self.view_configs
202 .iter()
203 .map(|config| config.weight)
204 .collect(),
205 );
206
207 Ok(MultiViewKNeighborsClassifier {
208 k: self.k,
209 view_configs: self.view_configs,
210 fusion_strategy: self.fusion_strategy,
211 consensus_threshold: self.consensus_threshold,
212 state: Trained {
213 x_train_views: x_views.clone(),
214 y_train: y.clone(),
215 classes,
216 view_weights,
217 },
218 })
219 }
220}
221
222impl Predict<Vec<Array2<Float>>, Array1<i32>> for MultiViewKNeighborsClassifier<Trained> {
223 fn predict(&self, x_views: &Vec<Array2<Float>>) -> Result<Array1<i32>, SklearsError> {
224 if x_views.len() != self.state.x_train_views.len() {
225 return Err(NeighborsError::InvalidInput(format!(
226 "Expected {} views, got {}",
227 self.state.x_train_views.len(),
228 x_views.len()
229 ))
230 .into());
231 }
232
233 let n_samples = x_views[0].nrows();
234 let mut predictions = Vec::with_capacity(n_samples);
235
236 for i in 0..n_samples {
237 let pred = self.predict_single(x_views, i)?;
238 predictions.push(pred);
239 }
240
241 Ok(Array1::from_vec(predictions))
242 }
243}
244
245impl PredictProba<Vec<Array2<Float>>, Array2<Float>> for MultiViewKNeighborsClassifier<Trained> {
246 fn predict_proba(&self, x_views: &Vec<Array2<Float>>) -> Result<Array2<Float>, SklearsError> {
247 if x_views.len() != self.state.x_train_views.len() {
248 return Err(NeighborsError::InvalidInput(format!(
249 "Expected {} views, got {}",
250 self.state.x_train_views.len(),
251 x_views.len()
252 ))
253 .into());
254 }
255
256 let n_samples = x_views[0].nrows();
257 let n_classes = self.state.classes.len();
258 let mut proba_matrix = Array2::zeros((n_samples, n_classes));
259
260 for i in 0..n_samples {
261 let proba = self.predict_proba_single(x_views, i)?;
262 proba_matrix.row_mut(i).assign(&proba);
263 }
264
265 Ok(proba_matrix)
266 }
267}
268
269impl MultiViewKNeighborsClassifier<Trained> {
270 fn predict_single(
272 &self,
273 x_views: &[Array2<Float>],
274 sample_idx: usize,
275 ) -> Result<i32, SklearsError> {
276 let proba = self.predict_proba_single(x_views, sample_idx)?;
277 let class_idx = proba
278 .iter()
279 .enumerate()
280 .max_by(|(_, a), (_, b)| a.partial_cmp(b).expect("operation should succeed"))
281 .map(|(idx, _)| idx)
282 .unwrap_or(0);
283 Ok(self.state.classes[class_idx])
284 }
285
286 fn predict_proba_single(
288 &self,
289 x_views: &[Array2<Float>],
290 sample_idx: usize,
291 ) -> Result<Array1<Float>, SklearsError> {
292 let n_classes = self.state.classes.len();
293
294 match self.fusion_strategy {
295 FusionStrategy::WeightedAverage => {
296 let mut combined_proba = Array1::zeros(n_classes);
297 let mut total_weight = 0.0;
298
299 for (view_idx, view) in x_views.iter().enumerate() {
300 let query = view.row(sample_idx);
301 let proba = self.predict_proba_for_view(view_idx, &query)?;
302 let weight = self.state.view_weights[view_idx];
303 combined_proba += &(proba * weight);
304 total_weight += weight;
305 }
306
307 if total_weight > 0.0 {
308 combined_proba /= total_weight;
309 }
310
311 Ok(combined_proba)
312 }
313 FusionStrategy::Product => {
314 let mut combined_proba = Array1::ones(n_classes);
315
316 for (view_idx, view) in x_views.iter().enumerate() {
317 let query = view.row(sample_idx);
318 let proba = self.predict_proba_for_view(view_idx, &query)?;
319 combined_proba *= &proba;
320 }
321
322 let sum: Float = combined_proba.sum();
324 if sum > 0.0 {
325 combined_proba /= sum;
326 }
327
328 Ok(combined_proba)
329 }
330 FusionStrategy::MajorityVoting => {
331 let mut votes = Array1::zeros(n_classes);
332
333 for (view_idx, view) in x_views.iter().enumerate() {
334 let query = view.row(sample_idx);
335 let proba = self.predict_proba_for_view(view_idx, &query)?;
336 let class_idx = proba
337 .iter()
338 .enumerate()
339 .max_by(|(_, a), (_, b)| {
340 a.partial_cmp(b).expect("operation should succeed")
341 })
342 .map(|(idx, _)| idx)
343 .unwrap_or(0);
344 votes[class_idx] += 1.0;
345 }
346
347 let total: Float = votes.sum();
349 if total > 0.0 {
350 votes /= total;
351 }
352
353 Ok(votes)
354 }
355 FusionStrategy::Stacking => {
356 let mut combined_proba = Array1::zeros(n_classes);
359
360 for (view_idx, view) in x_views.iter().enumerate() {
361 let query = view.row(sample_idx);
362 let proba = self.predict_proba_for_view(view_idx, &query)?;
363 combined_proba += &proba;
364 }
365
366 combined_proba /= x_views.len() as Float;
367 Ok(combined_proba)
368 }
369 }
370 }
371
372 fn predict_proba_for_view(
374 &self,
375 view_idx: usize,
376 query: &ArrayView1<Float>,
377 ) -> Result<Array1<Float>, SklearsError> {
378 let distance = &self.view_configs[view_idx].distance;
379 let train_view = &self.state.x_train_views[view_idx];
380
381 let mut distances_indices: Vec<(Float, usize)> = train_view
383 .rows()
384 .into_iter()
385 .enumerate()
386 .map(|(i, row)| (distance.calculate(query, &row), i))
387 .collect();
388
389 distances_indices.sort_by(|a, b| a.0.partial_cmp(&b.0).expect("operation should succeed"));
390 let neighbor_indices: Vec<usize> = distances_indices
391 .into_iter()
392 .take(self.k)
393 .map(|(_, idx)| idx)
394 .collect();
395
396 let mut class_counts = Array1::zeros(self.state.classes.len());
398 for &idx in &neighbor_indices {
399 let label = self.state.y_train[idx];
400 if let Some(class_idx) = self.state.classes.iter().position(|&c| c == label) {
401 class_counts[class_idx] += 1.0;
402 }
403 }
404
405 let total: Float = class_counts.sum();
407 if total > 0.0 {
408 class_counts /= total;
409 }
410
411 Ok(class_counts)
412 }
413
414 pub fn analyze_consensus(
416 &self,
417 x_views: &[Array2<Float>],
418 ) -> Result<ConsensusAnalysis, SklearsError> {
419 if x_views.len() != self.state.x_train_views.len() {
420 return Err(NeighborsError::InvalidInput(format!(
421 "Expected {} views, got {}",
422 self.state.x_train_views.len(),
423 x_views.len()
424 ))
425 .into());
426 }
427
428 let n_samples = x_views[0].nrows();
429 let mut consensus_scores = Vec::with_capacity(n_samples);
430 let mut view_agreements = Array2::zeros((x_views.len(), x_views.len()));
431
432 for i in 0..n_samples {
433 let mut view_neighbors = Vec::new();
435 for (view_idx, view) in x_views.iter().enumerate() {
436 let query = view.row(i);
437 let neighbors = self.get_neighbors_for_view(view_idx, &query)?;
438 view_neighbors.push(neighbors);
439 }
440
441 let mut consensus_sum = 0.0;
443 let mut count = 0;
444 for j in 0..view_neighbors.len() {
445 for k in (j + 1)..view_neighbors.len() {
446 let intersection: usize = view_neighbors[j]
447 .iter()
448 .filter(|&n| view_neighbors[k].contains(n))
449 .count();
450 let union = view_neighbors[j].len() + view_neighbors[k].len() - intersection;
451 let jaccard = if union > 0 {
452 intersection as Float / union as Float
453 } else {
454 0.0
455 };
456 consensus_sum += jaccard;
457 view_agreements[[j, k]] += jaccard;
458 view_agreements[[k, j]] += jaccard;
459 count += 1;
460 }
461 }
462
463 let consensus = if count > 0 {
464 consensus_sum / count as Float
465 } else {
466 0.0
467 };
468 consensus_scores.push(consensus);
469 }
470
471 if n_samples > 0 {
473 view_agreements /= n_samples as Float;
474 }
475
476 let mean_consensus =
477 consensus_scores.iter().sum::<Float>() / consensus_scores.len() as Float;
478
479 Ok(ConsensusAnalysis {
480 sample_consensus_scores: Array1::from_vec(consensus_scores),
481 mean_consensus,
482 view_agreement_matrix: view_agreements,
483 })
484 }
485
486 fn get_neighbors_for_view(
488 &self,
489 view_idx: usize,
490 query: &ArrayView1<Float>,
491 ) -> Result<Vec<usize>, SklearsError> {
492 let distance = &self.view_configs[view_idx].distance;
493 let train_view = &self.state.x_train_views[view_idx];
494
495 let mut distances_indices: Vec<(Float, usize)> = train_view
496 .rows()
497 .into_iter()
498 .enumerate()
499 .map(|(i, row)| (distance.calculate(query, &row), i))
500 .collect();
501
502 distances_indices.sort_by(|a, b| a.0.partial_cmp(&b.0).expect("operation should succeed"));
503 Ok(distances_indices
504 .into_iter()
505 .take(self.k)
506 .map(|(_, idx)| idx)
507 .collect())
508 }
509}
510
511#[derive(Debug, Clone)]
513pub struct ConsensusAnalysis {
514 pub sample_consensus_scores: Array1<Float>,
516 pub mean_consensus: Float,
518 pub view_agreement_matrix: Array2<Float>,
520}
521
522#[derive(Debug, Clone)]
524pub struct MultiViewKNeighborsRegressor<S> {
525 k: usize,
526 view_configs: Vec<ViewConfig>,
527 fusion_strategy: RegressionFusionStrategy,
528 state: S,
529}
530
531#[derive(Debug, Clone, Copy)]
533pub enum RegressionFusionStrategy {
534 WeightedAverage,
536 Median,
538 Stacking,
540}
541
542#[derive(Debug, Clone)]
544pub struct RegressorTrained {
545 x_train_views: Vec<Array2<Float>>,
546 y_train: Array1<Float>,
547 view_weights: Array1<Float>,
548}
549
550impl MultiViewKNeighborsRegressor<Untrained> {
551 pub fn new(k: usize, view_configs: Vec<ViewConfig>) -> Self {
553 Self {
554 k,
555 view_configs,
556 fusion_strategy: RegressionFusionStrategy::WeightedAverage,
557 state: Untrained,
558 }
559 }
560
561 pub fn with_fusion_strategy(mut self, strategy: RegressionFusionStrategy) -> Self {
563 self.fusion_strategy = strategy;
564 self
565 }
566}
567
568impl Fit<Vec<Array2<Float>>, Array1<Float>, MultiViewKNeighborsRegressor<RegressorTrained>>
569 for MultiViewKNeighborsRegressor<Untrained>
570{
571 type Fitted = MultiViewKNeighborsRegressor<RegressorTrained>;
572
573 fn fit(
574 self,
575 x_views: &Vec<Array2<Float>>,
576 y: &Array1<Float>,
577 ) -> Result<Self::Fitted, SklearsError> {
578 if x_views.is_empty() {
579 return Err(NeighborsError::InvalidInput("No views provided".to_string()).into());
580 }
581
582 if x_views.len() != self.view_configs.len() {
583 return Err(NeighborsError::InvalidInput(format!(
584 "Number of views ({}) doesn't match number of view configs ({})",
585 x_views.len(),
586 self.view_configs.len()
587 ))
588 .into());
589 }
590
591 let n_samples = x_views[0].nrows();
592
593 for view in x_views.iter() {
594 if view.nrows() != n_samples {
595 return Err(NeighborsError::ShapeMismatch {
596 expected: vec![n_samples],
597 actual: vec![view.nrows()],
598 }
599 .into());
600 }
601 }
602
603 if y.len() != n_samples {
604 return Err(NeighborsError::ShapeMismatch {
605 expected: vec![n_samples],
606 actual: vec![y.len()],
607 }
608 .into());
609 }
610
611 let view_weights = Array1::from_vec(
612 self.view_configs
613 .iter()
614 .map(|config| config.weight)
615 .collect(),
616 );
617
618 Ok(MultiViewKNeighborsRegressor {
619 k: self.k,
620 view_configs: self.view_configs,
621 fusion_strategy: self.fusion_strategy,
622 state: RegressorTrained {
623 x_train_views: x_views.clone(),
624 y_train: y.clone(),
625 view_weights,
626 },
627 })
628 }
629}
630
631impl Predict<Vec<Array2<Float>>, Array1<Float>> for MultiViewKNeighborsRegressor<RegressorTrained> {
632 fn predict(&self, x_views: &Vec<Array2<Float>>) -> Result<Array1<Float>, SklearsError> {
633 if x_views.len() != self.state.x_train_views.len() {
634 return Err(NeighborsError::InvalidInput(format!(
635 "Expected {} views, got {}",
636 self.state.x_train_views.len(),
637 x_views.len()
638 ))
639 .into());
640 }
641
642 let n_samples = x_views[0].nrows();
643 let mut predictions = Vec::with_capacity(n_samples);
644
645 for i in 0..n_samples {
646 let pred = self.predict_single(x_views, i)?;
647 predictions.push(pred);
648 }
649
650 Ok(Array1::from_vec(predictions))
651 }
652}
653
654impl MultiViewKNeighborsRegressor<RegressorTrained> {
655 fn predict_single(
656 &self,
657 x_views: &[Array2<Float>],
658 sample_idx: usize,
659 ) -> Result<Float, SklearsError> {
660 match self.fusion_strategy {
661 RegressionFusionStrategy::WeightedAverage => {
662 let mut weighted_sum = 0.0;
663 let mut total_weight = 0.0;
664
665 for (view_idx, view) in x_views.iter().enumerate() {
666 let query = view.row(sample_idx);
667 let pred = self.predict_for_view(view_idx, &query)?;
668 let weight = self.state.view_weights[view_idx];
669 weighted_sum += pred * weight;
670 total_weight += weight;
671 }
672
673 Ok(if total_weight > 0.0 {
674 weighted_sum / total_weight
675 } else {
676 0.0
677 })
678 }
679 RegressionFusionStrategy::Median => {
680 let mut predictions = Vec::new();
681 for (view_idx, view) in x_views.iter().enumerate() {
682 let query = view.row(sample_idx);
683 let pred = self.predict_for_view(view_idx, &query)?;
684 predictions.push(pred);
685 }
686 predictions.sort_by(|a, b| a.partial_cmp(b).expect("operation should succeed"));
687 Ok(predictions[predictions.len() / 2])
688 }
689 RegressionFusionStrategy::Stacking => {
690 let mut sum = 0.0;
692 for (view_idx, view) in x_views.iter().enumerate() {
693 let query = view.row(sample_idx);
694 let pred = self.predict_for_view(view_idx, &query)?;
695 sum += pred;
696 }
697 Ok(sum / (x_views.len() as Float))
698 }
699 }
700 }
701
702 fn predict_for_view(
703 &self,
704 view_idx: usize,
705 query: &ArrayView1<Float>,
706 ) -> Result<Float, SklearsError> {
707 let distance = &self.view_configs[view_idx].distance;
708 let train_view = &self.state.x_train_views[view_idx];
709
710 let mut distances_indices: Vec<(Float, usize)> = train_view
711 .rows()
712 .into_iter()
713 .enumerate()
714 .map(|(i, row)| (distance.calculate(query, &row), i))
715 .collect();
716
717 distances_indices.sort_by(|a, b| a.0.partial_cmp(&b.0).expect("operation should succeed"));
718 let neighbor_values: Vec<Float> = distances_indices
719 .into_iter()
720 .take(self.k)
721 .map(|(_, idx)| self.state.y_train[idx])
722 .collect();
723
724 Ok(neighbor_values.iter().sum::<Float>() / neighbor_values.len() as Float)
725 }
726}
727
728#[cfg(test)]
729mod tests {
730 use super::*;
731 use scirs2_core::ndarray::array;
732
733 #[test]
734 fn test_multi_view_classifier_basic() {
735 let view1 = Array2::from_shape_vec(
737 (6, 2),
738 vec![
739 -1.0, -1.0, -0.5, -1.0, 0.0, 0.0, 1.0, 1.0, 0.5, 1.0, 1.5, 1.5,
740 ],
741 )
742 .expect("operation should succeed");
743 let view2 = Array2::from_shape_vec(
744 (6, 2),
745 vec![
746 1.0, 1.0, 1.5, 1.0, 0.0, 0.0, -1.0, -1.0, -0.5, -1.0, -1.5, -1.5,
747 ],
748 )
749 .expect("operation should succeed");
750 let views = vec![view1, view2];
751 let y = array![0, 0, 1, 1, 1, 1];
752
753 let configs = vec![
754 ViewConfig::new(Distance::Euclidean, 1.0),
755 ViewConfig::new(Distance::Euclidean, 1.0),
756 ];
757
758 let classifier = MultiViewKNeighborsClassifier::new(3, configs);
759 let fitted = classifier
760 .fit(&views, &y)
761 .expect("operation should succeed");
762
763 let test_view1 = Array2::from_shape_vec((2, 2), vec![-0.8, -0.8, 0.8, 0.8])
764 .expect("operation should succeed");
765 let test_view2 = Array2::from_shape_vec((2, 2), vec![0.8, 0.8, -0.8, -0.8])
766 .expect("operation should succeed");
767 let test_views = vec![test_view1, test_view2];
768
769 let predictions = fitted
770 .predict(&test_views)
771 .expect("operation should succeed");
772 assert_eq!(predictions.len(), 2);
773 }
774
775 #[test]
776 fn test_multi_view_regressor() {
777 let view1 = Array2::from_shape_vec((4, 2), vec![1.0, 1.0, 2.0, 1.0, 1.0, 2.0, 2.0, 2.0])
778 .expect("operation should succeed");
779 let view2 = Array2::from_shape_vec((4, 2), vec![5.0, 5.0, 6.0, 5.0, 5.0, 6.0, 6.0, 6.0])
780 .expect("operation should succeed");
781 let views = vec![view1, view2];
782 let y = array![2.0, 3.0, 3.0, 4.0];
783
784 let configs = vec![
785 ViewConfig::new(Distance::Euclidean, 1.0),
786 ViewConfig::new(Distance::Euclidean, 1.0),
787 ];
788
789 let regressor = MultiViewKNeighborsRegressor::new(2, configs);
790 let fitted = regressor.fit(&views, &y).expect("operation should succeed");
791
792 let test_view1 =
793 Array2::from_shape_vec((1, 2), vec![1.5, 1.5]).expect("operation should succeed");
794 let test_view2 =
795 Array2::from_shape_vec((1, 2), vec![5.5, 5.5]).expect("operation should succeed");
796 let test_views = vec![test_view1, test_view2];
797
798 let predictions = fitted
799 .predict(&test_views)
800 .expect("operation should succeed");
801 assert_eq!(predictions.len(), 1);
802 assert!(predictions[0] > 2.0 && predictions[0] < 4.0);
803 }
804
805 #[test]
806 fn test_consensus_analysis() {
807 let view1 = Array2::from_shape_vec((4, 2), vec![1.0, 1.0, 2.0, 1.0, 1.0, 2.0, 2.0, 2.0])
808 .expect("operation should succeed");
809 let view2 = Array2::from_shape_vec((4, 2), vec![1.0, 1.0, 2.0, 1.0, 1.0, 2.0, 2.0, 2.0])
810 .expect("operation should succeed");
811 let views_train = vec![view1, view2];
812 let y = array![0, 0, 1, 1];
813
814 let configs = vec![
815 ViewConfig::new(Distance::Euclidean, 1.0),
816 ViewConfig::new(Distance::Euclidean, 1.0),
817 ];
818
819 let classifier = MultiViewKNeighborsClassifier::new(2, configs);
820 let fitted = classifier
821 .fit(&views_train, &y)
822 .expect("operation should succeed");
823
824 let consensus = fitted
825 .analyze_consensus(&views_train)
826 .expect("operation should succeed");
827 assert_eq!(consensus.sample_consensus_scores.len(), 4);
828 assert!(consensus.mean_consensus >= 0.0 && consensus.mean_consensus <= 1.0);
829 }
830
831 #[test]
832 fn test_fusion_strategies() {
833 let view1 = Array2::from_shape_vec(
834 (6, 2),
835 vec![
836 -1.0, -1.0, -0.5, -1.0, 0.0, 0.0, 1.0, 1.0, 0.5, 1.0, 1.5, 1.5,
837 ],
838 )
839 .expect("operation should succeed");
840 let view2 = Array2::from_shape_vec(
841 (6, 2),
842 vec![
843 1.0, 1.0, 1.5, 1.0, 0.0, 0.0, -1.0, -1.0, -0.5, -1.0, -1.5, -1.5,
844 ],
845 )
846 .expect("operation should succeed");
847 let views = vec![view1, view2];
848 let y = array![0, 0, 1, 1, 1, 1];
849
850 let strategies = vec![
851 FusionStrategy::WeightedAverage,
852 FusionStrategy::Product,
853 FusionStrategy::MajorityVoting,
854 FusionStrategy::Stacking,
855 ];
856
857 for strategy in strategies {
858 let configs = vec![
859 ViewConfig::new(Distance::Euclidean, 1.0),
860 ViewConfig::new(Distance::Euclidean, 1.0),
861 ];
862 let classifier =
863 MultiViewKNeighborsClassifier::new(3, configs).with_fusion_strategy(strategy);
864 let fitted = classifier
865 .fit(&views, &y)
866 .expect("operation should succeed");
867
868 let test_view1 =
869 Array2::from_shape_vec((1, 2), vec![-0.8, -0.8]).expect("operation should succeed");
870 let test_view2 =
871 Array2::from_shape_vec((1, 2), vec![0.8, 0.8]).expect("operation should succeed");
872 let test_views = vec![test_view1, test_view2];
873
874 let predictions = fitted
875 .predict(&test_views)
876 .expect("operation should succeed");
877 assert_eq!(predictions.len(), 1);
878 }
879 }
880
881 #[test]
882 fn test_view_config() {
883 let config = ViewConfig::new(Distance::Euclidean, 1.5).with_metric_learning();
884 assert_eq!(config.weight, 1.5);
885 assert!(config.learn_metric);
886 }
887
888 #[test]
889 fn test_error_handling() {
890 let view1 = Array2::from_shape_vec((4, 2), vec![1.0; 8]).expect("operation should succeed");
892 let views = vec![view1];
893 let y = array![0, 0, 1, 1];
894 let configs = vec![
895 ViewConfig::new(Distance::Euclidean, 1.0),
896 ViewConfig::new(Distance::Euclidean, 1.0),
897 ];
898
899 let classifier = MultiViewKNeighborsClassifier::new(2, configs);
900 assert!(classifier.fit(&views, &y).is_err());
901
902 let empty_views: Vec<Array2<Float>> = vec![];
904 let configs = vec![ViewConfig::new(Distance::Euclidean, 1.0)];
905 let classifier = MultiViewKNeighborsClassifier::new(2, configs);
906 assert!(classifier.fit(&empty_views, &y).is_err());
907 }
908}