1#![allow(non_snake_case)] use crate::multi_label::{BinaryRelevance, BinaryRelevanceTrained};
8use scirs2_core::ndarray::{s, Array1, Array2, ArrayView1, ArrayView2};
10use sklears_core::{
11 error::{Result as SklResult, SklearsError},
12 traits::{Estimator, Fit, Predict, Untrained},
13 types::Float,
14};
15use std::collections::HashMap;
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum ClassificationCriterion {
22 Gini,
24 Entropy,
26}
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub enum DAGInferenceMethod {
31 Greedy,
33 BeliefPropagation,
35 ExactILP,
37}
38
39#[derive(Debug, Clone)]
40struct DecisionNode {
41 is_leaf: bool,
42 prediction: Option<Array1<Float>>, feature_idx: Option<usize>,
44 threshold: Option<Float>,
45 left: Option<Box<DecisionNode>>,
46 right: Option<Box<DecisionNode>>,
47 n_samples: usize,
48 variance: Float, }
50
51#[derive(Debug, Clone)]
52pub struct ClassificationDecisionNode {
53 is_leaf: bool,
54 prediction: Option<Array1<i32>>, probabilities: Option<Array2<Float>>, feature_idx: Option<usize>,
57 threshold: Option<Float>,
58 left: Option<Box<ClassificationDecisionNode>>,
59 right: Option<Box<ClassificationDecisionNode>>,
60 #[allow(dead_code)]
61 n_samples: usize,
62 #[allow(dead_code)]
63 impurity: Float, }
65
66#[derive(Debug, Clone)]
89pub struct MultiTargetRegressionTree<S = Untrained> {
90 state: S,
91 max_depth: Option<usize>,
92 min_samples_split: usize,
93 min_samples_leaf: usize,
94 random_state: Option<u64>,
95}
96
97#[derive(Debug, Clone)]
98pub struct MultiTargetRegressionTreeTrained {
99 tree: DecisionNode,
100 n_features: usize,
101 n_targets: usize,
102 feature_importances: Array1<Float>,
103}
104
105impl MultiTargetRegressionTree<Untrained> {
106 pub fn new() -> Self {
108 Self {
109 state: Untrained,
110 max_depth: Some(5),
111 min_samples_split: 2,
112 min_samples_leaf: 1,
113 random_state: None,
114 }
115 }
116
117 pub fn max_depth(mut self, max_depth: Option<usize>) -> Self {
119 self.max_depth = max_depth;
120 self
121 }
122
123 pub fn min_samples_split(mut self, min_samples_split: usize) -> Self {
125 self.min_samples_split = min_samples_split;
126 self
127 }
128
129 pub fn min_samples_leaf(mut self, min_samples_leaf: usize) -> Self {
131 self.min_samples_leaf = min_samples_leaf;
132 self
133 }
134
135 pub fn random_state(mut self, random_state: Option<u64>) -> Self {
137 self.random_state = random_state;
138 self
139 }
140
141 pub fn get_max_depth(&self) -> Option<usize> {
143 self.max_depth
144 }
145
146 pub fn get_min_samples_split(&self) -> usize {
148 self.min_samples_split
149 }
150
151 pub fn get_min_samples_leaf(&self) -> usize {
153 self.min_samples_leaf
154 }
155
156 pub fn get_random_state(&self) -> Option<u64> {
158 self.random_state
159 }
160}
161
162impl Default for MultiTargetRegressionTree<Untrained> {
163 fn default() -> Self {
164 Self::new()
165 }
166}
167
168impl Estimator for MultiTargetRegressionTree<Untrained> {
169 type Config = ();
170 type Error = SklearsError;
171 type Float = Float;
172
173 fn config(&self) -> &Self::Config {
174 &()
175 }
176}
177
178impl Fit<ArrayView2<'_, Float>, Array2<Float>> for MultiTargetRegressionTree<Untrained> {
179 type Fitted = MultiTargetRegressionTree<MultiTargetRegressionTreeTrained>;
180
181 #[allow(non_snake_case)]
182 fn fit(self, X: &ArrayView2<'_, Float>, y: &Array2<Float>) -> SklResult<Self::Fitted> {
183 let X = X.to_owned();
184 let (n_samples, n_features) = X.dim();
185
186 if n_samples != y.nrows() {
187 return Err(SklearsError::InvalidInput(
188 "X and y must have the same number of samples".to_string(),
189 ));
190 }
191
192 let n_targets = y.ncols();
193 if n_targets == 0 {
194 return Err(SklearsError::InvalidInput(
195 "y must have at least one target".to_string(),
196 ));
197 }
198
199 if n_samples < self.min_samples_split {
200 return Err(SklearsError::InvalidInput(
201 "Number of samples is less than min_samples_split".to_string(),
202 ));
203 }
204
205 let indices: Vec<usize> = (0..n_samples).collect();
207 let tree = self.build_tree(&X, y, &indices, 0)?;
208
209 let mut feature_importances = Array1::<Float>::zeros(n_features);
211 self.calculate_feature_importances(&tree, &mut feature_importances, n_samples as Float);
212
213 let sum_importances: Float = feature_importances.sum();
215 if sum_importances > 0.0 {
216 feature_importances /= sum_importances;
217 }
218
219 Ok(MultiTargetRegressionTree {
220 state: MultiTargetRegressionTreeTrained {
221 tree,
222 n_features,
223 n_targets,
224 feature_importances,
225 },
226 max_depth: self.max_depth,
227 min_samples_split: self.min_samples_split,
228 min_samples_leaf: self.min_samples_leaf,
229 random_state: self.random_state,
230 })
231 }
232}
233
234impl MultiTargetRegressionTree<Untrained> {
235 fn build_tree(
236 &self,
237 X: &Array2<Float>,
238 y: &Array2<Float>,
239 indices: &[usize],
240 depth: usize,
241 ) -> SklResult<DecisionNode> {
242 let n_samples = indices.len();
243 let n_targets = y.ncols();
244
245 let mut prediction = Array1::<Float>::zeros(n_targets);
247 for &idx in indices {
248 for j in 0..n_targets {
249 prediction[j] += y[[idx, j]];
250 }
251 }
252 prediction /= n_samples as Float;
253
254 let mut variance = 0.0;
256 for &idx in indices {
257 for j in 0..n_targets {
258 let diff = y[[idx, j]] - prediction[j];
259 variance += diff * diff;
260 }
261 }
262 variance /= n_samples as Float;
263
264 let should_stop = n_samples < self.min_samples_split
266 || n_samples < self.min_samples_leaf
267 || self.max_depth.is_some_and(|max_d| depth >= max_d)
268 || variance < 1e-10;
269
270 if should_stop {
271 return Ok(DecisionNode {
272 is_leaf: true,
273 prediction: Some(prediction),
274 feature_idx: None,
275 threshold: None,
276 left: None,
277 right: None,
278 n_samples,
279 variance,
280 });
281 }
282
283 let (best_feature, best_threshold, best_variance_reduction) =
285 self.find_best_split(X, y, indices)?;
286
287 if best_variance_reduction <= 0.0 {
288 return Ok(DecisionNode {
289 is_leaf: true,
290 prediction: Some(prediction),
291 feature_idx: None,
292 threshold: None,
293 left: None,
294 right: None,
295 n_samples,
296 variance,
297 });
298 }
299
300 let (left_indices, right_indices) =
302 self.split_data(X, indices, best_feature, best_threshold);
303
304 if left_indices.len() < self.min_samples_leaf || right_indices.len() < self.min_samples_leaf
305 {
306 return Ok(DecisionNode {
307 is_leaf: true,
308 prediction: Some(prediction),
309 feature_idx: None,
310 threshold: None,
311 left: None,
312 right: None,
313 n_samples,
314 variance,
315 });
316 }
317
318 let left_child = self.build_tree(X, y, &left_indices, depth + 1)?;
320 let right_child = self.build_tree(X, y, &right_indices, depth + 1)?;
321
322 Ok(DecisionNode {
323 is_leaf: false,
324 prediction: None,
325 feature_idx: Some(best_feature),
326 threshold: Some(best_threshold),
327 left: Some(Box::new(left_child)),
328 right: Some(Box::new(right_child)),
329 n_samples,
330 variance,
331 })
332 }
333
334 fn find_best_split(
335 &self,
336 X: &Array2<Float>,
337 y: &Array2<Float>,
338 indices: &[usize],
339 ) -> SklResult<(usize, Float, Float)> {
340 let n_features = X.ncols();
341 let mut best_feature = 0;
342 let mut best_threshold = 0.0;
343 let mut best_variance_reduction = 0.0;
344
345 let current_variance = self.calculate_variance(y, indices);
347
348 for feature_idx in 0..n_features {
349 let mut feature_values: Vec<Float> =
351 indices.iter().map(|&idx| X[[idx, feature_idx]]).collect();
352 feature_values.sort_by(|a, b| a.partial_cmp(b).expect("operation should succeed"));
353 feature_values.dedup();
354
355 for i in 0..feature_values.len().saturating_sub(1) {
356 let threshold = (feature_values[i] + feature_values[i + 1]) / 2.0;
357
358 let (left_indices, right_indices) =
359 self.split_data(X, indices, feature_idx, threshold);
360
361 if left_indices.is_empty() || right_indices.is_empty() {
362 continue;
363 }
364
365 let left_variance = self.calculate_variance(y, &left_indices);
366 let right_variance = self.calculate_variance(y, &right_indices);
367
368 let weighted_variance = (left_indices.len() as Float * left_variance
369 + right_indices.len() as Float * right_variance)
370 / indices.len() as Float;
371
372 let variance_reduction = current_variance - weighted_variance;
373
374 if variance_reduction > best_variance_reduction {
375 best_variance_reduction = variance_reduction;
376 best_feature = feature_idx;
377 best_threshold = threshold;
378 }
379 }
380 }
381
382 Ok((best_feature, best_threshold, best_variance_reduction))
383 }
384
385 fn calculate_variance(&self, y: &Array2<Float>, indices: &[usize]) -> Float {
386 if indices.is_empty() {
387 return 0.0;
388 }
389
390 let n_targets = y.ncols();
391 let n_samples = indices.len();
392
393 let mut means = Array1::<Float>::zeros(n_targets);
395 for &idx in indices {
396 for j in 0..n_targets {
397 means[j] += y[[idx, j]];
398 }
399 }
400 means /= n_samples as Float;
401
402 let mut variance = 0.0;
404 for &idx in indices {
405 for j in 0..n_targets {
406 let diff = y[[idx, j]] - means[j];
407 variance += diff * diff;
408 }
409 }
410 variance / n_samples as Float
411 }
412
413 fn split_data(
414 &self,
415 X: &Array2<Float>,
416 indices: &[usize],
417 feature_idx: usize,
418 threshold: Float,
419 ) -> (Vec<usize>, Vec<usize>) {
420 let mut left_indices = Vec::new();
421 let mut right_indices = Vec::new();
422
423 for &idx in indices {
424 if X[[idx, feature_idx]] <= threshold {
425 left_indices.push(idx);
426 } else {
427 right_indices.push(idx);
428 }
429 }
430
431 (left_indices, right_indices)
432 }
433
434 fn calculate_feature_importances(
435 &self,
436 node: &DecisionNode,
437 importances: &mut Array1<Float>,
438 total_samples: Float,
439 ) {
440 if let (Some(feature_idx), Some(left), Some(right)) =
441 (node.feature_idx, &node.left, &node.right)
442 {
443 let importance = (node.n_samples as Float / total_samples) * node.variance;
444 importances[feature_idx] += importance;
445
446 self.calculate_feature_importances(left, importances, total_samples);
447 self.calculate_feature_importances(right, importances, total_samples);
448 }
449 }
450}
451
452impl MultiTargetRegressionTree<MultiTargetRegressionTreeTrained> {
453 pub fn feature_importances(&self) -> &Array1<Float> {
455 &self.state.feature_importances
456 }
457
458 pub fn n_features(&self) -> usize {
460 self.state.n_features
461 }
462
463 pub fn n_targets(&self) -> usize {
465 self.state.n_targets
466 }
467
468 #[allow(non_snake_case)]
470 pub fn predict(&self, X: &ArrayView2<'_, Float>) -> SklResult<Array2<Float>> {
471 let X = *X;
472 let (n_samples, n_features) = X.dim();
473
474 if n_features != self.state.n_features {
475 return Err(SklearsError::InvalidInput(
476 "Number of features doesn't match training data".to_string(),
477 ));
478 }
479
480 let mut predictions = Array2::<Float>::zeros((n_samples, self.state.n_targets));
481
482 for i in 0..n_samples {
483 let sample = X.slice(s![i, ..]);
484 let prediction = self.predict_single(&self.state.tree, &sample)?;
485 for j in 0..self.state.n_targets {
486 predictions[[i, j]] = prediction[j];
487 }
488 }
489
490 Ok(predictions)
491 }
492
493 fn predict_single(
494 &self,
495 node: &DecisionNode,
496 sample: &ArrayView1<'_, Float>,
497 ) -> SklResult<Array1<Float>> {
498 if node.is_leaf {
499 if let Some(ref prediction) = node.prediction {
500 Ok(prediction.clone())
501 } else {
502 Err(SklearsError::InvalidInput(
503 "Leaf node without prediction".to_string(),
504 ))
505 }
506 } else {
507 let feature_idx = node.feature_idx.ok_or(SklearsError::InvalidInput(
508 "Non-leaf node without feature index".to_string(),
509 ))?;
510 let threshold = node.threshold.ok_or(SklearsError::InvalidInput(
511 "Non-leaf node without threshold".to_string(),
512 ))?;
513
514 if sample[feature_idx] <= threshold {
515 if let Some(ref left) = node.left {
516 self.predict_single(left, sample)
517 } else {
518 Err(SklearsError::InvalidInput(
519 "Non-leaf node without left child".to_string(),
520 ))
521 }
522 } else if let Some(ref right) = node.right {
523 self.predict_single(right, sample)
524 } else {
525 Err(SklearsError::InvalidInput(
526 "Non-leaf node without right child".to_string(),
527 ))
528 }
529 }
530 }
531}
532
533#[derive(Debug, Clone)]
555pub struct MultiTargetDecisionTreeClassifier<S = Untrained> {
556 state: S,
557 max_depth: Option<usize>,
558 min_samples_split: usize,
559 min_samples_leaf: usize,
560 criterion: ClassificationCriterion,
561 random_state: Option<u64>,
562}
563
564#[derive(Debug, Clone)]
565pub struct MultiTargetDecisionTreeClassifierTrained {
566 tree: ClassificationDecisionNode,
567 n_features: usize,
568 n_targets: usize,
569 feature_importances: Array1<Float>,
570 classes_per_target: Vec<Vec<i32>>,
571}
572
573impl MultiTargetDecisionTreeClassifier<Untrained> {
574 pub fn new() -> Self {
576 Self {
577 state: Untrained,
578 max_depth: Some(5),
579 min_samples_split: 2,
580 min_samples_leaf: 1,
581 criterion: ClassificationCriterion::Gini,
582 random_state: None,
583 }
584 }
585
586 pub fn max_depth(mut self, max_depth: Option<usize>) -> Self {
588 self.max_depth = max_depth;
589 self
590 }
591
592 pub fn min_samples_split(mut self, min_samples_split: usize) -> Self {
594 self.min_samples_split = min_samples_split;
595 self
596 }
597
598 pub fn min_samples_leaf(mut self, min_samples_leaf: usize) -> Self {
600 self.min_samples_leaf = min_samples_leaf;
601 self
602 }
603
604 pub fn criterion(mut self, criterion: ClassificationCriterion) -> Self {
606 self.criterion = criterion;
607 self
608 }
609
610 pub fn random_state(mut self, random_state: Option<u64>) -> Self {
612 self.random_state = random_state;
613 self
614 }
615}
616
617impl Default for MultiTargetDecisionTreeClassifier<Untrained> {
618 fn default() -> Self {
619 Self::new()
620 }
621}
622
623impl Estimator for MultiTargetDecisionTreeClassifier<Untrained> {
624 type Config = ();
625 type Error = SklearsError;
626 type Float = Float;
627
628 fn config(&self) -> &Self::Config {
629 &()
630 }
631}
632
633impl Fit<ArrayView2<'_, Float>, Array2<i32>> for MultiTargetDecisionTreeClassifier<Untrained> {
634 type Fitted = MultiTargetDecisionTreeClassifier<MultiTargetDecisionTreeClassifierTrained>;
635
636 #[allow(non_snake_case)]
637 fn fit(self, X: &ArrayView2<'_, Float>, y: &Array2<i32>) -> SklResult<Self::Fitted> {
638 let X = X.to_owned();
639 let (n_samples, n_features) = X.dim();
640
641 if n_samples != y.nrows() {
642 return Err(SklearsError::InvalidInput(
643 "X and y must have the same number of samples".to_string(),
644 ));
645 }
646
647 let n_targets = y.ncols();
648 if n_targets == 0 {
649 return Err(SklearsError::InvalidInput(
650 "y must have at least one target".to_string(),
651 ));
652 }
653
654 let mut classes_per_target = Vec::new();
656 for target_idx in 0..n_targets {
657 let target_column = y.column(target_idx);
658 let mut unique_classes: Vec<i32> = target_column.iter().cloned().collect();
659 unique_classes.sort_unstable();
660 unique_classes.dedup();
661 classes_per_target.push(unique_classes);
662 }
663
664 let mut feature_importances = Array1::<Float>::zeros(n_features);
666
667 let indices: Vec<usize> = (0..n_samples).collect();
669 let tree = build_classification_tree(
670 &X,
671 y,
672 &indices,
673 &mut feature_importances,
674 0,
675 self.max_depth,
676 self.min_samples_split,
677 self.min_samples_leaf,
678 self.criterion,
679 &classes_per_target,
680 )?;
681
682 let importance_sum = feature_importances.sum();
684 if importance_sum > 0.0 {
685 feature_importances /= importance_sum;
686 }
687
688 let trained_state = MultiTargetDecisionTreeClassifierTrained {
689 tree,
690 n_features,
691 n_targets,
692 feature_importances,
693 classes_per_target,
694 };
695
696 Ok(MultiTargetDecisionTreeClassifier {
697 state: trained_state,
698 max_depth: self.max_depth,
699 min_samples_split: self.min_samples_split,
700 min_samples_leaf: self.min_samples_leaf,
701 criterion: self.criterion,
702 random_state: self.random_state,
703 })
704 }
705}
706
707impl Predict<ArrayView2<'_, Float>, Array2<i32>>
708 for MultiTargetDecisionTreeClassifier<MultiTargetDecisionTreeClassifierTrained>
709{
710 fn predict(&self, X: &ArrayView2<'_, Float>) -> SklResult<Array2<i32>> {
711 let (n_samples, n_features) = X.dim();
712 if n_features != self.state.n_features {
713 return Err(SklearsError::InvalidInput(
714 "X has different number of features than training data".to_string(),
715 ));
716 }
717
718 let mut predictions = Array2::<i32>::zeros((n_samples, self.state.n_targets));
719
720 for i in 0..n_samples {
721 let sample = X.row(i);
722 let prediction = predict_classification_sample(&self.state.tree, &sample);
723 for j in 0..self.state.n_targets {
724 predictions[[i, j]] = prediction[j];
725 }
726 }
727
728 Ok(predictions)
729 }
730}
731
732impl MultiTargetDecisionTreeClassifier<MultiTargetDecisionTreeClassifierTrained> {
733 pub fn feature_importances(&self) -> &Array1<Float> {
735 &self.state.feature_importances
736 }
737
738 pub fn predict_proba(&self, X: &ArrayView2<'_, Float>) -> SklResult<Vec<Array2<Float>>> {
740 let (n_samples, n_features) = X.dim();
741 if n_features != self.state.n_features {
742 return Err(SklearsError::InvalidInput(
743 "X has different number of features than training data".to_string(),
744 ));
745 }
746
747 let mut all_probabilities = Vec::new();
748
749 for target_idx in 0..self.state.n_targets {
751 let n_classes = self.state.classes_per_target[target_idx].len();
752 all_probabilities.push(Array2::<Float>::zeros((n_samples, n_classes)));
753 }
754
755 for i in 0..n_samples {
756 let sample = X.row(i);
757 let probabilities = predict_classification_probabilities(
758 &self.state.tree,
759 &sample,
760 &self.state.classes_per_target,
761 );
762
763 for (target_idx, target_probs) in probabilities.iter().enumerate() {
764 for (class_idx, &prob) in target_probs.iter().enumerate() {
765 all_probabilities[target_idx][[i, class_idx]] = prob;
766 }
767 }
768 }
769
770 Ok(all_probabilities)
771 }
772}
773
774#[derive(Debug, Clone)]
806pub struct RandomForestMultiOutput<S = Untrained> {
807 state: S,
808 n_estimators: usize,
809 max_depth: Option<usize>,
810 min_samples_split: usize,
811 min_samples_leaf: usize,
812 max_features: Option<usize>,
813 bootstrap: bool,
814 random_state: Option<u64>,
815}
816
817#[derive(Debug, Clone)]
818pub struct RandomForestMultiOutputTrained {
819 trees: Vec<MultiTargetRegressionTree<MultiTargetRegressionTreeTrained>>,
820 n_features: usize,
821 n_targets: usize,
822 feature_importances: Array1<Float>,
823}
824
825impl RandomForestMultiOutput<Untrained> {
826 pub fn new() -> Self {
828 Self {
829 state: Untrained,
830 n_estimators: 10,
831 max_depth: None,
832 min_samples_split: 2,
833 min_samples_leaf: 1,
834 max_features: None,
835 bootstrap: true,
836 random_state: None,
837 }
838 }
839
840 pub fn n_estimators(mut self, n_estimators: usize) -> Self {
842 self.n_estimators = n_estimators;
843 self
844 }
845
846 pub fn max_depth(mut self, max_depth: Option<usize>) -> Self {
848 self.max_depth = max_depth;
849 self
850 }
851
852 pub fn min_samples_split(mut self, min_samples_split: usize) -> Self {
854 self.min_samples_split = min_samples_split;
855 self
856 }
857
858 pub fn min_samples_leaf(mut self, min_samples_leaf: usize) -> Self {
860 self.min_samples_leaf = min_samples_leaf;
861 self
862 }
863
864 pub fn max_features(mut self, max_features: Option<usize>) -> Self {
866 self.max_features = max_features;
867 self
868 }
869
870 pub fn bootstrap(mut self, bootstrap: bool) -> Self {
872 self.bootstrap = bootstrap;
873 self
874 }
875
876 pub fn random_state(mut self, random_state: Option<u64>) -> Self {
878 self.random_state = random_state;
879 self
880 }
881
882 pub fn get_n_estimators(&self) -> usize {
884 self.n_estimators
885 }
886
887 pub fn get_max_depth(&self) -> Option<usize> {
889 self.max_depth
890 }
891
892 pub fn get_min_samples_split(&self) -> usize {
894 self.min_samples_split
895 }
896
897 pub fn get_min_samples_leaf(&self) -> usize {
899 self.min_samples_leaf
900 }
901
902 pub fn get_max_features(&self) -> Option<usize> {
904 self.max_features
905 }
906
907 pub fn get_bootstrap(&self) -> bool {
909 self.bootstrap
910 }
911
912 pub fn get_random_state(&self) -> Option<u64> {
914 self.random_state
915 }
916}
917
918impl Default for RandomForestMultiOutput<Untrained> {
919 fn default() -> Self {
920 Self::new()
921 }
922}
923
924impl Estimator for RandomForestMultiOutput<Untrained> {
925 type Config = ();
926 type Error = SklearsError;
927 type Float = Float;
928
929 fn config(&self) -> &Self::Config {
930 &()
931 }
932}
933
934impl Fit<ArrayView2<'_, Float>, Array2<Float>> for RandomForestMultiOutput<Untrained> {
935 type Fitted = RandomForestMultiOutput<RandomForestMultiOutputTrained>;
936
937 #[allow(non_snake_case)]
938 fn fit(self, X: &ArrayView2<'_, Float>, y: &Array2<Float>) -> SklResult<Self::Fitted> {
939 let X = X.to_owned();
940 let (n_samples, n_features) = X.dim();
941
942 if n_samples != y.nrows() {
943 return Err(SklearsError::InvalidInput(
944 "X and y must have the same number of samples".to_string(),
945 ));
946 }
947
948 let n_targets = y.ncols();
949 if n_targets == 0 {
950 return Err(SklearsError::InvalidInput(
951 "y must have at least one target".to_string(),
952 ));
953 }
954
955 let mut trees = Vec::new();
956 let mut feature_importances = Array1::<Float>::zeros(n_features);
957
958 for i in 0..self.n_estimators {
959 let (X_sample, y_sample) = if self.bootstrap {
961 self.create_bootstrap_sample(&X, y, i)?
962 } else {
963 (X.clone(), y.clone())
964 };
965
966 let tree = MultiTargetRegressionTree::new()
968 .max_depth(self.max_depth)
969 .min_samples_split(self.min_samples_split)
970 .min_samples_leaf(self.min_samples_leaf)
971 .random_state(self.random_state.map(|s| s.wrapping_add(i as u64)));
972
973 let trained_tree = tree.fit(&X_sample.view(), &y_sample)?;
974
975 feature_importances += trained_tree.feature_importances();
977
978 trees.push(trained_tree);
979 }
980
981 feature_importances /= self.n_estimators as Float;
983
984 Ok(RandomForestMultiOutput {
985 state: RandomForestMultiOutputTrained {
986 trees,
987 n_features,
988 n_targets,
989 feature_importances,
990 },
991 n_estimators: self.n_estimators,
992 max_depth: self.max_depth,
993 min_samples_split: self.min_samples_split,
994 min_samples_leaf: self.min_samples_leaf,
995 max_features: self.max_features,
996 bootstrap: self.bootstrap,
997 random_state: self.random_state,
998 })
999 }
1000}
1001
1002impl RandomForestMultiOutput<Untrained> {
1003 fn create_bootstrap_sample(
1004 &self,
1005 X: &Array2<Float>,
1006 y: &Array2<Float>,
1007 seed: usize,
1008 ) -> SklResult<(Array2<Float>, Array2<Float>)> {
1009 let n_samples = X.nrows();
1010 let mut rng_state = self.random_state.unwrap_or(42).wrapping_add(seed as u64);
1011
1012 let mut X_sample = Array2::<Float>::zeros(X.raw_dim());
1013 let mut y_sample = Array2::<Float>::zeros(y.raw_dim());
1014
1015 for i in 0..n_samples {
1016 rng_state = rng_state.wrapping_mul(1103515245).wrapping_add(12345);
1017 let idx = (rng_state / 65536) % (n_samples as u64);
1018
1019 X_sample
1020 .slice_mut(s![i, ..])
1021 .assign(&X.slice(s![idx as usize, ..]));
1022 y_sample
1023 .slice_mut(s![i, ..])
1024 .assign(&y.slice(s![idx as usize, ..]));
1025 }
1026
1027 Ok((X_sample, y_sample))
1028 }
1029}
1030
1031impl RandomForestMultiOutput<RandomForestMultiOutputTrained> {
1032 pub fn feature_importances(&self) -> &Array1<Float> {
1034 &self.state.feature_importances
1035 }
1036
1037 pub fn n_estimators(&self) -> usize {
1039 self.state.trees.len()
1040 }
1041
1042 pub fn n_features(&self) -> usize {
1044 self.state.n_features
1045 }
1046
1047 pub fn n_targets(&self) -> usize {
1049 self.state.n_targets
1050 }
1051
1052 #[allow(non_snake_case)]
1054 pub fn predict(&self, X: &ArrayView2<'_, Float>) -> SklResult<Array2<Float>> {
1055 let X = *X;
1056 let (n_samples, n_features) = X.dim();
1057
1058 if n_features != self.state.n_features {
1059 return Err(SklearsError::InvalidInput(
1060 "Number of features doesn't match training data".to_string(),
1061 ));
1062 }
1063
1064 let mut predictions = Array2::<Float>::zeros((n_samples, self.state.n_targets));
1065
1066 for tree in &self.state.trees {
1068 let tree_predictions = tree.predict(&X)?;
1069 predictions += &tree_predictions;
1070 }
1071
1072 predictions /= self.state.trees.len() as Float;
1073 Ok(predictions)
1074 }
1075}
1076
1077#[derive(Debug, Clone)]
1101pub struct TreeStructuredPredictor<State = Untrained> {
1102 max_depth: usize,
1103 branching_factor: usize,
1104 tree_structure: Vec<Vec<usize>>, #[allow(dead_code)]
1106 node_classifiers: HashMap<usize, String>,
1107 state: State,
1108}
1109
1110#[derive(Debug, Clone)]
1112pub struct TreeStructuredPredictorTrained {
1113 node_classifiers: HashMap<usize, BinaryRelevance<BinaryRelevanceTrained>>,
1114 tree_structure: Vec<Vec<usize>>,
1115 max_depth: usize,
1116 #[allow(dead_code)]
1117 n_nodes: usize,
1118}
1119
1120impl Default for TreeStructuredPredictor<Untrained> {
1121 fn default() -> Self {
1122 Self::new()
1123 }
1124}
1125
1126impl TreeStructuredPredictor<Untrained> {
1127 pub fn new() -> Self {
1129 Self {
1130 max_depth: 5,
1131 branching_factor: 2,
1132 tree_structure: Vec::new(),
1133 node_classifiers: HashMap::new(),
1134 state: Untrained,
1135 }
1136 }
1137
1138 pub fn max_depth(mut self, depth: usize) -> Self {
1140 self.max_depth = depth;
1141 self
1142 }
1143
1144 pub fn branching_factor(mut self, factor: usize) -> Self {
1146 self.branching_factor = factor;
1147 self
1148 }
1149
1150 pub fn tree_structure(mut self, structure: Vec<Vec<usize>>) -> Self {
1152 self.tree_structure = structure;
1153 self
1154 }
1155}
1156
1157impl Estimator for TreeStructuredPredictor<Untrained> {
1158 type Config = ();
1159 type Error = SklearsError;
1160 type Float = Float;
1161
1162 fn config(&self) -> &Self::Config {
1163 &()
1164 }
1165}
1166
1167impl Fit<Array2<Float>, Array2<i32>> for TreeStructuredPredictor<Untrained> {
1168 type Fitted = TreeStructuredPredictor<TreeStructuredPredictorTrained>;
1169
1170 fn fit(self, X: &Array2<Float>, y: &Array2<i32>) -> SklResult<Self::Fitted> {
1171 let (n_samples, _n_features) = X.dim();
1172 let (y_samples, max_path_length) = y.dim();
1173
1174 if n_samples != y_samples {
1175 return Err(SklearsError::InvalidInput(
1176 "Number of samples in X and y must match".to_string(),
1177 ));
1178 }
1179
1180 let tree_structure = if self.tree_structure.is_empty() {
1182 self.build_default_tree_structure()?
1183 } else {
1184 self.tree_structure.clone()
1185 };
1186
1187 let n_nodes = tree_structure.len();
1188 let mut node_classifiers = HashMap::new();
1189
1190 for node_id in 0..n_nodes {
1192 if !tree_structure[node_id].is_empty() {
1193 let (node_X, node_y) = self.create_node_training_data(
1196 &X.view(),
1197 &y.view(),
1198 node_id,
1199 &tree_structure,
1200 max_path_length,
1201 )?;
1202
1203 if !node_y.is_empty() {
1204 let classifier = BinaryRelevance::new();
1205 let trained_classifier = classifier.fit(&node_X.view(), &node_y)?;
1206 node_classifiers.insert(node_id, trained_classifier);
1207 }
1208 }
1209 }
1210
1211 Ok(TreeStructuredPredictor {
1212 max_depth: self.max_depth,
1213 branching_factor: self.branching_factor,
1214 tree_structure: tree_structure.clone(),
1215 node_classifiers: HashMap::new(),
1216 state: TreeStructuredPredictorTrained {
1217 node_classifiers,
1218 tree_structure,
1219 max_depth: self.max_depth,
1220 n_nodes,
1221 },
1222 })
1223 }
1224}
1225
1226impl TreeStructuredPredictor<Untrained> {
1227 fn build_default_tree_structure(&self) -> SklResult<Vec<Vec<usize>>> {
1229 let mut total_nodes = 0;
1230 for depth in 0..self.max_depth {
1231 total_nodes += self.branching_factor.pow(depth as u32);
1232 }
1233
1234 let mut tree_structure = vec![Vec::new(); total_nodes];
1235 let mut node_id = 0;
1236
1237 for depth in 0..(self.max_depth - 1) {
1239 let nodes_at_depth = self.branching_factor.pow(depth as u32);
1240
1241 for _ in 0..nodes_at_depth {
1242 for child in 0..self.branching_factor {
1243 let child_id = node_id + nodes_at_depth + child;
1244 if child_id < total_nodes {
1245 tree_structure[node_id].push(child_id);
1246 }
1247 }
1248 node_id += 1;
1249 }
1250 }
1251
1252 Ok(tree_structure)
1253 }
1254
1255 fn create_node_training_data(
1257 &self,
1258 X: &ArrayView2<Float>,
1259 y: &ArrayView2<i32>,
1260 node_id: usize,
1261 tree_structure: &[Vec<usize>],
1262 max_path_length: usize,
1263 ) -> SklResult<(Array2<Float>, Array2<i32>)> {
1264 let n_samples = X.nrows();
1265 let mut valid_samples = Vec::new();
1266 let mut node_labels = Vec::new();
1267
1268 for sample_idx in 0..n_samples {
1269 let path = y.row(sample_idx);
1270
1271 for pos in 0..max_path_length {
1273 if path[pos] as usize == node_id && pos + 1 < max_path_length {
1274 let next_node = path[pos + 1] as usize;
1276
1277 if let Some(child_idx) = tree_structure[node_id]
1279 .iter()
1280 .position(|&child| child == next_node)
1281 {
1282 valid_samples.push(sample_idx);
1283 node_labels.push(child_idx as i32);
1284 break;
1285 }
1286 }
1287 }
1288 }
1289
1290 let n_valid = valid_samples.len();
1292 if n_valid == 0 {
1293 return Ok((
1294 Array2::<Float>::zeros((0, X.ncols())),
1295 Array2::<i32>::zeros((0, 1)),
1296 ));
1297 }
1298
1299 let mut node_X = Array2::<Float>::zeros((n_valid, X.ncols()));
1300 let mut node_y = Array2::<i32>::zeros((n_valid, 1));
1301
1302 for (i, &sample_idx) in valid_samples.iter().enumerate() {
1303 for j in 0..X.ncols() {
1304 node_X[[i, j]] = X[[sample_idx, j]];
1305 }
1306 node_y[[i, 0]] = node_labels[i];
1307 }
1308
1309 Ok((node_X, node_y))
1310 }
1311}
1312
1313impl Predict<Array2<Float>, Array2<i32>>
1314 for TreeStructuredPredictor<TreeStructuredPredictorTrained>
1315{
1316 fn predict(&self, X: &Array2<Float>) -> SklResult<Array2<i32>> {
1317 let n_samples = X.nrows();
1318 let mut predictions = Array2::<i32>::zeros((n_samples, self.state.max_depth));
1319
1320 for sample_idx in 0..n_samples {
1321 let sample = X.row(sample_idx);
1322 let path = self.predict_tree_path(&sample)?;
1323
1324 for (pos, &node) in path.iter().enumerate() {
1325 if pos < self.state.max_depth {
1326 predictions[[sample_idx, pos]] = node as i32;
1327 }
1328 }
1329 }
1330
1331 Ok(predictions)
1332 }
1333}
1334
1335impl TreeStructuredPredictor<TreeStructuredPredictorTrained> {
1336 fn predict_tree_path(&self, sample: &ArrayView1<Float>) -> SklResult<Vec<usize>> {
1338 let mut path = Vec::new();
1339 let mut current_node = 0; path.push(current_node);
1341
1342 while !self.state.tree_structure[current_node].is_empty() {
1343 if let Some(classifier) = self.state.node_classifiers.get(¤t_node) {
1345 let sample_2d = sample.to_owned().insert_axis(scirs2_core::ndarray::Axis(0));
1346 let prediction = classifier.predict(&sample_2d.view())?;
1347 let child_idx = prediction[[0, 0]] as usize;
1348
1349 if child_idx < self.state.tree_structure[current_node].len() {
1350 current_node = self.state.tree_structure[current_node][child_idx];
1351 path.push(current_node);
1352 } else {
1353 break; }
1355 } else {
1356 break; }
1358 }
1359
1360 Ok(path)
1361 }
1362
1363 pub fn tree_structure(&self) -> &Vec<Vec<usize>> {
1365 &self.state.tree_structure
1366 }
1367}
1368
1369#[allow(clippy::too_many_arguments)]
1373pub fn build_classification_tree(
1374 X: &Array2<Float>,
1375 y: &Array2<i32>,
1376 indices: &[usize],
1377 feature_importances: &mut Array1<Float>,
1378 depth: usize,
1379 max_depth: Option<usize>,
1380 min_samples_split: usize,
1381 min_samples_leaf: usize,
1382 criterion: ClassificationCriterion,
1383 classes_per_target: &[Vec<i32>],
1384) -> SklResult<ClassificationDecisionNode> {
1385 let n_samples = indices.len();
1386
1387 let (current_impurity, prediction, probabilities) =
1389 calculate_classification_metrics(y, indices, classes_per_target, criterion);
1390
1391 let should_stop = n_samples < min_samples_split
1393 || (max_depth.is_some() && depth >= max_depth.expect("operation should succeed"))
1394 || current_impurity == 0.0;
1395
1396 if should_stop {
1397 return Ok(ClassificationDecisionNode {
1398 is_leaf: true,
1399 prediction: Some(prediction),
1400 probabilities: Some(probabilities),
1401 feature_idx: None,
1402 threshold: None,
1403 left: None,
1404 right: None,
1405 n_samples,
1406 impurity: current_impurity,
1407 });
1408 }
1409
1410 let mut best_impurity_reduction = 0.0;
1412 let mut best_feature = None;
1413 let mut best_threshold = None;
1414 let mut best_left_indices = Vec::new();
1415 let mut best_right_indices = Vec::new();
1416
1417 for feature_idx in 0..X.ncols() {
1418 let mut feature_values: Vec<Float> = indices.iter().map(|&i| X[[i, feature_idx]]).collect();
1420 feature_values.sort_by(|a, b| a.partial_cmp(b).expect("operation should succeed"));
1421 feature_values.dedup();
1422
1423 for i in 0..feature_values.len().saturating_sub(1) {
1425 let threshold = (feature_values[i] + feature_values[i + 1]) / 2.0;
1426
1427 let (left_indices, right_indices): (Vec<usize>, Vec<usize>) = indices
1428 .iter()
1429 .partition(|&&idx| X[[idx, feature_idx]] <= threshold);
1430
1431 if left_indices.len() < min_samples_leaf || right_indices.len() < min_samples_leaf {
1433 continue;
1434 }
1435
1436 let (left_impurity, _, _) =
1438 calculate_classification_metrics(y, &left_indices, classes_per_target, criterion);
1439 let (right_impurity, _, _) =
1440 calculate_classification_metrics(y, &right_indices, classes_per_target, criterion);
1441
1442 let weighted_impurity = (left_indices.len() as Float * left_impurity
1443 + right_indices.len() as Float * right_impurity)
1444 / n_samples as Float;
1445 let impurity_reduction = current_impurity - weighted_impurity;
1446
1447 if impurity_reduction > best_impurity_reduction {
1448 best_impurity_reduction = impurity_reduction;
1449 best_feature = Some(feature_idx);
1450 best_threshold = Some(threshold);
1451 best_left_indices = left_indices;
1452 best_right_indices = right_indices;
1453 }
1454 }
1455 }
1456
1457 if best_feature.is_none() || best_impurity_reduction <= 0.0 {
1459 return Ok(ClassificationDecisionNode {
1460 is_leaf: true,
1461 prediction: Some(prediction),
1462 probabilities: Some(probabilities),
1463 feature_idx: None,
1464 threshold: None,
1465 left: None,
1466 right: None,
1467 n_samples,
1468 impurity: current_impurity,
1469 });
1470 }
1471
1472 feature_importances[best_feature.expect("sampling should succeed")] +=
1474 best_impurity_reduction * n_samples as Float;
1475
1476 let left_child = build_classification_tree(
1478 X,
1479 y,
1480 &best_left_indices,
1481 feature_importances,
1482 depth + 1,
1483 max_depth,
1484 min_samples_split,
1485 min_samples_leaf,
1486 criterion,
1487 classes_per_target,
1488 )?;
1489
1490 let right_child = build_classification_tree(
1491 X,
1492 y,
1493 &best_right_indices,
1494 feature_importances,
1495 depth + 1,
1496 max_depth,
1497 min_samples_split,
1498 min_samples_leaf,
1499 criterion,
1500 classes_per_target,
1501 )?;
1502
1503 Ok(ClassificationDecisionNode {
1504 is_leaf: false,
1505 prediction: Some(prediction),
1506 probabilities: Some(probabilities),
1507 feature_idx: best_feature,
1508 threshold: best_threshold,
1509 left: Some(Box::new(left_child)),
1510 right: Some(Box::new(right_child)),
1511 n_samples,
1512 impurity: current_impurity,
1513 })
1514}
1515
1516pub fn calculate_classification_metrics(
1518 y: &Array2<i32>,
1519 indices: &[usize],
1520 classes_per_target: &[Vec<i32>],
1521 criterion: ClassificationCriterion,
1522) -> (Float, Array1<i32>, Array2<Float>) {
1523 let n_targets = y.ncols();
1524 let n_samples = indices.len();
1525
1526 let mut prediction = Array1::<i32>::zeros(n_targets);
1527 let mut total_impurity = 0.0;
1528
1529 let max_classes = classes_per_target
1531 .iter()
1532 .map(|classes| classes.len())
1533 .max()
1534 .unwrap_or(0);
1535 let mut probabilities = Array2::<Float>::zeros((n_targets, max_classes));
1536
1537 for target_idx in 0..n_targets {
1538 let classes = &classes_per_target[target_idx];
1539 let n_classes = classes.len();
1540
1541 let mut class_counts = vec![0; n_classes];
1543 for &sample_idx in indices {
1544 let class_label = y[[sample_idx, target_idx]];
1545 if let Some(class_idx) = classes.iter().position(|&c| c == class_label) {
1546 class_counts[class_idx] += 1;
1547 }
1548 }
1549
1550 let majority_class_idx = class_counts
1552 .iter()
1553 .enumerate()
1554 .max_by_key(|(_, &count)| count)
1555 .map(|(idx, _)| idx)
1556 .unwrap_or(0);
1557
1558 prediction[target_idx] = classes[majority_class_idx];
1559
1560 let mut target_impurity = 0.0;
1562 for (class_idx, &count) in class_counts.iter().enumerate() {
1563 let prob = count as Float / n_samples as Float;
1564 probabilities[[target_idx, class_idx]] = prob;
1565
1566 if prob > 0.0 {
1567 target_impurity += match criterion {
1568 ClassificationCriterion::Gini => prob * (1.0 - prob),
1569 ClassificationCriterion::Entropy => -prob * prob.ln(),
1570 };
1571 }
1572 }
1573
1574 if matches!(criterion, ClassificationCriterion::Gini) {
1576 target_impurity *= 2.0;
1577 }
1578
1579 total_impurity += target_impurity;
1580 }
1581
1582 total_impurity /= n_targets as Float;
1584
1585 (total_impurity, prediction, probabilities)
1586}
1587
1588pub fn predict_classification_sample(
1590 node: &ClassificationDecisionNode,
1591 sample: &ArrayView1<Float>,
1592) -> Array1<i32> {
1593 if node.is_leaf {
1594 return node
1595 .prediction
1596 .as_ref()
1597 .expect("operation should succeed")
1598 .clone();
1599 }
1600
1601 let feature_value = sample[node.feature_idx.expect("sampling should succeed")];
1602 let threshold = node.threshold.expect("operation should succeed");
1603
1604 if feature_value <= threshold {
1605 predict_classification_sample(node.left.as_ref().expect("sampling should succeed"), sample)
1606 } else {
1607 predict_classification_sample(
1608 node.right.as_ref().expect("sampling should succeed"),
1609 sample,
1610 )
1611 }
1612}
1613
1614pub fn predict_classification_probabilities(
1616 node: &ClassificationDecisionNode,
1617 sample: &ArrayView1<Float>,
1618 classes_per_target: &[Vec<i32>],
1619) -> Vec<Array1<Float>> {
1620 if node.is_leaf {
1621 let mut result = Vec::new();
1622 for (target_idx, target_classes) in classes_per_target.iter().enumerate() {
1623 let n_classes = target_classes.len();
1624 let mut target_probs = Array1::<Float>::zeros(n_classes);
1625 for class_idx in 0..n_classes {
1626 target_probs[class_idx] = node
1627 .probabilities
1628 .as_ref()
1629 .expect("operation should succeed")[[target_idx, class_idx]];
1630 }
1631 result.push(target_probs);
1632 }
1633 return result;
1634 }
1635
1636 let feature_value = sample[node.feature_idx.expect("sampling should succeed")];
1637 let threshold = node.threshold.expect("operation should succeed");
1638
1639 if feature_value <= threshold {
1640 predict_classification_probabilities(
1641 node.left.as_ref().expect("operation should succeed"),
1642 sample,
1643 classes_per_target,
1644 )
1645 } else {
1646 predict_classification_probabilities(
1647 node.right.as_ref().expect("operation should succeed"),
1648 sample,
1649 classes_per_target,
1650 )
1651 }
1652}