Skip to main content

contree/common/
loss.rs

1//! The cost of a leaf, and whether threshold skipping may assume it behaves
2//! like misclassification.
3
4use crate::common::{classification_error, Cost};
5
6/// What a custom loss receives for a leaf.
7#[derive(Copy, Clone, Debug, PartialEq, Eq)]
8pub enum LossInput {
9    /// The number of instances of each class in the leaf.
10    ClassCounts,
11    /// The ids of the leaf's instances (their row in the training data).
12    Indices,
13}
14
15/// A leaf, as a custom loss sees it.
16#[derive(Copy, Clone, Debug)]
17pub enum Leaf<'a> {
18    /// The number of instances of each class.
19    ClassCounts(&'a [usize]),
20    /// The ids of the instances.
21    Indices(&'a [usize]),
22}
23
24/// A caller's leaf loss: `(cost, class predicted)`. The cost of a tree is the
25/// sum of the costs of its leaves, and a cost must be non-negative.
26pub trait LeafLoss: Send + Sync {
27    fn cost(&self, leaf: Leaf<'_>) -> (Cost, usize);
28}
29
30/// The loss a search minimises.
31#[derive(Default)]
32pub enum Loss {
33    /// Misclassification: the number of instances not of the majority
34    /// class.
35    #[default]
36    Misclassification,
37    /// A caller's loss, with the input it asks for.
38    Custom {
39        function: Box<dyn LeafLoss>,
40        input: LossInput,
41    },
42}
43
44impl Loss {
45    /// A caller's loss.
46    pub fn custom(function: Box<dyn LeafLoss>, input: LossInput) -> Self {
47        Loss::Custom { function, input }
48    }
49
50    /// Whether this is the built-in misclassification.
51    pub fn is_misclassification(&self) -> bool {
52        matches!(self, Loss::Misclassification)
53    }
54
55    /// Whether the loss reads instance ids rather than class counts.
56    pub fn uses_indices(&self) -> bool {
57        matches!(
58            self,
59            Loss::Custom {
60                input: LossInput::Indices,
61                ..
62            }
63        )
64    }
65
66    /// The cost of a leaf with these class counts and instance ids.
67    /// `indices` is only called for a loss that reads them.
68    pub fn leaf(
69        &self,
70        class_counts: &[usize],
71        indices: impl FnOnce() -> Vec<usize>,
72    ) -> (Cost, usize) {
73        match self {
74            Loss::Misclassification => classification_error(class_counts),
75            Loss::Custom {
76                function,
77                input: LossInput::ClassCounts,
78            } => function.cost(Leaf::ClassCounts(class_counts)),
79            Loss::Custom {
80                function,
81                input: LossInput::Indices,
82            } => function.cost(Leaf::Indices(&indices())),
83        }
84    }
85}
86
87/// Whether the search skips thresholds: the interval pruner, and the skipping
88/// and early stops of the depth-2 solver's scan.
89///
90/// Skipping assumes what misclassification guarantees: moving `n` instances
91/// into a leaf never lowers its cost and raises it by at most `n`, and
92/// removing them never raises it and lowers it by at most `n`. A custom loss
93/// may not, so `Auto` turns skipping off for one. `On` forces it, and the
94/// depth-2 solver then counts the moves that break the assumption.
95///
96/// With a minimum support above 1 the interval pruner stays off whatever the
97/// setting, since its rules do not hold there for any loss; `On` then forces
98/// only the depth-2 solver's skipping, whose argument does not involve the
99/// support.
100#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
101pub enum Skipping {
102    /// On for misclassification, off for a custom loss.
103    #[default]
104    Auto,
105    /// On whatever the loss.
106    On,
107    /// Off.
108    Off,
109}
110
111impl Skipping {
112    /// Whether thresholds are skipped for this loss.
113    ///
114    /// The interval pruner adds its own condition, a minimum support of 1;
115    /// see `IntervalsPruner::is_sound`.
116    pub fn applies(self, loss: &Loss) -> bool {
117        match self {
118            Skipping::Auto => loss.is_misclassification(),
119            Skipping::On => true,
120            Skipping::Off => false,
121        }
122    }
123}