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::Statistics;
7use crate::data::DatasetError;
8use crate::tree::{Tree, TreeError};
9
10/// A fit that could not start.
11#[derive(Clone, Debug, PartialEq, Eq)]
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}
33
34impl fmt::Display for SearchError {
35    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
36        match self {
37            SearchError::EmptyDataset => write!(f, "the dataset has no instances"),
38            SearchError::NoFeatures => write!(f, "the dataset has no features"),
39            SearchError::UnpreparedDataset => write!(
40                f,
41                "the dataset has not been sorted and indexed; build it with \
42                 Dataset::from_rows or DataReader"
43            ),
44            SearchError::InvalidParameter { name, reason } => {
45                write!(f, "invalid {name}: {reason}")
46            }
47            SearchError::Data(err) => write!(f, "{err}"),
48            SearchError::Tree(err) => write!(f, "the search produced an invalid tree: {err}"),
49        }
50    }
51}
52
53impl std::error::Error for SearchError {}
54
55impl From<DatasetError> for SearchError {
56    fn from(err: DatasetError) -> Self {
57        SearchError::Data(err)
58    }
59}
60
61impl From<TreeError> for SearchError {
62    fn from(err: TreeError) -> Self {
63        SearchError::Tree(err)
64    }
65}
66
67/// Why the search stopped.
68///
69/// Only `Optimal` means the tree is proven best for the given depth and
70/// support; the others mean the search ran out of something first.
71#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
72#[serde(rename_all = "snake_case")]
73pub enum SearchStatus {
74    /// The search space was exhausted: no better tree exists.
75    Optimal,
76    /// The time limit ran out first.
77    TimeLimit,
78    /// The anytime search ran out of budget to widen with.
79    BudgetExhausted,
80    /// A tree within `max_gap` of the caller's error bound was found, and the
81    /// search stopped there rather than proving optimality.
82    ErrorBoundReached,
83}
84
85impl SearchStatus {
86    /// Whether the returned tree is proven optimal.
87    pub fn is_optimal(self) -> bool {
88        matches!(self, SearchStatus::Optimal)
89    }
90}
91
92impl fmt::Display for SearchStatus {
93    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
94        f.write_str(match self {
95            SearchStatus::Optimal => "optimal",
96            SearchStatus::TimeLimit => "time limit",
97            SearchStatus::BudgetExhausted => "budget exhausted",
98            SearchStatus::ErrorBoundReached => "error bound reached",
99        })
100    }
101}
102
103/// The result of a fit.
104#[derive(Clone, Debug)]
105pub struct FitOutcome {
106    /// The best tree found.
107    pub tree: Tree,
108    /// Search counters and the training error of `tree`.
109    pub statistics: Statistics,
110    /// Why the search stopped.
111    pub status: SearchStatus,
112}
113
114impl FitOutcome {
115    /// Training-set misclassifications of the returned tree.
116    pub fn error(&self) -> usize {
117        self.statistics.error
118    }
119}