Skip to main content

smartcore/tree/
decision_tree_classifier.rs

1//! # Decision Tree Classifier
2//!
3//! The process of building a classification tree is similar to the task of building a [regression tree](../decision_tree_regressor/index.html).
4//! However, in the classification setting one of these criteriums is used for making the binary splits:
5//!
6//! * Classification error rate, \\(E = 1 - \max_k(p_{mk})\\)
7//!
8//! * Gini index, \\(G = \sum_{k=1}^K p_{mk}(1 - p_{mk})\\)
9//!
10//! * Entropy, \\(D = -\sum_{k=1}^K p_{mk}\log p_{mk}\\)
11//!
12//! where \\(p_{mk}\\) represents the proportion of training observations in the *m*th region that are from the *k*th class.
13//!
14//! The classification error rate is simply the fraction of the training observations in that region that do not belong to the most common class.
15//! Classification error is not sufficiently sensitive for tree-growing, and in practice Gini index or Entropy are preferable.
16//!
17//! The Gini index is referred to as a measure of node purity. A small value indicates that a node contains predominantly observations from a single class.
18//!
19//! The Entropy, like Gini index will take on a small value if the *m*th node is pure.
20//!
21//! Example:
22//!
23//! ```
24//! use rand::Rng;
25//!
26//! use smartcore::linalg::basic::matrix::DenseMatrix;
27//! use smartcore::tree::decision_tree_classifier::*;
28//!
29//! // Iris dataset
30//! let x = DenseMatrix::from_2d_array(&[
31//!            &[5.1, 3.5, 1.4, 0.2],
32//!            &[4.9, 3.0, 1.4, 0.2],
33//!            &[4.7, 3.2, 1.3, 0.2],
34//!            &[4.6, 3.1, 1.5, 0.2],
35//!            &[5.0, 3.6, 1.4, 0.2],
36//!            &[5.4, 3.9, 1.7, 0.4],
37//!            &[4.6, 3.4, 1.4, 0.3],
38//!            &[5.0, 3.4, 1.5, 0.2],
39//!            &[4.4, 2.9, 1.4, 0.2],
40//!            &[4.9, 3.1, 1.5, 0.1],
41//!            &[7.0, 3.2, 4.7, 1.4],
42//!            &[6.4, 3.2, 4.5, 1.5],
43//!            &[6.9, 3.1, 4.9, 1.5],
44//!            &[5.5, 2.3, 4.0, 1.3],
45//!            &[6.5, 2.8, 4.6, 1.5],
46//!            &[5.7, 2.8, 4.5, 1.3],
47//!            &[6.3, 3.3, 4.7, 1.6],
48//!            &[4.9, 2.4, 3.3, 1.0],
49//!            &[6.6, 2.9, 4.6, 1.3],
50//!            &[5.2, 2.7, 3.9, 1.4],
51//!         ]).unwrap();
52//! let y = vec![ 0, 0, 0, 0, 0, 0, 0, 0,
53//!            1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1];
54//!
55//! let tree = DecisionTreeClassifier::fit(&x, &y, Default::default()).unwrap();
56//!
57//! let y_hat = tree.predict(&x).unwrap(); // use the same data for prediction
58//! ```
59//!
60//!
61//! ## References:
62//! * ["Classification and regression trees", Breiman, L, Friedman, J H, Olshen, R A, and Stone, C J, 1984](https://www.sciencebase.gov/catalog/item/545d07dfe4b0ba8303f728c1)
63//! * ["An Introduction to Statistical Learning", James G., Witten D., Hastie T., Tibshirani R., Chapter 8](http://faculty.marshall.usc.edu/gareth-james/ISL/)
64//!
65//! <script src="https://polyfill.io/v3/polyfill.min.js?features=es6"></script>
66//! <script id="MathJax-script" async src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>
67use std::collections::LinkedList;
68use std::default::Default;
69use std::fmt::Debug;
70use std::marker::PhantomData;
71
72use rand::Rng;
73use rand::seq::SliceRandom;
74
75#[cfg(feature = "serde")]
76use serde::{Deserialize, Serialize};
77
78use crate::api::{Predictor, SupervisedEstimator};
79use crate::error::Failed;
80use crate::linalg::basic::arrays::MutArray;
81use crate::linalg::basic::arrays::{Array1, Array2, MutArrayView1};
82use crate::linalg::basic::matrix::DenseMatrix;
83use crate::numbers::basenum::Number;
84use crate::rand_custom::get_rng_impl;
85
86#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
87#[derive(Debug, Clone)]
88/// Parameters of Decision Tree
89pub struct DecisionTreeClassifierParameters {
90    #[cfg_attr(feature = "serde", serde(default))]
91    /// Split criteria to use when building a tree.
92    pub criterion: SplitCriterion,
93    #[cfg_attr(feature = "serde", serde(default))]
94    /// The maximum depth of the tree.
95    pub max_depth: Option<u16>,
96    #[cfg_attr(feature = "serde", serde(default))]
97    /// The minimum number of samples required to be at a leaf node.
98    pub min_samples_leaf: usize,
99    #[cfg_attr(feature = "serde", serde(default))]
100    /// The minimum number of samples required to split an internal node.
101    pub min_samples_split: usize,
102    #[cfg_attr(feature = "serde", serde(default))]
103    /// Controls the randomness of the estimator
104    pub seed: Option<u64>,
105}
106
107/// Decision Tree
108#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
109#[derive(Debug)]
110pub struct DecisionTreeClassifier<
111    TX: Number + PartialOrd,
112    TY: Number + Ord,
113    X: Array2<TX>,
114    Y: Array1<TY>,
115> {
116    nodes: Vec<Node>,
117    parameters: Option<DecisionTreeClassifierParameters>,
118    num_classes: usize,
119    classes: Vec<TY>,
120    depth: u16,
121    num_features: usize,
122    _phantom_tx: PhantomData<TX>,
123    _phantom_x: PhantomData<X>,
124    _phantom_y: PhantomData<Y>,
125}
126
127impl<TX: Number + PartialOrd, TY: Number + Ord, X: Array2<TX>, Y: Array1<TY>>
128    DecisionTreeClassifier<TX, TY, X, Y>
129{
130    /// Get nodes, return a shared reference
131    fn nodes(&self) -> &Vec<Node> {
132        self.nodes.as_ref()
133    }
134    /// Get parameters, return a shared reference
135    fn parameters(&self) -> &DecisionTreeClassifierParameters {
136        self.parameters.as_ref().unwrap()
137    }
138    /// get classes vector, return a shared reference
139    fn classes(&self) -> &Vec<TY> {
140        self.classes.as_ref()
141    }
142    /// Get depth of tree
143    pub fn depth(&self) -> u16 {
144        self.depth
145    }
146}
147
148/// The function to measure the quality of a split.
149#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
150#[derive(Debug, Clone, Default)]
151pub enum SplitCriterion {
152    /// [Gini index](../decision_tree_classifier/index.html)
153    #[default]
154    Gini,
155    /// [Entropy](../decision_tree_classifier/index.html)
156    Entropy,
157    /// [Classification error](../decision_tree_classifier/index.html)
158    ClassificationError,
159}
160
161#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
162#[derive(Debug, Clone)]
163struct Node {
164    output: usize,
165    n_node_samples: usize,
166    split_feature: usize,
167    split_value: Option<f64>,
168    split_score: Option<f64>,
169    true_child: Option<usize>,
170    false_child: Option<usize>,
171    impurity: Option<f64>,
172}
173
174impl<TX: Number + PartialOrd, TY: Number + Ord, X: Array2<TX>, Y: Array1<TY>> PartialEq
175    for DecisionTreeClassifier<TX, TY, X, Y>
176{
177    fn eq(&self, other: &Self) -> bool {
178        if self.depth != other.depth
179            || self.num_classes != other.num_classes
180            || self.nodes().len() != other.nodes().len()
181        {
182            false
183        } else {
184            self.classes()
185                .iter()
186                .zip(other.classes().iter())
187                .all(|(a, b)| a == b)
188                && self
189                    .nodes()
190                    .iter()
191                    .zip(other.nodes().iter())
192                    .all(|(a, b)| a == b)
193        }
194    }
195}
196
197impl PartialEq for Node {
198    fn eq(&self, other: &Self) -> bool {
199        self.output == other.output
200            && self.split_feature == other.split_feature
201            && match (self.split_value, other.split_value) {
202                (Some(a), Some(b)) => (a - b).abs() < f64::EPSILON,
203                (None, None) => true,
204                _ => false,
205            }
206            && match (self.split_score, other.split_score) {
207                (Some(a), Some(b)) => (a - b).abs() < f64::EPSILON,
208                (None, None) => true,
209                _ => false,
210            }
211    }
212}
213
214impl DecisionTreeClassifierParameters {
215    /// Split criteria to use when building a tree.
216    pub fn with_criterion(mut self, criterion: SplitCriterion) -> Self {
217        self.criterion = criterion;
218        self
219    }
220    /// The maximum depth of the tree.
221    pub fn with_max_depth(mut self, max_depth: u16) -> Self {
222        self.max_depth = Some(max_depth);
223        self
224    }
225    /// The minimum number of samples required to be at a leaf node.
226    pub fn with_min_samples_leaf(mut self, min_samples_leaf: usize) -> Self {
227        self.min_samples_leaf = min_samples_leaf;
228        self
229    }
230    /// The minimum number of samples required to split an internal node.
231    pub fn with_min_samples_split(mut self, min_samples_split: usize) -> Self {
232        self.min_samples_split = min_samples_split;
233        self
234    }
235}
236
237impl Default for DecisionTreeClassifierParameters {
238    fn default() -> Self {
239        DecisionTreeClassifierParameters {
240            criterion: SplitCriterion::default(),
241            max_depth: Option::None,
242            min_samples_leaf: 1,
243            min_samples_split: 2,
244            seed: Option::None,
245        }
246    }
247}
248
249/// DecisionTreeClassifier grid search parameters
250#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
251#[derive(Debug, Clone)]
252pub struct DecisionTreeClassifierSearchParameters {
253    #[cfg_attr(feature = "serde", serde(default))]
254    /// Split criteria to use when building a tree. See [Decision Tree Classifier](../../tree/decision_tree_classifier/index.html)
255    pub criterion: Vec<SplitCriterion>,
256    #[cfg_attr(feature = "serde", serde(default))]
257    /// Tree max depth. See [Decision Tree Classifier](../../tree/decision_tree_classifier/index.html)
258    pub max_depth: Vec<Option<u16>>,
259    #[cfg_attr(feature = "serde", serde(default))]
260    /// The minimum number of samples required to be at a leaf node. See [Decision Tree Classifier](../../tree/decision_tree_classifier/index.html)
261    pub min_samples_leaf: Vec<usize>,
262    #[cfg_attr(feature = "serde", serde(default))]
263    /// The minimum number of samples required to split an internal node. See [Decision Tree Classifier](../../tree/decision_tree_classifier/index.html)
264    pub min_samples_split: Vec<usize>,
265    #[cfg_attr(feature = "serde", serde(default))]
266    /// Controls the randomness of the estimator
267    pub seed: Vec<Option<u64>>,
268}
269
270/// DecisionTreeClassifier grid search iterator
271pub struct DecisionTreeClassifierSearchParametersIterator {
272    decision_tree_classifier_search_parameters: DecisionTreeClassifierSearchParameters,
273    current_criterion: usize,
274    current_max_depth: usize,
275    current_min_samples_leaf: usize,
276    current_min_samples_split: usize,
277    current_seed: usize,
278}
279
280impl IntoIterator for DecisionTreeClassifierSearchParameters {
281    type Item = DecisionTreeClassifierParameters;
282    type IntoIter = DecisionTreeClassifierSearchParametersIterator;
283
284    fn into_iter(self) -> Self::IntoIter {
285        DecisionTreeClassifierSearchParametersIterator {
286            decision_tree_classifier_search_parameters: self,
287            current_criterion: 0,
288            current_max_depth: 0,
289            current_min_samples_leaf: 0,
290            current_min_samples_split: 0,
291            current_seed: 0,
292        }
293    }
294}
295
296impl Iterator for DecisionTreeClassifierSearchParametersIterator {
297    type Item = DecisionTreeClassifierParameters;
298
299    fn next(&mut self) -> Option<Self::Item> {
300        if self.current_criterion
301            == self
302                .decision_tree_classifier_search_parameters
303                .criterion
304                .len()
305            && self.current_max_depth
306                == self
307                    .decision_tree_classifier_search_parameters
308                    .max_depth
309                    .len()
310            && self.current_min_samples_leaf
311                == self
312                    .decision_tree_classifier_search_parameters
313                    .min_samples_leaf
314                    .len()
315            && self.current_min_samples_split
316                == self
317                    .decision_tree_classifier_search_parameters
318                    .min_samples_split
319                    .len()
320            && self.current_seed == self.decision_tree_classifier_search_parameters.seed.len()
321        {
322            return None;
323        }
324
325        let next = DecisionTreeClassifierParameters {
326            criterion: self.decision_tree_classifier_search_parameters.criterion
327                [self.current_criterion]
328                .clone(),
329            max_depth: self.decision_tree_classifier_search_parameters.max_depth
330                [self.current_max_depth],
331            min_samples_leaf: self
332                .decision_tree_classifier_search_parameters
333                .min_samples_leaf[self.current_min_samples_leaf],
334            min_samples_split: self
335                .decision_tree_classifier_search_parameters
336                .min_samples_split[self.current_min_samples_split],
337            seed: self.decision_tree_classifier_search_parameters.seed[self.current_seed],
338        };
339
340        if self.current_criterion + 1
341            < self
342                .decision_tree_classifier_search_parameters
343                .criterion
344                .len()
345        {
346            self.current_criterion += 1;
347        } else if self.current_max_depth + 1
348            < self
349                .decision_tree_classifier_search_parameters
350                .max_depth
351                .len()
352        {
353            self.current_criterion = 0;
354            self.current_max_depth += 1;
355        } else if self.current_min_samples_leaf + 1
356            < self
357                .decision_tree_classifier_search_parameters
358                .min_samples_leaf
359                .len()
360        {
361            self.current_criterion = 0;
362            self.current_max_depth = 0;
363            self.current_min_samples_leaf += 1;
364        } else if self.current_min_samples_split + 1
365            < self
366                .decision_tree_classifier_search_parameters
367                .min_samples_split
368                .len()
369        {
370            self.current_criterion = 0;
371            self.current_max_depth = 0;
372            self.current_min_samples_leaf = 0;
373            self.current_min_samples_split += 1;
374        } else if self.current_seed + 1 < self.decision_tree_classifier_search_parameters.seed.len()
375        {
376            self.current_criterion = 0;
377            self.current_max_depth = 0;
378            self.current_min_samples_leaf = 0;
379            self.current_min_samples_split = 0;
380            self.current_seed += 1;
381        } else {
382            self.current_criterion += 1;
383            self.current_max_depth += 1;
384            self.current_min_samples_leaf += 1;
385            self.current_min_samples_split += 1;
386            self.current_seed += 1;
387        }
388
389        Some(next)
390    }
391}
392
393impl Default for DecisionTreeClassifierSearchParameters {
394    fn default() -> Self {
395        let default_params = DecisionTreeClassifierParameters::default();
396
397        DecisionTreeClassifierSearchParameters {
398            criterion: vec![default_params.criterion],
399            max_depth: vec![default_params.max_depth],
400            min_samples_leaf: vec![default_params.min_samples_leaf],
401            min_samples_split: vec![default_params.min_samples_split],
402            seed: vec![default_params.seed],
403        }
404    }
405}
406
407impl Node {
408    fn new(output: usize, n_node_samples: usize) -> Self {
409        Node {
410            output,
411            n_node_samples,
412            split_feature: 0,
413            split_value: Option::None,
414            split_score: Option::None,
415            true_child: Option::None,
416            false_child: Option::None,
417            impurity: Option::None,
418        }
419    }
420}
421
422struct NodeVisitor<'a, TX: Number + PartialOrd, X: Array2<TX>> {
423    x: &'a X,
424    y: &'a [usize],
425    node: usize,
426    samples: Vec<usize>,
427    order: &'a [Vec<usize>],
428    true_child_output: usize,
429    false_child_output: usize,
430    level: u16,
431    phantom: PhantomData<&'a TX>,
432}
433
434fn impurity(criterion: &SplitCriterion, count: &[usize], n: usize) -> f64 {
435    let mut impurity = 0f64;
436
437    match criterion {
438        SplitCriterion::Gini => {
439            impurity = 1f64;
440            for count_i in count.iter() {
441                if *count_i > 0 {
442                    let p = *count_i as f64 / n as f64;
443                    impurity -= p * p;
444                }
445            }
446        }
447
448        SplitCriterion::Entropy => {
449            for count_i in count.iter() {
450                if *count_i > 0 {
451                    let p = *count_i as f64 / n as f64;
452                    impurity -= p * p.log2();
453                }
454            }
455        }
456        SplitCriterion::ClassificationError => {
457            for count_i in count.iter() {
458                if *count_i > 0 {
459                    impurity = impurity.max(*count_i as f64 / n as f64);
460                }
461            }
462            impurity = (1f64 - impurity).abs();
463        }
464    }
465
466    impurity
467}
468
469impl<'a, TX: Number + PartialOrd, X: Array2<TX>> NodeVisitor<'a, TX, X> {
470    fn new(
471        node_id: usize,
472        samples: Vec<usize>,
473        order: &'a [Vec<usize>],
474        x: &'a X,
475        y: &'a [usize],
476        level: u16,
477    ) -> Self {
478        NodeVisitor {
479            x,
480            y,
481            node: node_id,
482            samples,
483            order,
484            true_child_output: 0,
485            false_child_output: 0,
486            level,
487            phantom: PhantomData,
488        }
489    }
490}
491
492pub(crate) fn which_max(x: &[usize]) -> usize {
493    let mut m = x[0];
494    let mut which = 0;
495
496    for (i, x_i) in x.iter().enumerate().skip(1) {
497        if *x_i > m {
498            m = *x_i;
499            which = i;
500        }
501    }
502
503    which
504}
505
506impl<TX: Number + PartialOrd, TY: Number + Ord, X: Array2<TX>, Y: Array1<TY>>
507    SupervisedEstimator<X, Y, DecisionTreeClassifierParameters>
508    for DecisionTreeClassifier<TX, TY, X, Y>
509{
510    fn new() -> Self {
511        Self {
512            nodes: vec![],
513            parameters: Option::None,
514            num_classes: 0usize,
515            classes: vec![],
516            depth: 0u16,
517            num_features: 0usize,
518            _phantom_tx: PhantomData,
519            _phantom_x: PhantomData,
520            _phantom_y: PhantomData,
521        }
522    }
523
524    fn fit(x: &X, y: &Y, parameters: DecisionTreeClassifierParameters) -> Result<Self, Failed> {
525        DecisionTreeClassifier::fit(x, y, parameters)
526    }
527}
528
529impl<TX: Number + PartialOrd, TY: Number + Ord, X: Array2<TX>, Y: Array1<TY>> Predictor<X, Y>
530    for DecisionTreeClassifier<TX, TY, X, Y>
531{
532    fn predict(&self, x: &X) -> Result<Y, Failed> {
533        self.predict(x)
534    }
535}
536
537impl<TX: Number + PartialOrd, TY: Number + Ord, X: Array2<TX>, Y: Array1<TY>>
538    DecisionTreeClassifier<TX, TY, X, Y>
539{
540    /// Build a decision tree classifier from the training data.
541    /// * `x` - _NxM_ matrix with _N_ observations and _M_ features in each observation.
542    /// * `y` - the target class values
543    pub fn fit(
544        x: &X,
545        y: &Y,
546        parameters: DecisionTreeClassifierParameters,
547    ) -> Result<DecisionTreeClassifier<TX, TY, X, Y>, Failed> {
548        let (x_nrows, num_attributes) = x.shape();
549        if x_nrows != y.shape() {
550            return Err(Failed::fit("Size of x should equal size of y"));
551        }
552
553        let samples = vec![1; x_nrows];
554        DecisionTreeClassifier::fit_weak_learner(x, y, samples, num_attributes, parameters)
555    }
556
557    pub(crate) fn fit_weak_learner(
558        x: &X,
559        y: &Y,
560        samples: Vec<usize>,
561        mtry: usize,
562        parameters: DecisionTreeClassifierParameters,
563    ) -> Result<DecisionTreeClassifier<TX, TY, X, Y>, Failed> {
564        let y_ncols = y.shape();
565        let (_, num_attributes) = x.shape();
566        let classes = y.unique();
567        let k = classes.len();
568        if k < 2 {
569            return Err(Failed::fit(&format!(
570                "Incorrect number of classes: {k}. Should be >= 2."
571            )));
572        }
573
574        let mut rng = get_rng_impl(parameters.seed);
575        let mut yi: Vec<usize> = vec![0; y_ncols];
576
577        for (i, yi_i) in yi.iter_mut().enumerate().take(y_ncols) {
578            let yc = y.get(i);
579            *yi_i = classes.iter().position(|c| yc == c).unwrap();
580        }
581
582        let mut change_nodes: Vec<Node> = Vec::new();
583
584        let mut count = vec![0; k];
585        for i in 0..y_ncols {
586            count[yi[i]] += samples[i];
587        }
588
589        let root = Node::new(which_max(&count), y_ncols);
590        change_nodes.push(root);
591        let mut order: Vec<Vec<usize>> = Vec::new();
592
593        for i in 0..num_attributes {
594            let mut col_i: Vec<TX> = x.get_col(i).iterator(0).copied().collect();
595            order.push(col_i.argsort_mut());
596        }
597
598        let mut tree = DecisionTreeClassifier {
599            nodes: change_nodes,
600            parameters: Some(parameters),
601            num_classes: k,
602            classes,
603            depth: 0u16,
604            num_features: num_attributes,
605            _phantom_tx: PhantomData,
606            _phantom_x: PhantomData,
607            _phantom_y: PhantomData,
608        };
609
610        let mut visitor = NodeVisitor::<TX, X>::new(0, samples, &order, x, &yi, 1);
611
612        let mut visitor_queue: LinkedList<NodeVisitor<'_, TX, X>> = LinkedList::new();
613
614        if tree.find_best_cutoff(&mut visitor, mtry, &mut rng) {
615            visitor_queue.push_back(visitor);
616        }
617
618        while tree.depth() < tree.parameters().max_depth.unwrap_or(u16::MAX) {
619            match visitor_queue.pop_front() {
620                Some(node) => tree.split(node, mtry, &mut visitor_queue, &mut rng),
621                None => break,
622            };
623        }
624
625        Ok(tree)
626    }
627
628    /// Predict class value for `x`.
629    /// * `x` - _KxM_ data where _K_ is number of observations and _M_ is number of features.
630    pub fn predict(&self, x: &X) -> Result<Y, Failed> {
631        let mut result = Y::zeros(x.shape().0);
632
633        let (n, _) = x.shape();
634
635        for i in 0..n {
636            result.set(i, self.classes()[self.predict_for_row(x, i)]);
637        }
638
639        Ok(result)
640    }
641
642    pub(crate) fn predict_for_row(&self, x: &X, row: usize) -> usize {
643        let mut node_id = 0;
644        loop {
645            let node = &self.nodes()[node_id];
646            let Some(true_child) = node.true_child else {
647                return node.output;
648            };
649            let false_child = node.false_child.unwrap();
650            node_id = if x.get((row, node.split_feature)).to_f64().unwrap()
651                <= node.split_value.unwrap_or(f64::NAN)
652            {
653                true_child
654            } else {
655                false_child
656            };
657        }
658    }
659
660    fn find_best_cutoff(
661        &mut self,
662        visitor: &mut NodeVisitor<'_, TX, X>,
663        mtry: usize,
664        rng: &mut impl Rng,
665    ) -> bool {
666        let (n_rows, n_attr) = visitor.x.shape();
667
668        let mut label = None;
669        let mut is_pure = true;
670        for i in 0..n_rows {
671            if visitor.samples[i] > 0 {
672                match label {
673                    None => {
674                        label = Some(visitor.y[i]);
675                    }
676                    Some(current_label) => {
677                        if visitor.y[i] != current_label {
678                            is_pure = false;
679                            break;
680                        }
681                    }
682                }
683            }
684        }
685
686        let n = visitor.samples.iter().sum();
687        let mut count = vec![0; self.num_classes];
688        let mut false_count = vec![0; self.num_classes];
689        for i in 0..n_rows {
690            if visitor.samples[i] > 0 {
691                count[visitor.y[i]] += visitor.samples[i];
692            }
693        }
694
695        self.nodes[visitor.node].impurity = Some(impurity(&self.parameters().criterion, &count, n));
696
697        if is_pure {
698            return false;
699        }
700
701        if n <= self.parameters().min_samples_split {
702            return false;
703        }
704
705        let mut variables = (0..n_attr).collect::<Vec<_>>();
706
707        if mtry < n_attr {
708            variables.shuffle(rng);
709        }
710
711        for variable in variables.iter().take(mtry) {
712            self.find_best_split(visitor, n, &count, &mut false_count, *variable);
713        }
714
715        self.nodes()[visitor.node].split_score.is_some()
716    }
717
718    fn find_best_split(
719        &mut self,
720        visitor: &mut NodeVisitor<'_, TX, X>,
721        n: usize,
722        count: &[usize],
723        false_count: &mut [usize],
724        j: usize,
725    ) {
726        let mut true_count = vec![0; self.num_classes];
727        let mut prevx = Option::None;
728        let mut prevy = 0;
729
730        for i in visitor.order[j].iter() {
731            if visitor.samples[*i] > 0 {
732                let x_ij = *visitor.x.get((*i, j));
733
734                if prevx.is_none() || x_ij == prevx.unwrap() || visitor.y[*i] == prevy {
735                    prevx = Some(x_ij);
736                    prevy = visitor.y[*i];
737                    true_count[visitor.y[*i]] += visitor.samples[*i];
738                    continue;
739                }
740
741                let tc = true_count.iter().sum();
742                let fc = n - tc;
743
744                if tc < self.parameters().min_samples_leaf
745                    || fc < self.parameters().min_samples_leaf
746                {
747                    prevx = Some(x_ij);
748                    prevy = visitor.y[*i];
749                    true_count[visitor.y[*i]] += visitor.samples[*i];
750                    continue;
751                }
752
753                for l in 0..self.num_classes {
754                    false_count[l] = count[l] - true_count[l];
755                }
756
757                let true_label = which_max(&true_count);
758                let false_label = which_max(false_count);
759                let parent_impurity = self.nodes()[visitor.node].impurity.unwrap();
760                let gain = parent_impurity
761                    - tc as f64 / n as f64
762                        * impurity(&self.parameters().criterion, &true_count, tc)
763                    - fc as f64 / n as f64
764                        * impurity(&self.parameters().criterion, false_count, fc);
765
766                if self.nodes()[visitor.node].split_score.is_none()
767                    || gain > self.nodes()[visitor.node].split_score.unwrap()
768                {
769                    self.nodes[visitor.node].split_feature = j;
770                    self.nodes[visitor.node].split_value =
771                        Option::Some((x_ij + prevx.unwrap()).to_f64().unwrap() / 2f64);
772                    self.nodes[visitor.node].split_score = Option::Some(gain);
773
774                    visitor.true_child_output = true_label;
775                    visitor.false_child_output = false_label;
776                }
777
778                prevx = Some(x_ij);
779                prevy = visitor.y[*i];
780                true_count[visitor.y[*i]] += visitor.samples[*i];
781            }
782        }
783    }
784
785    fn split<'a>(
786        &mut self,
787        mut visitor: NodeVisitor<'a, TX, X>,
788        mtry: usize,
789        visitor_queue: &mut LinkedList<NodeVisitor<'a, TX, X>>,
790        rng: &mut impl Rng,
791    ) -> bool {
792        let (n, _) = visitor.x.shape();
793        let mut tc = 0;
794        let mut fc = 0;
795        let mut true_samples: Vec<usize> = vec![0; n];
796
797        for (i, true_sample) in true_samples.iter_mut().enumerate().take(n) {
798            if visitor.samples[i] > 0 {
799                if visitor
800                    .x
801                    .get((i, self.nodes()[visitor.node].split_feature))
802                    .to_f64()
803                    .unwrap()
804                    <= self.nodes()[visitor.node].split_value.unwrap_or(f64::NAN)
805                {
806                    *true_sample = visitor.samples[i];
807                    tc += *true_sample;
808                    visitor.samples[i] = 0;
809                } else {
810                    fc += visitor.samples[i];
811                }
812            }
813        }
814
815        if tc < self.parameters().min_samples_leaf || fc < self.parameters().min_samples_leaf {
816            self.nodes[visitor.node].split_feature = 0;
817            self.nodes[visitor.node].split_value = Option::None;
818            self.nodes[visitor.node].split_score = Option::None;
819
820            return false;
821        }
822
823        let true_child_idx = self.nodes().len();
824
825        self.nodes.push(Node::new(visitor.true_child_output, tc));
826        let false_child_idx = self.nodes().len();
827        self.nodes.push(Node::new(visitor.false_child_output, fc));
828        self.nodes[visitor.node].true_child = Some(true_child_idx);
829        self.nodes[visitor.node].false_child = Some(false_child_idx);
830
831        self.depth = u16::max(self.depth, visitor.level + 1);
832
833        let mut true_visitor = NodeVisitor::<TX, X>::new(
834            true_child_idx,
835            true_samples,
836            visitor.order,
837            visitor.x,
838            visitor.y,
839            visitor.level + 1,
840        );
841
842        if self.find_best_cutoff(&mut true_visitor, mtry, rng) {
843            visitor_queue.push_back(true_visitor);
844        }
845
846        let mut false_visitor = NodeVisitor::<TX, X>::new(
847            false_child_idx,
848            visitor.samples,
849            visitor.order,
850            visitor.x,
851            visitor.y,
852            visitor.level + 1,
853        );
854
855        if self.find_best_cutoff(&mut false_visitor, mtry, rng) {
856            visitor_queue.push_back(false_visitor);
857        }
858
859        true
860    }
861
862    /// Compute feature importances for the fitted tree.
863    pub fn compute_feature_importances(&self, normalize: bool) -> Vec<f64> {
864        let mut importances = vec![0f64; self.num_features];
865
866        for node in self.nodes().iter() {
867            if node.true_child.is_none() && node.false_child.is_none() {
868                continue;
869            }
870            let left = &self.nodes()[node.true_child.unwrap()];
871            let right = &self.nodes()[node.false_child.unwrap()];
872
873            importances[node.split_feature] += node.n_node_samples as f64 * node.impurity.unwrap()
874                - left.n_node_samples as f64 * left.impurity.unwrap()
875                - right.n_node_samples as f64 * right.impurity.unwrap();
876        }
877        for item in importances.iter_mut() {
878            *item /= self.nodes()[0].n_node_samples as f64;
879        }
880        if normalize {
881            let sum = importances.iter().sum::<f64>();
882            for importance in importances.iter_mut() {
883                *importance /= sum;
884            }
885        }
886        importances
887    }
888
889    /// Predict class probabilities for the input samples.
890    ///
891    /// # Arguments
892    ///
893    /// * `x` - The input samples as a matrix where each row is a sample and each column is a feature.
894    ///
895    /// # Returns
896    ///
897    /// A `Result` containing a `DenseMatrix<f64>` where each row corresponds to a sample and each column
898    /// corresponds to a class. The values represent the probability of the sample belonging to each class.
899    ///
900    /// # Errors
901    ///
902    /// Returns an error if at least one row prediction process fails.
903    pub fn predict_proba(&self, x: &X) -> Result<DenseMatrix<f64>, Failed> {
904        let (n_samples, _) = x.shape();
905        let n_classes = self.classes().len();
906        let mut result = DenseMatrix::<f64>::zeros(n_samples, n_classes);
907
908        for i in 0..n_samples {
909            let probs = self.predict_proba_for_row(x, i)?;
910            for (j, &prob) in probs.iter().enumerate() {
911                result.set((i, j), prob);
912            }
913        }
914
915        Ok(result)
916    }
917
918    /// Predict class probabilities for a single input sample.
919    ///
920    /// # Arguments
921    ///
922    /// * `x` - The input matrix containing all samples.
923    /// * `row` - The index of the row in `x` for which to predict probabilities.
924    ///
925    /// # Returns
926    ///
927    /// A vector of probabilities, one for each class, representing the probability
928    /// of the input sample belonging to each class.
929    fn predict_proba_for_row(&self, x: &X, row: usize) -> Result<Vec<f64>, Failed> {
930        let mut node = 0;
931
932        while let Some(current_node) = self.nodes().get(node) {
933            if current_node.true_child.is_none() && current_node.false_child.is_none() {
934                // Leaf node reached
935                let mut probs = vec![0.0; self.classes().len()];
936                probs[current_node.output] = 1.0;
937                return Ok(probs);
938            }
939
940            let split_feature = current_node.split_feature;
941            let split_value = current_node.split_value.unwrap_or(f64::NAN);
942
943            if x.get((row, split_feature)).to_f64().unwrap() <= split_value {
944                node = current_node.true_child.unwrap();
945            } else {
946                node = current_node.false_child.unwrap();
947            }
948        }
949
950        // This should never happen if the tree is properly constructed
951        Err(Failed::predict("Nodes iteration did not reach leaf"))
952    }
953}
954
955#[cfg(test)]
956mod tests {
957    use super::*;
958    use crate::linalg::basic::arrays::Array;
959    use crate::linalg::basic::matrix::DenseMatrix;
960
961    #[test]
962    fn search_parameters() {
963        let parameters = DecisionTreeClassifierSearchParameters {
964            max_depth: vec![Some(10), Some(100)],
965            min_samples_split: vec![1, 2],
966            ..Default::default()
967        };
968        let mut iter = parameters.into_iter();
969        let next = iter.next().unwrap();
970        assert_eq!(next.max_depth, Some(10));
971        assert_eq!(next.min_samples_split, 1);
972        let next = iter.next().unwrap();
973        assert_eq!(next.max_depth, Some(100));
974        assert_eq!(next.min_samples_split, 1);
975        let next = iter.next().unwrap();
976        assert_eq!(next.max_depth, Some(10));
977        assert_eq!(next.min_samples_split, 2);
978        let next = iter.next().unwrap();
979        assert_eq!(next.max_depth, Some(100));
980        assert_eq!(next.min_samples_split, 2);
981        assert!(iter.next().is_none());
982    }
983
984    #[cfg_attr(
985        all(target_arch = "wasm32", not(target_os = "wasi")),
986        wasm_bindgen_test::wasm_bindgen_test
987    )]
988    #[test]
989    fn gini_impurity() {
990        assert!((impurity(&SplitCriterion::Gini, &[7, 3], 10) - 0.42).abs() < f64::EPSILON);
991        assert!(
992            (impurity(&SplitCriterion::Entropy, &[7, 3], 10) - 0.8812908992306927).abs()
993                < f64::EPSILON
994        );
995        assert!(
996            (impurity(&SplitCriterion::ClassificationError, &[7, 3], 10) - 0.3).abs()
997                < f64::EPSILON
998        );
999    }
1000
1001    #[cfg_attr(
1002        all(target_arch = "wasm32", not(target_os = "wasi")),
1003        wasm_bindgen_test::wasm_bindgen_test
1004    )]
1005    #[test]
1006    fn test_predict_proba() {
1007        let x: DenseMatrix<f64> = DenseMatrix::from_2d_array(&[
1008            &[5.1, 3.5, 1.4, 0.2],
1009            &[4.9, 3.0, 1.4, 0.2],
1010            &[4.7, 3.2, 1.3, 0.2],
1011            &[4.6, 3.1, 1.5, 0.2],
1012            &[5.0, 3.6, 1.4, 0.2],
1013            &[7.0, 3.2, 4.7, 1.4],
1014            &[6.4, 3.2, 4.5, 1.5],
1015            &[6.9, 3.1, 4.9, 1.5],
1016            &[5.5, 2.3, 4.0, 1.3],
1017            &[6.5, 2.8, 4.6, 1.5],
1018        ])
1019        .unwrap();
1020        let y: Vec<usize> = vec![0, 0, 0, 0, 0, 1, 1, 1, 1, 1];
1021
1022        let tree = DecisionTreeClassifier::fit(&x, &y, Default::default()).unwrap();
1023        let probabilities = tree.predict_proba(&x).unwrap();
1024
1025        assert_eq!(probabilities.shape(), (10, 2));
1026
1027        for row in 0..10 {
1028            let row_sum: f64 = probabilities.get_row(row).sum();
1029            assert!(
1030                (row_sum - 1.0).abs() < 1e-6,
1031                "Row probabilities should sum to 1"
1032            );
1033        }
1034
1035        // Check if the first 5 samples have higher probability for class 0
1036        for i in 0..5 {
1037            assert!(probabilities.get((i, 0)) > probabilities.get((i, 1)));
1038        }
1039
1040        // Check if the last 5 samples have higher probability for class 1
1041        for i in 5..10 {
1042            assert!(probabilities.get((i, 1)) > probabilities.get((i, 0)));
1043        }
1044    }
1045
1046    #[cfg_attr(
1047        all(target_arch = "wasm32", not(target_os = "wasi")),
1048        wasm_bindgen_test::wasm_bindgen_test
1049    )]
1050    #[test]
1051    #[cfg(feature = "datasets")]
1052    fn fit_predict_iris() {
1053        let x: DenseMatrix<f64> = DenseMatrix::from_2d_array(&[
1054            &[5.1, 3.5, 1.4, 0.2],
1055            &[4.9, 3.0, 1.4, 0.2],
1056            &[4.7, 3.2, 1.3, 0.2],
1057            &[4.6, 3.1, 1.5, 0.2],
1058            &[5.0, 3.6, 1.4, 0.2],
1059            &[5.4, 3.9, 1.7, 0.4],
1060            &[4.6, 3.4, 1.4, 0.3],
1061            &[5.0, 3.4, 1.5, 0.2],
1062            &[4.4, 2.9, 1.4, 0.2],
1063            &[4.9, 3.1, 1.5, 0.1],
1064            &[7.0, 3.2, 4.7, 1.4],
1065            &[6.4, 3.2, 4.5, 1.5],
1066            &[6.9, 3.1, 4.9, 1.5],
1067            &[5.5, 2.3, 4.0, 1.3],
1068            &[6.5, 2.8, 4.6, 1.5],
1069            &[5.7, 2.8, 4.5, 1.3],
1070            &[6.3, 3.3, 4.7, 1.6],
1071            &[4.9, 2.4, 3.3, 1.0],
1072            &[6.6, 2.9, 4.6, 1.3],
1073            &[5.2, 2.7, 3.9, 1.4],
1074        ])
1075        .unwrap();
1076        let y: Vec<u32> = vec![0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1];
1077
1078        assert_eq!(
1079            y,
1080            DecisionTreeClassifier::fit(&x, &y, Default::default())
1081                .and_then(|t| t.predict(&x))
1082                .unwrap()
1083        );
1084
1085        println!(
1086            "{:?}",
1087            //3,
1088            DecisionTreeClassifier::fit(
1089                &x,
1090                &y,
1091                DecisionTreeClassifierParameters {
1092                    criterion: SplitCriterion::Entropy,
1093                    max_depth: Some(3),
1094                    min_samples_leaf: 1,
1095                    min_samples_split: 2,
1096                    seed: Option::None
1097                }
1098            )
1099            .unwrap()
1100            .depth
1101        );
1102    }
1103
1104    #[test]
1105    fn test_random_matrix_with_wrong_rownum() {
1106        let x_rand: DenseMatrix<f64> = DenseMatrix::<f64>::rand(21, 200);
1107
1108        let y: Vec<u32> = vec![0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1];
1109
1110        let fail = DecisionTreeClassifier::fit(&x_rand, &y, Default::default());
1111
1112        assert!(fail.is_err());
1113    }
1114
1115    #[cfg_attr(
1116        all(target_arch = "wasm32", not(target_os = "wasi")),
1117        wasm_bindgen_test::wasm_bindgen_test
1118    )]
1119    #[test]
1120    fn fit_predict_baloons() {
1121        let x: DenseMatrix<f64> = DenseMatrix::from_2d_array(&[
1122            &[1., 1., 1., 0.],
1123            &[1., 1., 1., 0.],
1124            &[1., 1., 1., 1.],
1125            &[1., 1., 0., 0.],
1126            &[1., 1., 0., 1.],
1127            &[1., 0., 1., 0.],
1128            &[1., 0., 1., 0.],
1129            &[1., 0., 1., 1.],
1130            &[1., 0., 0., 0.],
1131            &[1., 0., 0., 1.],
1132            &[0., 1., 1., 0.],
1133            &[0., 1., 1., 0.],
1134            &[0., 1., 1., 1.],
1135            &[0., 1., 0., 0.],
1136            &[0., 1., 0., 1.],
1137            &[0., 0., 1., 0.],
1138            &[0., 0., 1., 0.],
1139            &[0., 0., 1., 1.],
1140            &[0., 0., 0., 0.],
1141            &[0., 0., 0., 1.],
1142        ])
1143        .unwrap();
1144        let y: Vec<u32> = vec![1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0];
1145
1146        assert_eq!(
1147            y,
1148            DecisionTreeClassifier::fit(&x, &y, Default::default())
1149                .and_then(|t| t.predict(&x))
1150                .unwrap()
1151        );
1152    }
1153
1154    #[test]
1155    fn test_compute_feature_importances() {
1156        let x: DenseMatrix<f64> = DenseMatrix::from_2d_array(&[
1157            &[1., 1., 1., 0.],
1158            &[1., 1., 1., 0.],
1159            &[1., 1., 1., 1.],
1160            &[1., 1., 0., 0.],
1161            &[1., 1., 0., 1.],
1162            &[1., 0., 1., 0.],
1163            &[1., 0., 1., 0.],
1164            &[1., 0., 1., 1.],
1165            &[1., 0., 0., 0.],
1166            &[1., 0., 0., 1.],
1167            &[0., 1., 1., 0.],
1168            &[0., 1., 1., 0.],
1169            &[0., 1., 1., 1.],
1170            &[0., 1., 0., 0.],
1171            &[0., 1., 0., 1.],
1172            &[0., 0., 1., 0.],
1173            &[0., 0., 1., 0.],
1174            &[0., 0., 1., 1.],
1175            &[0., 0., 0., 0.],
1176            &[0., 0., 0., 1.],
1177        ])
1178        .unwrap();
1179        let y: Vec<u32> = vec![1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0];
1180        let tree = DecisionTreeClassifier::fit(&x, &y, Default::default()).unwrap();
1181        assert_eq!(
1182            tree.compute_feature_importances(false),
1183            vec![0., 0., 0.21333333333333332, 0.26666666666666666]
1184        );
1185        assert_eq!(
1186            tree.compute_feature_importances(true),
1187            vec![0., 0., 0.4444444444444444, 0.5555555555555556]
1188        );
1189    }
1190
1191    #[cfg_attr(
1192        all(target_arch = "wasm32", not(target_os = "wasi")),
1193        wasm_bindgen_test::wasm_bindgen_test
1194    )]
1195    #[test]
1196    #[cfg(feature = "serde")]
1197    fn serde() {
1198        let x = DenseMatrix::from_2d_array(&[
1199            &[1., 1., 1., 0.],
1200            &[1., 1., 1., 0.],
1201            &[1., 1., 1., 1.],
1202            &[1., 1., 0., 0.],
1203            &[1., 1., 0., 1.],
1204            &[1., 0., 1., 0.],
1205            &[1., 0., 1., 0.],
1206            &[1., 0., 1., 1.],
1207            &[1., 0., 0., 0.],
1208            &[1., 0., 0., 1.],
1209            &[0., 1., 1., 0.],
1210            &[0., 1., 1., 0.],
1211            &[0., 1., 1., 1.],
1212            &[0., 1., 0., 0.],
1213            &[0., 1., 0., 1.],
1214            &[0., 0., 1., 0.],
1215            &[0., 0., 1., 0.],
1216            &[0., 0., 1., 1.],
1217            &[0., 0., 0., 0.],
1218            &[0., 0., 0., 1.],
1219        ])
1220        .unwrap();
1221        let y = vec![1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0];
1222
1223        let tree = DecisionTreeClassifier::fit(&x, &y, Default::default()).unwrap();
1224
1225        let deserialized_tree: DecisionTreeClassifier<f64, i64, DenseMatrix<f64>, Vec<i64>> =
1226            postcard::from_bytes(&postcard::to_allocvec(&tree).unwrap()).unwrap();
1227
1228        assert_eq!(tree, deserialized_tree);
1229    }
1230}