1use 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)]
88pub struct DecisionTreeClassifierParameters {
90 #[cfg_attr(feature = "serde", serde(default))]
91 pub criterion: SplitCriterion,
93 #[cfg_attr(feature = "serde", serde(default))]
94 pub max_depth: Option<u16>,
96 #[cfg_attr(feature = "serde", serde(default))]
97 pub min_samples_leaf: usize,
99 #[cfg_attr(feature = "serde", serde(default))]
100 pub min_samples_split: usize,
102 #[cfg_attr(feature = "serde", serde(default))]
103 pub seed: Option<u64>,
105}
106
107#[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 fn nodes(&self) -> &Vec<Node> {
132 self.nodes.as_ref()
133 }
134 fn parameters(&self) -> &DecisionTreeClassifierParameters {
136 self.parameters.as_ref().unwrap()
137 }
138 fn classes(&self) -> &Vec<TY> {
140 self.classes.as_ref()
141 }
142 pub fn depth(&self) -> u16 {
144 self.depth
145 }
146}
147
148#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
150#[derive(Debug, Clone, Default)]
151pub enum SplitCriterion {
152 #[default]
154 Gini,
155 Entropy,
157 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 pub fn with_criterion(mut self, criterion: SplitCriterion) -> Self {
217 self.criterion = criterion;
218 self
219 }
220 pub fn with_max_depth(mut self, max_depth: u16) -> Self {
222 self.max_depth = Some(max_depth);
223 self
224 }
225 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 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#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
251#[derive(Debug, Clone)]
252pub struct DecisionTreeClassifierSearchParameters {
253 #[cfg_attr(feature = "serde", serde(default))]
254 pub criterion: Vec<SplitCriterion>,
256 #[cfg_attr(feature = "serde", serde(default))]
257 pub max_depth: Vec<Option<u16>>,
259 #[cfg_attr(feature = "serde", serde(default))]
260 pub min_samples_leaf: Vec<usize>,
262 #[cfg_attr(feature = "serde", serde(default))]
263 pub min_samples_split: Vec<usize>,
265 #[cfg_attr(feature = "serde", serde(default))]
266 pub seed: Vec<Option<u64>>,
268}
269
270pub 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 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 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 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 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 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 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 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 for i in 0..5 {
1037 assert!(probabilities.get((i, 0)) > probabilities.get((i, 1)));
1038 }
1039
1040 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 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}