Skip to main content

contree/common/
outcome.rs

1//! What a fit produced, why it stopped, and why it could not start.
2
3use serde::{Deserialize, Serialize};
4use std::fmt;
5
6use crate::common::{Cost, Statistics};
7use crate::data::DatasetError;
8use crate::tree::{Tree, TreeError};
9
10/// A fit that could not start, or whose loss failed.
11#[derive(Clone, Debug, PartialEq)]
12pub enum SearchError {
13    /// The dataset has no instances.
14    EmptyDataset,
15    /// The dataset has no feature columns.
16    NoFeatures,
17    /// `sort_features` and `compute_unique_feature_values` have not both run.
18    ///
19    /// The search compares unique-value indices, so an unprepared dataset
20    /// would silently produce a single leaf. Build
21    /// datasets through [`Dataset::from_rows`](crate::data::Dataset::from_rows)
22    /// or [`DataReader`](crate::reader::data_reader::DataReader), which both
23    /// prepare them.
24    UnpreparedDataset,
25    /// A parameter whose value cannot produce a meaningful search.
26    InvalidParameter { name: &'static str, reason: String },
27    /// The dataset itself is malformed.
28    Data(DatasetError),
29    /// The search produced a tree that does not hold the tree invariant. This
30    /// is a bug in the crate, not in the caller's input.
31    Tree(TreeError),
32    /// A custom loss returned a negative or NaN cost: the first one seen.
33    InvalidLeafCost { cost: Cost },
34}
35
36impl fmt::Display for SearchError {
37    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
38        match self {
39            SearchError::EmptyDataset => write!(f, "the dataset has no instances"),
40            SearchError::NoFeatures => write!(f, "the dataset has no features"),
41            SearchError::UnpreparedDataset => write!(
42                f,
43                "the dataset has not been sorted and indexed; build it with \
44                 Dataset::from_rows or DataReader"
45            ),
46            SearchError::InvalidParameter { name, reason } => {
47                write!(f, "invalid {name}: {reason}")
48            }
49            SearchError::Data(err) => write!(f, "{err}"),
50            SearchError::Tree(err) => write!(f, "the search produced an invalid tree: {err}"),
51            SearchError::InvalidLeafCost { cost } => write!(
52                f,
53                "the loss returned the leaf cost {cost}; costs must be non-negative numbers"
54            ),
55        }
56    }
57}
58
59impl std::error::Error for SearchError {}
60
61impl From<DatasetError> for SearchError {
62    fn from(err: DatasetError) -> Self {
63        SearchError::Data(err)
64    }
65}
66
67impl From<TreeError> for SearchError {
68    fn from(err: TreeError) -> Self {
69        SearchError::Tree(err)
70    }
71}
72
73/// Why the search stopped.
74///
75/// Only `Optimal` means the tree is proven best for the given depth and
76/// support; the others mean the search ran out of something first.
77#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
78#[serde(rename_all = "snake_case")]
79pub enum SearchStatus {
80    /// The search space was exhausted: no better tree exists.
81    Optimal,
82    /// The time limit ran out first.
83    TimeLimit,
84    /// The anytime search ran out of budget to widen with.
85    BudgetExhausted,
86    /// A tree within `max_gap` of the caller's error bound was found, and the
87    /// search stopped there rather than proving optimality.
88    ErrorBoundReached,
89    /// The search finished, but with threshold skipping forced on for a loss
90    /// that broke its assumption, so the tree may not be optimal.
91    NotProven,
92}
93
94impl SearchStatus {
95    /// Whether the returned tree is proven optimal.
96    pub fn is_optimal(self) -> bool {
97        matches!(self, SearchStatus::Optimal)
98    }
99}
100
101impl fmt::Display for SearchStatus {
102    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
103        f.write_str(match self {
104            SearchStatus::Optimal => "optimal",
105            SearchStatus::TimeLimit => "time limit",
106            SearchStatus::BudgetExhausted => "budget exhausted",
107            SearchStatus::ErrorBoundReached => "error bound reached",
108            SearchStatus::NotProven => "not proven",
109        })
110    }
111}
112
113/// The result of a fit.
114#[derive(Clone, Debug)]
115pub struct FitOutcome {
116    /// The best tree found.
117    pub tree: Tree,
118    /// Search counters and the training error of `tree`.
119    pub statistics: Statistics,
120    /// Why the search stopped.
121    pub status: SearchStatus,
122}
123
124impl FitOutcome {
125    /// Training-set misclassifications of the returned tree.
126    pub fn error(&self) -> Cost {
127        self.statistics.error
128    }
129}