Skip to main content

sklears_multioutput/
tree.rs

1//! Tree-based multi-output algorithms
2//!
3//! This module provides tree-based algorithms for multi-output prediction problems,
4//! including decision trees, random forests, and structured predictors.
5#![allow(non_snake_case)] // Standard ML notation: X for feature matrices, K for kernels
6
7use crate::multi_label::{BinaryRelevance, BinaryRelevanceTrained};
8// Use SciRS2-Core for arrays and random number generation (SciRS2 Policy)
9use 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// Tree-related enums and helper structures
18
19/// Classification criterion for decision trees
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum ClassificationCriterion {
22    /// Gini impurity
23    Gini,
24    /// Information gain / entropy
25    Entropy,
26}
27
28/// DAG inference methods
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub enum DAGInferenceMethod {
31    /// Greedy inference following topological order
32    Greedy,
33    /// Belief propagation on the DAG
34    BeliefPropagation,
35    /// Integer linear programming for exact inference
36    ExactILP,
37}
38
39#[derive(Debug, Clone)]
40struct DecisionNode {
41    is_leaf: bool,
42    prediction: Option<Array1<Float>>, // Mean values for each target
43    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, // Sum of variances across all targets
49}
50
51#[derive(Debug, Clone)]
52pub struct ClassificationDecisionNode {
53    is_leaf: bool,
54    prediction: Option<Array1<i32>>, // Mode/majority class for each target
55    probabilities: Option<Array2<Float>>, // Probability distributions per target
56    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, // Combined impurity across all targets
64}
65
66/// Multi-Target Regression Tree
67///
68/// A decision tree regressor that can handle multiple target variables simultaneously.
69/// Uses joint variance reduction for optimal splits across all targets.
70///
71/// # Examples
72///
73/// ```
74/// use sklears_multioutput::MultiTargetRegressionTree;
75/// use sklears_core::traits::{Predict, Fit};
76/// // Use SciRS2-Core for arrays and random number generation (SciRS2 Policy)
77/// use scirs2_core::ndarray::array;
78///
79/// let X = array![[1.0, 2.0], [2.0, 3.0], [3.0, 1.0], [4.0, 4.0]];
80/// let y = array![[1.5, 2.5], [2.5, 3.5], [3.5, 1.5], [4.5, 4.5]];
81///
82/// let tree = MultiTargetRegressionTree::new()
83///     .max_depth(Some(3))
84///     .min_samples_split(2);
85/// let trained_tree = tree.fit(&X.view(), &y).unwrap();
86/// let predictions = trained_tree.predict(&X.view()).unwrap();
87/// ```
88#[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    /// Create a new MultiTargetRegressionTree instance
107    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    /// Set the maximum depth of the tree
118    pub fn max_depth(mut self, max_depth: Option<usize>) -> Self {
119        self.max_depth = max_depth;
120        self
121    }
122
123    /// Set the minimum number of samples required to split an internal node
124    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    /// Set the minimum number of samples required to be at a leaf node
130    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    /// Set the random state for reproducible results
136    pub fn random_state(mut self, random_state: Option<u64>) -> Self {
137        self.random_state = random_state;
138        self
139    }
140
141    /// Get the maximum depth of the tree
142    pub fn get_max_depth(&self) -> Option<usize> {
143        self.max_depth
144    }
145
146    /// Get the minimum number of samples required to split an internal node
147    pub fn get_min_samples_split(&self) -> usize {
148        self.min_samples_split
149    }
150
151    /// Get the minimum number of samples required to be at a leaf node
152    pub fn get_min_samples_leaf(&self) -> usize {
153        self.min_samples_leaf
154    }
155
156    /// Get the random state
157    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        // Build the tree
206        let indices: Vec<usize> = (0..n_samples).collect();
207        let tree = self.build_tree(&X, y, &indices, 0)?;
208
209        // Calculate feature importances (simplified)
210        let mut feature_importances = Array1::<Float>::zeros(n_features);
211        self.calculate_feature_importances(&tree, &mut feature_importances, n_samples as Float);
212
213        // Normalize feature importances
214        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        // Calculate current prediction (mean of targets)
246        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        // Calculate variance across all targets
255        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        // Check stopping criteria
265        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        // Find best split
284        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        // Split the data
301        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        // Recursively build child nodes
319        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        // Calculate current variance
346        let current_variance = self.calculate_variance(y, indices);
347
348        for feature_idx in 0..n_features {
349            // Get unique feature values
350            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        // Calculate means
394        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        // Calculate variance
403        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    /// Get the feature importances
454    pub fn feature_importances(&self) -> &Array1<Float> {
455        &self.state.feature_importances
456    }
457
458    /// Get the number of features
459    pub fn n_features(&self) -> usize {
460        self.state.n_features
461    }
462
463    /// Get the number of targets
464    pub fn n_targets(&self) -> usize {
465        self.state.n_targets
466    }
467
468    /// Predict using the fitted tree
469    #[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/// Multi-Target Decision Tree Classifier
534///
535/// A decision tree classifier that can handle multiple target variables simultaneously.
536/// Uses joint entropy/gini reduction for optimal splits across all targets.
537///
538/// # Examples
539///
540/// ```
541/// use sklears_multioutput::MultiTargetDecisionTreeClassifier;
542/// use sklears_core::traits::{Predict, Fit};
543/// // Use SciRS2-Core for arrays and random number generation (SciRS2 Policy)
544/// use scirs2_core::ndarray::array;
545///
546/// let X = array![[1.0, 2.0], [2.0, 3.0], [3.0, 1.0], [4.0, 4.0]];
547/// let y = array![[0, 1], [1, 0], [1, 1], [0, 0]]; // Two binary classification targets
548///
549/// let tree = MultiTargetDecisionTreeClassifier::new()
550///     .max_depth(Some(3));
551/// let trained_tree = tree.fit(&X.view(), &y).unwrap();
552/// let predictions = trained_tree.predict(&X.view()).unwrap();
553/// ```
554#[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    /// Create a new MultiTargetDecisionTreeClassifier instance
575    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    /// Set the maximum depth of the tree
587    pub fn max_depth(mut self, max_depth: Option<usize>) -> Self {
588        self.max_depth = max_depth;
589        self
590    }
591
592    /// Set the minimum number of samples required to split an internal node
593    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    /// Set the minimum number of samples required to be at a leaf node
599    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    /// Set the split criterion
605    pub fn criterion(mut self, criterion: ClassificationCriterion) -> Self {
606        self.criterion = criterion;
607        self
608    }
609
610    /// Set the random state for reproducible results
611    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        // Get unique classes for each target
655        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        // Initialize feature importances
665        let mut feature_importances = Array1::<Float>::zeros(n_features);
666
667        // Build the tree
668        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        // Normalize feature importances
683        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    /// Get feature importances
734    pub fn feature_importances(&self) -> &Array1<Float> {
735        &self.state.feature_importances
736    }
737
738    /// Predict class probabilities for each target
739    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        // Initialize probability arrays for each target
750        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/// Random Forest Multi-Output Extension
775///
776/// A random forest that can handle multiple output variables simultaneously.
777/// This implementation creates multiple multi-target regression trees and
778/// averages their predictions for robust multi-output regression.
779///
780/// # Mathematical Foundation
781///
782/// The random forest combines multiple multi-target regression trees:
783/// - Each tree is trained on a bootstrap sample of the data
784/// - Each tree considers only a random subset of features at each split
785/// - Final prediction is the average of all tree predictions
786/// - Feature importance is averaged across all trees
787///
788/// # Examples
789///
790/// ```
791/// use sklears_multioutput::RandomForestMultiOutput;
792/// use sklears_core::traits::{Predict, Fit};
793/// // Use SciRS2-Core for arrays and random number generation (SciRS2 Policy)
794/// use scirs2_core::ndarray::array;
795///
796/// let X = array![[1.0, 2.0], [2.0, 3.0], [3.0, 1.0], [4.0, 4.0]];
797/// let y = array![[1.5, 2.5], [2.5, 3.5], [3.5, 1.5], [4.5, 4.5]];
798///
799/// let forest = RandomForestMultiOutput::new()
800///     .n_estimators(10)
801///     .max_depth(Some(3));
802/// let trained_forest = forest.fit(&X.view(), &y).unwrap();
803/// let predictions = trained_forest.predict(&X.view()).unwrap();
804/// ```
805#[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    /// Create a new RandomForestMultiOutput instance
827    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    /// Set the number of trees in the forest
841    pub fn n_estimators(mut self, n_estimators: usize) -> Self {
842        self.n_estimators = n_estimators;
843        self
844    }
845
846    /// Set the maximum depth of the trees
847    pub fn max_depth(mut self, max_depth: Option<usize>) -> Self {
848        self.max_depth = max_depth;
849        self
850    }
851
852    /// Set the minimum number of samples required to split an internal node
853    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    /// Set the minimum number of samples required to be at a leaf node
859    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    /// Set the number of features to consider when looking for the best split
865    pub fn max_features(mut self, max_features: Option<usize>) -> Self {
866        self.max_features = max_features;
867        self
868    }
869
870    /// Set whether to use bootstrap samples when building trees
871    pub fn bootstrap(mut self, bootstrap: bool) -> Self {
872        self.bootstrap = bootstrap;
873        self
874    }
875
876    /// Set the random state for reproducible results
877    pub fn random_state(mut self, random_state: Option<u64>) -> Self {
878        self.random_state = random_state;
879        self
880    }
881
882    /// Get the number of trees in the forest
883    pub fn get_n_estimators(&self) -> usize {
884        self.n_estimators
885    }
886
887    /// Get the maximum depth of the trees
888    pub fn get_max_depth(&self) -> Option<usize> {
889        self.max_depth
890    }
891
892    /// Get the minimum number of samples required to split an internal node
893    pub fn get_min_samples_split(&self) -> usize {
894        self.min_samples_split
895    }
896
897    /// Get the minimum number of samples required to be at a leaf node
898    pub fn get_min_samples_leaf(&self) -> usize {
899        self.min_samples_leaf
900    }
901
902    /// Get the maximum number of features to consider when looking for the best split
903    pub fn get_max_features(&self) -> Option<usize> {
904        self.max_features
905    }
906
907    /// Get whether bootstrap samples are used when building trees
908    pub fn get_bootstrap(&self) -> bool {
909        self.bootstrap
910    }
911
912    /// Get the random state
913    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            // Create bootstrap sample if needed
960            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            // Create and train tree
967            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            // Accumulate feature importances
976            feature_importances += trained_tree.feature_importances();
977
978            trees.push(trained_tree);
979        }
980
981        // Average feature importances
982        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    /// Get the feature importances averaged across all trees
1033    pub fn feature_importances(&self) -> &Array1<Float> {
1034        &self.state.feature_importances
1035    }
1036
1037    /// Get the number of estimators (trees)
1038    pub fn n_estimators(&self) -> usize {
1039        self.state.trees.len()
1040    }
1041
1042    /// Get the number of features
1043    pub fn n_features(&self) -> usize {
1044        self.state.n_features
1045    }
1046
1047    /// Get the number of targets
1048    pub fn n_targets(&self) -> usize {
1049        self.state.n_targets
1050    }
1051
1052    /// Predict using the forest (average of all tree predictions)
1053    #[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        // Average predictions from all trees
1067        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/// Tree-Structured Prediction
1078///
1079/// A structured prediction method for outputs that follow a tree structure.
1080/// Each internal node in the tree has a classifier that decides which branch to take,
1081/// enabling hierarchical multi-label classification.
1082///
1083/// # Examples
1084///
1085/// ```
1086/// use sklears_core::traits::{Predict, Fit};
1087/// use sklears_multioutput::TreeStructuredPredictor;
1088/// // Use SciRS2-Core for arrays and random number generation (SciRS2 Policy)
1089/// use scirs2_core::ndarray::array;
1090///
1091/// let X = array![[1.0, 2.0], [2.0, 3.0]];
1092/// let y = array![[0, 1, 2], [1, 2, 0]]; // Tree paths
1093///
1094/// let tree_predictor = TreeStructuredPredictor::new()
1095///     .max_depth(3)
1096///     .branching_factor(3);
1097/// let trained_predictor = tree_predictor.fit(&X, &y).unwrap();
1098/// let predictions = trained_predictor.predict(&X).unwrap();
1099/// ```
1100#[derive(Debug, Clone)]
1101pub struct TreeStructuredPredictor<State = Untrained> {
1102    max_depth: usize,
1103    branching_factor: usize,
1104    tree_structure: Vec<Vec<usize>>, // Adjacency list representation
1105    #[allow(dead_code)]
1106    node_classifiers: HashMap<usize, String>,
1107    state: State,
1108}
1109
1110/// Trained state for Tree-Structured Predictor
1111#[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    /// Create new tree-structured predictor
1128    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    /// Set maximum tree depth
1139    pub fn max_depth(mut self, depth: usize) -> Self {
1140        self.max_depth = depth;
1141        self
1142    }
1143
1144    /// Set branching factor
1145    pub fn branching_factor(mut self, factor: usize) -> Self {
1146        self.branching_factor = factor;
1147        self
1148    }
1149
1150    /// Set custom tree structure
1151    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        // Build tree structure if not provided
1181        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        // Train classifier for each internal node
1191        for node_id in 0..n_nodes {
1192            if !tree_structure[node_id].is_empty() {
1193                // Internal node
1194                // Create binary classification data for this node
1195                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    /// Build default tree structure
1228    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        // Build complete tree
1238        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    /// Create training data for a specific node
1256    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            // Check if this sample's path passes through this node
1272            for pos in 0..max_path_length {
1273                if path[pos] as usize == node_id && pos + 1 < max_path_length {
1274                    // Sample passes through this node, determine which child to predict
1275                    let next_node = path[pos + 1] as usize;
1276
1277                    // Find which child index this corresponds to
1278                    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        // Create training data for this node
1291        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    /// Predict tree path for a single sample
1337    fn predict_tree_path(&self, sample: &ArrayView1<Float>) -> SklResult<Vec<usize>> {
1338        let mut path = Vec::new();
1339        let mut current_node = 0; // Start at root
1340        path.push(current_node);
1341
1342        while !self.state.tree_structure[current_node].is_empty() {
1343            // Internal node - predict which child to take
1344            if let Some(classifier) = self.state.node_classifiers.get(&current_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; // Invalid prediction
1354                }
1355            } else {
1356                break; // No classifier for this node
1357            }
1358        }
1359
1360        Ok(path)
1361    }
1362
1363    /// Get tree structure
1364    pub fn tree_structure(&self) -> &Vec<Vec<usize>> {
1365        &self.state.tree_structure
1366    }
1367}
1368
1369// Supporting functions for tree algorithms
1370
1371/// Build a classification decision tree recursively
1372#[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    // Calculate current impurity and predictions
1388    let (current_impurity, prediction, probabilities) =
1389        calculate_classification_metrics(y, indices, classes_per_target, criterion);
1390
1391    // Check stopping criteria
1392    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    // Find best split
1411    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        // Get unique values for this feature in current samples
1419        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        // Try splits between consecutive unique values
1424        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            // Check minimum samples per leaf
1432            if left_indices.len() < min_samples_leaf || right_indices.len() < min_samples_leaf {
1433                continue;
1434            }
1435
1436            // Calculate impurity reduction
1437            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 no good split found, create leaf
1458    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    // Update feature importance
1473    feature_importances[best_feature.expect("sampling should succeed")] +=
1474        best_impurity_reduction * n_samples as Float;
1475
1476    // Recursively build left and right subtrees
1477    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
1516/// Calculate classification metrics for a set of samples
1517pub 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    // Calculate max number of classes across all targets for probability matrix
1530    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        // Count class frequencies
1542        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        // Find majority class
1551        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        // Calculate probabilities and impurity
1561        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        // For Gini, multiply by 2; for Entropy, it's already correct
1575        if matches!(criterion, ClassificationCriterion::Gini) {
1576            target_impurity *= 2.0;
1577        }
1578
1579        total_impurity += target_impurity;
1580    }
1581
1582    // Average impurity across targets
1583    total_impurity /= n_targets as Float;
1584
1585    (total_impurity, prediction, probabilities)
1586}
1587
1588/// Predict for a single classification sample
1589pub 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
1614/// Predict probabilities for a single classification sample
1615pub 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}