1use scirs2_core::ndarray::{Array1, Array2, ArrayView1, ArrayView2};
9use scirs2_core::random::rngs::StdRng as RealStdRng;
10#[allow(unused_imports)]
11use scirs2_core::random::RngExt; use scirs2_core::random::{thread_rng, SeedableRng};
13use sklears_core::{
14 error::{Result as SklResult, SklearsError},
15 traits::{Estimator, Fit, Predict, Untrained},
16 types::Float,
17};
18
19#[derive(Debug, Clone)]
24pub struct CalibratedBinaryRelevance<S = Untrained> {
25 state: S,
26 calibration_method: CalibrationMethod,
27}
28
29#[derive(Debug, Clone, Copy, PartialEq)]
31pub enum CalibrationMethod {
32 Platt,
34 Isotonic,
36}
37
38#[derive(Debug, Clone)]
40pub struct CalibratedBinaryRelevanceTrained {
41 base_models: Vec<(Array1<Float>, Float)>, calibration_params: Vec<(Float, Float)>, pub calibration_method: CalibrationMethod,
45 pub n_features: usize,
47 n_labels: usize,
48}
49
50impl Default for CalibratedBinaryRelevance<Untrained> {
51 fn default() -> Self {
52 Self::new()
53 }
54}
55
56impl Estimator for CalibratedBinaryRelevance<Untrained> {
57 type Config = ();
58 type Error = SklearsError;
59 type Float = Float;
60
61 fn config(&self) -> &Self::Config {
62 &()
63 }
64}
65
66impl Fit<ArrayView2<'_, Float>, Array2<i32>> for CalibratedBinaryRelevance<Untrained> {
67 type Fitted = CalibratedBinaryRelevance<CalibratedBinaryRelevanceTrained>;
68
69 #[allow(non_snake_case)] fn fit(self, X: &ArrayView2<'_, Float>, y: &Array2<i32>) -> SklResult<Self::Fitted> {
71 let (n_samples, n_features) = X.dim();
72 let n_labels = y.ncols();
73
74 if n_samples != y.nrows() {
75 return Err(SklearsError::InvalidInput(
76 "X and y must have the same number of samples".to_string(),
77 ));
78 }
79
80 let mut base_models = Vec::new();
81 let mut calibration_params = Vec::new();
82
83 for label_idx in 0..n_labels {
85 let y_label = y.column(label_idx);
86
87 let mut weights = Array1::<Float>::zeros(n_features);
89 let mut bias = 0.0;
90 let learning_rate = 0.01;
91 let max_iter = 100;
92
93 for _iter in 0..max_iter {
95 let mut weight_gradient = Array1::<Float>::zeros(n_features);
96 let mut bias_gradient = 0.0;
97
98 for sample_idx in 0..n_samples {
99 let x = X.row(sample_idx);
100 let y_true = y_label[sample_idx] as Float;
101
102 let logit = x.dot(&weights) + bias;
103 let prob = 1.0 / (1.0 + (-logit).exp());
104 let error = prob - y_true;
105
106 for feat_idx in 0..n_features {
108 weight_gradient[feat_idx] += error * x[feat_idx];
109 }
110 bias_gradient += error;
111 }
112
113 for i in 0..n_features {
115 weights[i] -= learning_rate * weight_gradient[i] / n_samples as Float;
116 }
117 bias -= learning_rate * bias_gradient / n_samples as Float;
118 }
119
120 let mut probs = Vec::new();
122 let mut labels = Vec::new();
123 for sample_idx in 0..n_samples {
124 let x = X.row(sample_idx);
125 let logit = x.dot(&weights) + bias;
126 let prob = 1.0 / (1.0 + (-logit).exp());
127 probs.push(prob);
128 labels.push(y_label[sample_idx] as Float);
129 }
130
131 let (slope, intercept) = self.fit_calibration(&probs, &labels)?;
133
134 base_models.push((weights, bias));
135 calibration_params.push((slope, intercept));
136 }
137
138 Ok(CalibratedBinaryRelevance {
139 state: CalibratedBinaryRelevanceTrained {
140 base_models,
141 calibration_params,
142 calibration_method: self.calibration_method,
143 n_features,
144 n_labels,
145 },
146 calibration_method: self.calibration_method,
147 })
148 }
149}
150
151impl CalibratedBinaryRelevance<Untrained> {
152 pub fn new() -> Self {
154 Self {
155 state: Untrained,
156 calibration_method: CalibrationMethod::Platt,
157 }
158 }
159
160 pub fn calibration_method(mut self, method: CalibrationMethod) -> Self {
162 self.calibration_method = method;
163 self
164 }
165
166 fn fit_calibration(&self, probs: &[Float], labels: &[Float]) -> SklResult<(Float, Float)> {
168 match self.calibration_method {
170 CalibrationMethod::Platt => {
171 let mut a = -1.0;
174 let mut b = 0.0;
175 let learning_rate = 0.01;
176
177 for _iter in 0..100 {
178 let mut grad_a = 0.0;
179 let mut grad_b = 0.0;
180
181 for (i, &prob) in probs.iter().enumerate() {
182 let y_true = labels[i];
183 let logit = a * prob + b;
184 let cal_prob = 1.0 / (1.0 + (-logit).exp());
185 let error = cal_prob - y_true;
186
187 grad_a += error * prob;
188 grad_b += error;
189 }
190
191 a -= learning_rate * grad_a / probs.len() as Float;
192 b -= learning_rate * grad_b / probs.len() as Float;
193 }
194
195 Ok((a, b))
196 }
197 CalibrationMethod::Isotonic => {
198 Ok((-1.0, 0.0))
200 }
201 }
202 }
203}
204
205impl Predict<ArrayView2<'_, Float>, Array2<i32>>
206 for CalibratedBinaryRelevance<CalibratedBinaryRelevanceTrained>
207{
208 #[allow(non_snake_case)] fn predict(&self, X: &ArrayView2<'_, Float>) -> SklResult<Array2<i32>> {
210 let (n_samples, n_features) = X.dim();
211
212 if n_features != self.state.n_features {
213 return Err(SklearsError::InvalidInput(
214 "X has different number of features than training data".to_string(),
215 ));
216 }
217
218 let mut predictions = Array2::<i32>::zeros((n_samples, self.state.n_labels));
219
220 for sample_idx in 0..n_samples {
221 let x = X.row(sample_idx);
222
223 for label_idx in 0..self.state.n_labels {
224 let (weights, bias) = &self.state.base_models[label_idx];
225 let (slope, intercept) = self.state.calibration_params[label_idx];
226
227 let logit = x.dot(weights) + bias;
229 let base_prob = 1.0 / (1.0 + (-logit).exp());
230
231 let cal_logit = slope * base_prob + intercept;
233 let cal_prob = 1.0 / (1.0 + (-cal_logit).exp());
234
235 predictions[[sample_idx, label_idx]] = if cal_prob > 0.5 { 1 } else { 0 };
236 }
237 }
238
239 Ok(predictions)
240 }
241}
242
243impl CalibratedBinaryRelevance<CalibratedBinaryRelevanceTrained> {
244 #[allow(non_snake_case)] pub fn predict_proba(&self, X: &ArrayView2<'_, Float>) -> SklResult<Array2<Float>> {
247 let (n_samples, n_features) = X.dim();
248
249 if n_features != self.state.n_features {
250 return Err(SklearsError::InvalidInput(
251 "X has different number of features than training data".to_string(),
252 ));
253 }
254
255 let mut probabilities = Array2::<Float>::zeros((n_samples, self.state.n_labels));
256
257 for sample_idx in 0..n_samples {
258 let x = X.row(sample_idx);
259
260 for label_idx in 0..self.state.n_labels {
261 let (weights, bias) = &self.state.base_models[label_idx];
262 let (slope, intercept) = self.state.calibration_params[label_idx];
263
264 let logit = x.dot(weights) + bias;
266 let base_prob = 1.0 / (1.0 + (-logit).exp());
267
268 let cal_logit = slope * base_prob + intercept;
270 let cal_prob = 1.0 / (1.0 + (-cal_logit).exp());
271
272 probabilities[[sample_idx, label_idx]] = cal_prob;
273 }
274 }
275
276 Ok(probabilities)
277 }
278}
279
280pub struct RandomLabelCombinations {
285 n_labels: usize,
286 n_combinations: usize,
287 label_density: Float,
288 random_state: Option<u64>,
289}
290
291impl RandomLabelCombinations {
292 pub fn new(n_labels: usize) -> Self {
294 Self {
295 n_labels,
296 n_combinations: 100,
297 label_density: 0.3,
298 random_state: None,
299 }
300 }
301
302 pub fn n_combinations(mut self, n_combinations: usize) -> Self {
304 self.n_combinations = n_combinations;
305 self
306 }
307
308 pub fn label_density(mut self, density: Float) -> Self {
310 self.label_density = density;
311 self
312 }
313
314 pub fn random_state(mut self, seed: u64) -> Self {
316 self.random_state = Some(seed);
317 self
318 }
319
320 pub fn generate(&self) -> Array2<i32> {
322 let mut rng = match self.random_state {
323 Some(seed) => RealStdRng::seed_from_u64(seed),
324 None => RealStdRng::from_seed(thread_rng().random()),
325 };
326
327 let mut combinations = Array2::<i32>::zeros((self.n_combinations, self.n_labels));
328
329 for i in 0..self.n_combinations {
330 for j in 0..self.n_labels {
331 combinations[[i, j]] = if rng.random::<Float>() < self.label_density {
332 1
333 } else {
334 0
335 };
336 }
337 }
338
339 combinations
340 }
341}
342
343#[derive(Debug, Clone)]
349pub struct MLkNN<S = Untrained> {
350 state: S,
351 k: usize,
352 smooth: Float,
353 distance_metric: DistanceMetric,
354}
355
356#[derive(Debug, Clone, Copy, PartialEq)]
358pub enum DistanceMetric {
359 Euclidean,
361 Manhattan,
363 Cosine,
365}
366
367#[derive(Debug, Clone)]
369pub struct MLkNNTrained {
370 training_data: Array2<Float>,
371 training_labels: Array2<i32>,
372 prior_probs: Array1<Float>,
373 conditional_probs: Array2<Float>, k: usize,
375 pub smooth: Float,
377 distance_metric: DistanceMetric,
378 n_labels: usize,
379}
380
381impl Default for MLkNN<Untrained> {
382 fn default() -> Self {
383 Self::new()
384 }
385}
386
387impl Estimator for MLkNN<Untrained> {
388 type Config = ();
389 type Error = SklearsError;
390 type Float = Float;
391
392 fn config(&self) -> &Self::Config {
393 &()
394 }
395}
396
397impl Fit<ArrayView2<'_, Float>, Array2<i32>> for MLkNN<Untrained> {
398 type Fitted = MLkNN<MLkNNTrained>;
399
400 #[allow(non_snake_case)] fn fit(self, X: &ArrayView2<'_, Float>, y: &Array2<i32>) -> SklResult<Self::Fitted> {
402 let (n_samples, _n_features) = X.dim();
403 let n_labels = y.ncols();
404
405 if n_samples != y.nrows() {
406 return Err(SklearsError::InvalidInput(
407 "X and y must have the same number of samples".to_string(),
408 ));
409 }
410
411 if self.k >= n_samples {
412 return Err(SklearsError::InvalidInput(
413 "k must be smaller than the number of training samples".to_string(),
414 ));
415 }
416
417 let mut prior_probs = Array1::<Float>::zeros(n_labels);
419 for label_idx in 0..n_labels {
420 let positive_count = y.column(label_idx).iter().filter(|&&x| x == 1).count();
421 prior_probs[label_idx] =
422 (positive_count as Float + self.smooth) / (n_samples as Float + 2.0 * self.smooth);
423 }
424
425 let mut conditional_probs = Array2::<Float>::zeros((n_labels, self.k + 1));
427
428 for sample_idx in 0..n_samples {
429 let neighbors = self.find_k_neighbors(X, sample_idx, &X.view())?;
430
431 for label_idx in 0..n_labels {
432 let label_count = neighbors
433 .iter()
434 .filter(|&&neighbor_idx| y[[neighbor_idx, label_idx]] == 1)
435 .count();
436
437 if y[[sample_idx, label_idx]] == 1 {
438 conditional_probs[[label_idx, label_count]] += 1.0;
439 }
440 }
441 }
442
443 for label_idx in 0..n_labels {
445 let total_positive = y.column(label_idx).iter().filter(|&&x| x == 1).count() as Float;
446 for count in 0..=self.k {
447 conditional_probs[[label_idx, count]] = (conditional_probs[[label_idx, count]]
448 + self.smooth)
449 / (total_positive + (self.k + 1) as Float * self.smooth);
450 }
451 }
452
453 Ok(MLkNN {
454 state: MLkNNTrained {
455 training_data: X.to_owned(),
456 training_labels: y.clone(),
457 prior_probs,
458 conditional_probs,
459 k: self.k,
460 smooth: self.smooth,
461 distance_metric: self.distance_metric,
462 n_labels,
463 },
464 k: self.k,
465 smooth: self.smooth,
466 distance_metric: self.distance_metric,
467 })
468 }
469}
470
471impl MLkNN<Untrained> {
472 pub fn new() -> Self {
474 Self {
475 state: Untrained,
476 k: 10,
477 smooth: 1.0,
478 distance_metric: DistanceMetric::Euclidean,
479 }
480 }
481
482 pub fn k(mut self, k: usize) -> Self {
484 self.k = k;
485 self
486 }
487
488 pub fn smooth(mut self, smooth: Float) -> Self {
490 self.smooth = smooth;
491 self
492 }
493
494 pub fn distance_metric(mut self, metric: DistanceMetric) -> Self {
496 self.distance_metric = metric;
497 self
498 }
499
500 #[allow(non_snake_case)] fn find_k_neighbors(
503 &self,
504 X: &ArrayView2<'_, Float>,
505 sample_idx: usize,
506 training_data: &ArrayView2<'_, Float>,
507 ) -> SklResult<Vec<usize>> {
508 let query = X.row(sample_idx);
509 let mut distances = Vec::new();
510
511 for (train_idx, train_sample) in training_data.rows().into_iter().enumerate() {
512 if train_idx != sample_idx {
513 let distance = self.calculate_distance(&query, &train_sample);
514 distances.push((distance, train_idx));
515 }
516 }
517
518 distances.sort_by(|a, b| a.0.partial_cmp(&b.0).expect("operation should succeed"));
519 let neighbors = distances
520 .into_iter()
521 .take(self.k)
522 .map(|(_, idx)| idx)
523 .collect();
524
525 Ok(neighbors)
526 }
527
528 fn calculate_distance(&self, a: &ArrayView1<'_, Float>, b: &ArrayView1<'_, Float>) -> Float {
530 match self.distance_metric {
531 DistanceMetric::Euclidean => a
532 .iter()
533 .zip(b.iter())
534 .map(|(x, y)| (x - y).powi(2))
535 .sum::<Float>()
536 .sqrt(),
537 DistanceMetric::Manhattan => a.iter().zip(b.iter()).map(|(x, y)| (x - y).abs()).sum(),
538 DistanceMetric::Cosine => {
539 let dot = a.iter().zip(b.iter()).map(|(x, y)| x * y).sum::<Float>();
540 let norm_a = a.iter().map(|x| x.powi(2)).sum::<Float>().sqrt();
541 let norm_b = b.iter().map(|x| x.powi(2)).sum::<Float>().sqrt();
542 if norm_a > 0.0 && norm_b > 0.0 {
543 1.0 - dot / (norm_a * norm_b)
544 } else {
545 1.0
546 }
547 }
548 }
549 }
550}
551
552impl Predict<ArrayView2<'_, Float>, Array2<i32>> for MLkNN<MLkNNTrained> {
553 #[allow(non_snake_case)] fn predict(&self, X: &ArrayView2<'_, Float>) -> SklResult<Array2<i32>> {
555 let (n_samples, n_features) = X.dim();
556
557 if n_features != self.state.training_data.ncols() {
558 return Err(SklearsError::InvalidInput(
559 "X has different number of features than training data".to_string(),
560 ));
561 }
562
563 let mut predictions = Array2::<i32>::zeros((n_samples, self.state.n_labels));
564
565 for sample_idx in 0..n_samples {
566 let neighbors = self.find_k_neighbors_trained(X, sample_idx)?;
567
568 for label_idx in 0..self.state.n_labels {
569 let positive_neighbors = neighbors
571 .iter()
572 .filter(|&&neighbor_idx| {
573 self.state.training_labels[[neighbor_idx, label_idx]] == 1
574 })
575 .count();
576
577 let prob_positive = self.state.prior_probs[label_idx]
579 * self.state.conditional_probs[[label_idx, positive_neighbors]];
580 let prob_negative = (1.0 - self.state.prior_probs[label_idx])
581 * (1.0 - self.state.conditional_probs[[label_idx, positive_neighbors]]);
582
583 predictions[[sample_idx, label_idx]] =
584 if prob_positive > prob_negative { 1 } else { 0 };
585 }
586 }
587
588 Ok(predictions)
589 }
590}
591
592impl MLkNN<MLkNNTrained> {
593 #[allow(non_snake_case)] fn find_k_neighbors_trained(
596 &self,
597 X: &ArrayView2<'_, Float>,
598 sample_idx: usize,
599 ) -> SklResult<Vec<usize>> {
600 let query = X.row(sample_idx);
601 let mut distances = Vec::new();
602
603 for (train_idx, train_sample) in self.state.training_data.rows().into_iter().enumerate() {
604 let distance = self.calculate_distance_trained(&query, &train_sample);
605 distances.push((distance, train_idx));
606 }
607
608 distances.sort_by(|a, b| a.0.partial_cmp(&b.0).expect("operation should succeed"));
609 let neighbors = distances
610 .into_iter()
611 .take(self.state.k)
612 .map(|(_, idx)| idx)
613 .collect();
614
615 Ok(neighbors)
616 }
617
618 fn calculate_distance_trained(
620 &self,
621 a: &ArrayView1<'_, Float>,
622 b: &ArrayView1<'_, Float>,
623 ) -> Float {
624 match self.state.distance_metric {
625 DistanceMetric::Euclidean => a
626 .iter()
627 .zip(b.iter())
628 .map(|(x, y)| (x - y).powi(2))
629 .sum::<Float>()
630 .sqrt(),
631 DistanceMetric::Manhattan => a.iter().zip(b.iter()).map(|(x, y)| (x - y).abs()).sum(),
632 DistanceMetric::Cosine => {
633 let dot = a.iter().zip(b.iter()).map(|(x, y)| x * y).sum::<Float>();
634 let norm_a = a.iter().map(|x| x.powi(2)).sum::<Float>().sqrt();
635 let norm_b = b.iter().map(|x| x.powi(2)).sum::<Float>().sqrt();
636 if norm_a > 0.0 && norm_b > 0.0 {
637 1.0 - dot / (norm_a * norm_b)
638 } else {
639 1.0
640 }
641 }
642 }
643 }
644
645 pub fn k(&self) -> usize {
647 self.state.k
648 }
649
650 pub fn prior_probabilities(&self) -> &Array1<Float> {
652 &self.state.prior_probs
653 }
654}
655
656#[derive(Debug, Clone)]
661pub struct CostSensitiveBinaryRelevance<S = Untrained> {
662 state: S,
663 cost_matrix: CostMatrix,
664 learning_rate: Float,
665 max_iterations: usize,
666 regularization: Float,
667}
668
669#[derive(Debug, Clone)]
671pub struct CostMatrix {
672 false_positive_costs: Array1<Float>,
674 false_negative_costs: Array1<Float>,
676}
677
678impl CostMatrix {
679 pub fn new(false_positive_costs: Array1<Float>, false_negative_costs: Array1<Float>) -> Self {
681 Self {
682 false_positive_costs,
683 false_negative_costs,
684 }
685 }
686
687 pub fn uniform(n_labels: usize, fp_cost: Float, fn_cost: Float) -> Self {
689 Self {
690 false_positive_costs: Array1::from_elem(n_labels, fp_cost),
691 false_negative_costs: Array1::from_elem(n_labels, fn_cost),
692 }
693 }
694
695 pub fn fp_cost(&self, label_idx: usize) -> Float {
697 self.false_positive_costs
698 .get(label_idx)
699 .copied()
700 .unwrap_or(1.0)
701 }
702
703 pub fn fn_cost(&self, label_idx: usize) -> Float {
705 self.false_negative_costs
706 .get(label_idx)
707 .copied()
708 .unwrap_or(1.0)
709 }
710}
711
712#[derive(Debug, Clone)]
714pub struct CostSensitiveBinaryRelevanceTrained {
715 models: Vec<SimpleBinaryModel>,
716 cost_matrix: CostMatrix,
717 n_features: usize,
718 n_labels: usize,
719}
720
721#[derive(Debug, Clone)]
723pub struct SimpleBinaryModel {
724 weights: Array1<Float>,
725 bias: Float,
726 threshold: Float, }
728
729impl Default for CostSensitiveBinaryRelevance<Untrained> {
730 fn default() -> Self {
731 Self::new()
732 }
733}
734
735impl Estimator for CostSensitiveBinaryRelevance<Untrained> {
736 type Config = ();
737 type Error = SklearsError;
738 type Float = Float;
739
740 fn config(&self) -> &Self::Config {
741 &()
742 }
743}
744
745impl Fit<ArrayView2<'_, Float>, Array2<i32>> for CostSensitiveBinaryRelevance<Untrained> {
746 type Fitted = CostSensitiveBinaryRelevance<CostSensitiveBinaryRelevanceTrained>;
747
748 #[allow(non_snake_case)] fn fit(self, X: &ArrayView2<'_, Float>, y: &Array2<i32>) -> SklResult<Self::Fitted> {
750 let (n_samples, n_features) = X.dim();
751 let n_labels = y.ncols();
752
753 if n_samples != y.nrows() {
754 return Err(SklearsError::InvalidInput(
755 "X and y must have the same number of samples".to_string(),
756 ));
757 }
758
759 let mut models = Vec::new();
760
761 for label_idx in 0..n_labels {
763 let y_label = y.column(label_idx);
764 let fp_cost = self.cost_matrix.fp_cost(label_idx);
765 let fn_cost = self.cost_matrix.fn_cost(label_idx);
766
767 let mut weights = Array1::<Float>::zeros(n_features);
768 let mut bias = 0.0;
769
770 for _iter in 0..self.max_iterations {
772 let mut weight_gradient = Array1::<Float>::zeros(n_features);
773 let mut bias_gradient = 0.0;
774
775 for sample_idx in 0..n_samples {
776 let x = X.row(sample_idx);
777 let y_true = y_label[sample_idx] as Float;
778
779 let logit = x.dot(&weights) + bias;
780 let prob = 1.0 / (1.0 + (-logit).exp());
781
782 let cost_weight = if y_true == 1.0 { fn_cost } else { fp_cost };
784 let error = (prob - y_true) * cost_weight;
785
786 for feat_idx in 0..n_features {
788 weight_gradient[feat_idx] += error * x[feat_idx];
789 }
790 bias_gradient += error;
791 }
792
793 for i in 0..n_features {
795 weight_gradient[i] += self.regularization * weights[i];
796 }
797
798 for i in 0..n_features {
800 weights[i] -= self.learning_rate * weight_gradient[i] / n_samples as Float;
801 }
802 bias -= self.learning_rate * bias_gradient / n_samples as Float;
803 }
804
805 let threshold = self.calculate_cost_sensitive_threshold(fp_cost, fn_cost);
807
808 models.push(SimpleBinaryModel {
809 weights,
810 bias,
811 threshold,
812 });
813 }
814
815 Ok(CostSensitiveBinaryRelevance {
816 state: CostSensitiveBinaryRelevanceTrained {
817 models,
818 cost_matrix: self.cost_matrix,
819 n_features,
820 n_labels,
821 },
822 cost_matrix: CostMatrix::uniform(n_labels, 1.0, 1.0),
823 learning_rate: self.learning_rate,
824 max_iterations: self.max_iterations,
825 regularization: self.regularization,
826 })
827 }
828}
829
830impl CostSensitiveBinaryRelevance<Untrained> {
831 pub fn new() -> Self {
833 Self {
834 state: Untrained,
835 cost_matrix: CostMatrix::uniform(1, 1.0, 1.0),
836 learning_rate: 0.01,
837 max_iterations: 100,
838 regularization: 0.01,
839 }
840 }
841
842 pub fn cost_matrix(mut self, cost_matrix: CostMatrix) -> Self {
844 self.cost_matrix = cost_matrix;
845 self
846 }
847
848 pub fn learning_rate(mut self, learning_rate: Float) -> Self {
850 self.learning_rate = learning_rate;
851 self
852 }
853
854 pub fn max_iterations(mut self, max_iterations: usize) -> Self {
856 self.max_iterations = max_iterations;
857 self
858 }
859
860 pub fn regularization(mut self, regularization: Float) -> Self {
862 self.regularization = regularization;
863 self
864 }
865
866 fn calculate_cost_sensitive_threshold(&self, fp_cost: Float, fn_cost: Float) -> Float {
868 fp_cost / (fp_cost + fn_cost)
872 }
873}
874
875impl Predict<ArrayView2<'_, Float>, Array2<i32>>
876 for CostSensitiveBinaryRelevance<CostSensitiveBinaryRelevanceTrained>
877{
878 #[allow(non_snake_case)] fn predict(&self, X: &ArrayView2<'_, Float>) -> SklResult<Array2<i32>> {
880 let (n_samples, n_features) = X.dim();
881
882 if n_features != self.state.n_features {
883 return Err(SklearsError::InvalidInput(
884 "X has different number of features than training data".to_string(),
885 ));
886 }
887
888 let mut predictions = Array2::<i32>::zeros((n_samples, self.state.n_labels));
889
890 for sample_idx in 0..n_samples {
891 let x = X.row(sample_idx);
892
893 for (label_idx, model) in self.state.models.iter().enumerate() {
894 let logit = x.dot(&model.weights) + model.bias;
895 let prob = 1.0 / (1.0 + (-logit).exp());
896
897 predictions[[sample_idx, label_idx]] = if prob > model.threshold { 1 } else { 0 };
898 }
899 }
900
901 Ok(predictions)
902 }
903}
904
905impl CostSensitiveBinaryRelevance<CostSensitiveBinaryRelevanceTrained> {
906 pub fn cost_matrix(&self) -> &CostMatrix {
908 &self.state.cost_matrix
909 }
910
911 pub fn thresholds(&self) -> Vec<Float> {
913 self.state.models.iter().map(|m| m.threshold).collect()
914 }
915}
916
917#[allow(non_snake_case)]
918#[cfg(test)]
919mod tests {
920 use super::*;
921 use scirs2_core::ndarray::array;
923
924 #[test]
925 #[allow(non_snake_case)]
926 fn test_calibrated_binary_relevance_basic() {
927 let X = array![[1.0, 2.0], [2.0, 3.0], [3.0, 1.0], [4.0, 4.0]];
928 let y = array![[1, 0], [0, 1], [1, 1], [0, 0]];
929
930 let cbr = CalibratedBinaryRelevance::new().calibration_method(CalibrationMethod::Platt);
931 let trained_cbr = cbr
932 .fit(&X.view(), &y)
933 .expect("model fitting should succeed");
934 let predictions = trained_cbr
935 .predict(&X.view())
936 .expect("prediction should succeed");
937
938 assert_eq!(predictions.dim(), (4, 2));
939 assert!(predictions.iter().all(|&x| x == 0 || x == 1));
940 }
941
942 #[test]
943 #[allow(non_snake_case)]
944 fn test_calibrated_binary_relevance_probabilities() {
945 let X = array![[1.0, 2.0], [2.0, 3.0]];
946 let y = array![[1, 0], [0, 1]];
947
948 let cbr = CalibratedBinaryRelevance::new();
949 let trained_cbr = cbr
950 .fit(&X.view(), &y)
951 .expect("model fitting should succeed");
952 let probabilities = trained_cbr
953 .predict_proba(&X.view())
954 .expect("operation should succeed");
955
956 assert_eq!(probabilities.dim(), (2, 2));
957 assert!(probabilities.iter().all(|&p| (0.0..=1.0).contains(&p)));
958 }
959
960 #[test]
961 fn test_random_label_combinations() {
962 let generator = RandomLabelCombinations::new(3)
963 .n_combinations(5)
964 .label_density(0.5)
965 .random_state(42);
966
967 let combinations = generator.generate();
968 assert_eq!(combinations.dim(), (5, 3));
969 assert!(combinations.iter().all(|&x| x == 0 || x == 1));
970 }
971
972 #[test]
973 fn test_random_label_combinations_deterministic_seeding() {
974 let result1 = RandomLabelCombinations::new(5)
976 .n_combinations(10)
977 .random_state(42)
978 .generate();
979 let result2 = RandomLabelCombinations::new(5)
980 .n_combinations(10)
981 .random_state(42)
982 .generate();
983 assert_eq!(
984 result1, result2,
985 "same seed should produce identical results"
986 );
987
988 let result3 = RandomLabelCombinations::new(5)
990 .n_combinations(10)
991 .random_state(43)
992 .generate();
993 assert_ne!(
994 result1, result3,
995 "different seeds should produce different results"
996 );
997 }
998
999 #[test]
1000 #[allow(non_snake_case)]
1001 fn test_mlknn_basic() {
1002 let X = array![[1.0, 2.0], [2.0, 3.0], [3.0, 1.0], [4.0, 4.0], [1.5, 2.5]];
1003 let y = array![[1, 0], [0, 1], [1, 1], [0, 0], [1, 0]];
1004
1005 let mlknn = MLkNN::new().k(3).smooth(1.0);
1006 let trained_mlknn = mlknn
1007 .fit(&X.view(), &y)
1008 .expect("model fitting should succeed");
1009 let predictions = trained_mlknn
1010 .predict(&X.view())
1011 .expect("prediction should succeed");
1012
1013 assert_eq!(predictions.dim(), (5, 2));
1014 assert!(predictions.iter().all(|&x| x == 0 || x == 1));
1015 assert_eq!(trained_mlknn.k(), 3);
1016 }
1017
1018 #[test]
1019 #[allow(non_snake_case)]
1020 fn test_mlknn_distance_metrics() {
1021 let X = array![[1.0, 2.0], [2.0, 3.0], [3.0, 1.0]];
1022 let y = array![[1, 0], [0, 1], [1, 1]];
1023
1024 let mlknn_euclidean = MLkNN::new().k(2).distance_metric(DistanceMetric::Euclidean);
1025 let trained_euclidean = mlknn_euclidean
1026 .fit(&X.view(), &y)
1027 .expect("model fitting should succeed");
1028
1029 let mlknn_manhattan = MLkNN::new().k(2).distance_metric(DistanceMetric::Manhattan);
1030 let trained_manhattan = mlknn_manhattan
1031 .fit(&X.view(), &y)
1032 .expect("model fitting should succeed");
1033
1034 let pred_euclidean = trained_euclidean
1035 .predict(&X.view())
1036 .expect("prediction should succeed");
1037 let pred_manhattan = trained_manhattan
1038 .predict(&X.view())
1039 .expect("prediction should succeed");
1040
1041 assert_eq!(pred_euclidean.dim(), (3, 2));
1042 assert_eq!(pred_manhattan.dim(), (3, 2));
1043 }
1044
1045 #[test]
1046 #[allow(non_snake_case)]
1047 fn test_cost_sensitive_binary_relevance() {
1048 let X = array![[1.0, 2.0], [2.0, 3.0], [3.0, 1.0], [4.0, 4.0]];
1049 let y = array![[1, 0], [0, 1], [1, 1], [0, 0]];
1050
1051 let fp_costs = array![2.0, 1.0]; let fn_costs = array![1.0, 3.0]; let cost_matrix = CostMatrix::new(fp_costs, fn_costs);
1054
1055 let csbr = CostSensitiveBinaryRelevance::new()
1056 .cost_matrix(cost_matrix)
1057 .learning_rate(0.01)
1058 .max_iterations(50);
1059
1060 let trained_csbr = csbr
1061 .fit(&X.view(), &y)
1062 .expect("model fitting should succeed");
1063 let predictions = trained_csbr
1064 .predict(&X.view())
1065 .expect("prediction should succeed");
1066
1067 assert_eq!(predictions.dim(), (4, 2));
1068 assert!(predictions.iter().all(|&x| x == 0 || x == 1));
1069
1070 let thresholds = trained_csbr.thresholds();
1071 assert_eq!(thresholds.len(), 2);
1072 }
1073
1074 #[test]
1075 fn test_cost_matrix_creation() {
1076 let fp_costs = array![1.0, 2.0, 3.0];
1077 let fn_costs = array![2.0, 1.0, 1.0];
1078 let cost_matrix = CostMatrix::new(fp_costs, fn_costs);
1079
1080 assert_eq!(cost_matrix.fp_cost(0), 1.0);
1081 assert_eq!(cost_matrix.fp_cost(1), 2.0);
1082 assert_eq!(cost_matrix.fn_cost(0), 2.0);
1083 assert_eq!(cost_matrix.fn_cost(1), 1.0);
1084
1085 let uniform_costs = CostMatrix::uniform(3, 1.5, 2.5);
1086 assert_eq!(uniform_costs.fp_cost(0), 1.5);
1087 assert_eq!(uniform_costs.fn_cost(2), 2.5);
1088 }
1089
1090 #[test]
1091 fn test_calibration_methods() {
1092 let cbr_platt =
1093 CalibratedBinaryRelevance::new().calibration_method(CalibrationMethod::Platt);
1094 let cbr_isotonic =
1095 CalibratedBinaryRelevance::new().calibration_method(CalibrationMethod::Isotonic);
1096
1097 assert_eq!(cbr_platt.calibration_method, CalibrationMethod::Platt);
1099 assert_eq!(cbr_isotonic.calibration_method, CalibrationMethod::Isotonic);
1100 }
1101
1102 #[test]
1103 #[allow(non_snake_case)]
1104 fn test_mlknn_prior_probabilities() {
1105 let X = array![[1.0, 2.0], [2.0, 3.0], [3.0, 1.0], [4.0, 4.0]];
1106 let y = array![[1, 0], [0, 1], [1, 1], [0, 0]]; let mlknn = MLkNN::new().k(2).smooth(1.0);
1109 let trained_mlknn = mlknn
1110 .fit(&X.view(), &y)
1111 .expect("model fitting should succeed");
1112
1113 let priors = trained_mlknn.prior_probabilities();
1114 assert_eq!(priors.len(), 2);
1115
1116 assert!((priors[0] - 0.5).abs() < 1e-6);
1118 assert!((priors[1] - 0.5).abs() < 1e-6);
1119 }
1120}