Skip to main content

radiate_gp/regression/
accuracy.rs

1use super::{DataSet, Loss};
2use crate::{Eval, EvalMut, Graph, GraphEvaluator, Op, Tree, ops::OpFloat};
3use std::fmt::Debug;
4
5#[derive(Clone, Default)]
6pub struct Accuracy<'a, F: OpFloat> {
7    name: Option<String>,
8    data_set: Option<&'a DataSet<F>>,
9    loss_fn: Option<Loss>,
10}
11
12impl<'a, F: OpFloat> Accuracy<'a, F> {
13    pub fn named(mut self, name: impl Into<String>) -> Self {
14        self.name = Some(name.into());
15        self
16    }
17
18    pub fn on(mut self, data_set: &'a DataSet<F>) -> Self {
19        self.data_set = Some(data_set);
20        self
21    }
22
23    pub fn loss(mut self, loss_fn: Loss) -> Self {
24        self.loss_fn = Some(loss_fn);
25        self
26    }
27
28    pub fn calc(&self, eval: &mut impl EvalMut<[F], Vec<F>>) -> AccuracyResult {
29        let data_set = self
30            .data_set
31            .expect("DataSet reference must be provided for accuracy calculation");
32        let loss_fn = self
33            .loss_fn
34            .expect("Loss function must be provided for accuracy calculation");
35
36        self.calc_internal(eval, data_set, loss_fn)
37    }
38
39    pub fn calc_internal(
40        &self,
41        eval: &mut impl EvalMut<[F], Vec<F>>,
42        data_set: &DataSet<F>,
43        loss_fn: Loss,
44    ) -> AccuracyResult {
45        let mut outputs = Vec::new();
46        let mut total_samples = F::ZERO;
47        let mut correct_predictions = F::ZERO;
48        let mut is_regression = true;
49
50        let mut mae = F::ZERO;
51        let mut mse = F::ZERO;
52        let mut min_output = F::MAX;
53        let mut max_output = F::MIN;
54        let mut ss_total = F::ZERO;
55        let mut ss_residual = F::ZERO;
56        let mut y_mean = F::ZERO;
57
58        let mut tp = F::ZERO;
59        let mut fp = F::ZERO;
60        let mut fn_ = F::ZERO;
61
62        let loss = loss_fn.calc(data_set, eval);
63
64        let total_values = data_set.len();
65        if total_values > 0 {
66            let mut sum = F::ZERO;
67            for row in data_set.iter() {
68                sum = sum + row.1[0];
69            }
70            y_mean = sum / F::from(total_values).unwrap();
71        }
72
73        for row in data_set.iter() {
74            let output = eval.eval_mut(row.0);
75            outputs.push(output.clone());
76
77            if output.len() == 1 {
78                is_regression = true;
79                let y_true = row.1[0];
80                let y_pred = output[0];
81
82                mae = mae + (y_true - y_pred).abs();
83                mse = mse + (y_true - y_pred).powi(2);
84                ss_residual = ss_residual + (y_true - y_pred).powi(2);
85                ss_total = ss_total + (y_true - y_mean).powi(2);
86
87                min_output = min_output.min(y_true);
88                max_output = max_output.max(y_true);
89                total_samples = total_samples + F::ONE;
90            } else {
91                is_regression = false;
92                if let Some((max_idx, _)) = output
93                    .iter()
94                    .enumerate()
95                    .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
96                {
97                    if let Some(target) = row.1.iter().position(|&x| x == F::ONE) {
98                        total_samples = total_samples + F::ONE;
99                        if max_idx == target {
100                            correct_predictions = correct_predictions + F::ONE;
101                            tp = tp + F::ONE;
102                        } else {
103                            fp = fp + F::ONE;
104                        }
105                    } else {
106                        fn_ = fn_ + F::ONE;
107                    }
108                }
109            }
110        }
111
112        // Compute final accuracy
113        let accuracy = if is_regression {
114            if total_samples > F::ZERO && (max_output - min_output) > F::ZERO {
115                F::ONE - (mae / total_samples) / (max_output - min_output)
116            } else {
117                F::ZERO
118            }
119        } else if total_samples > F::ZERO {
120            correct_predictions / total_samples
121        } else {
122            F::ZERO
123        };
124
125        // Compute classification metrics only if it's a classification task
126        let (precision, recall, f1_score) = if is_regression {
127            (F::ZERO, F::ZERO, F::ZERO) // Not applicable for regression
128        } else {
129            let precision = if tp + fp > F::ZERO {
130                tp / (tp + fp)
131            } else {
132                F::ZERO
133            };
134            let recall = if tp + fn_ > F::ZERO {
135                tp / (tp + fn_)
136            } else {
137                F::ZERO
138            };
139            let f1_score = if precision + recall > F::ZERO {
140                F::TWO * (precision * recall) / (precision + recall)
141            } else {
142                F::ZERO
143            };
144            (precision, recall, f1_score)
145        };
146
147        let rmse = if total_samples > F::ZERO {
148            (mse / total_samples).sqrt()
149        } else {
150            F::ZERO
151        };
152
153        // Compute R² score
154        let r_squared = if ss_total > F::ZERO {
155            F::ONE - (ss_residual / ss_total)
156        } else {
157            F::ZERO
158        };
159
160        AccuracyResult {
161            name: match &self.name {
162                Some(name) => name.clone(),
163                None => {
164                    if is_regression {
165                        "Regression Accuracy".to_string()
166                    } else {
167                        "Classification Accuracy".to_string()
168                    }
169                }
170            },
171            accuracy: accuracy.extract().unwrap_or(0.0),
172            precision: precision.extract().unwrap_or(0.0),
173            recall: recall.extract().unwrap_or(0.0),
174            f1_score: f1_score.extract().unwrap_or(0.0),
175            rmse: rmse.extract().unwrap_or(0.0),
176            r_squared: r_squared.extract().unwrap_or(0.0),
177            loss: loss.extract().unwrap_or(0.0),
178            loss_fn,
179            sample_count: data_set.len(),
180            is_regression,
181        }
182    }
183}
184
185pub struct AccuracyResult {
186    name: String,
187    accuracy: f32,
188    precision: f32, // Only for classification
189    recall: f32,    // Only for classification
190    f1_score: f32,  // Only for classification
191    rmse: f32,      // Only for regression
192    r_squared: f32, // Only for regression
193    sample_count: usize,
194    loss: f32,
195    loss_fn: Loss,
196    is_regression: bool,
197}
198
199impl AccuracyResult {
200    pub fn name(&self) -> &str {
201        &self.name
202    }
203
204    pub fn accuracy(&self) -> f32 {
205        self.accuracy
206    }
207
208    pub fn precision(&self) -> f32 {
209        self.precision
210    }
211
212    pub fn recall(&self) -> f32 {
213        self.recall
214    }
215
216    pub fn f1_score(&self) -> f32 {
217        self.f1_score
218    }
219
220    pub fn rmse(&self) -> f32 {
221        self.rmse
222    }
223
224    pub fn r_squared(&self) -> f32 {
225        self.r_squared
226    }
227
228    pub fn sample_count(&self) -> usize {
229        self.sample_count
230    }
231
232    pub fn loss(&self) -> f32 {
233        self.loss
234    }
235
236    pub fn loss_fn(&self) -> Loss {
237        self.loss_fn
238    }
239}
240
241impl Debug for AccuracyResult {
242    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
243        if self.is_regression {
244            write!(
245                f,
246                "{:?} {{\n\tN: {:?} \n\tAccuracy: {:.2}%\n\tR² Score: {:.5}\n\tRMSE: {:.5}\n\tLoss ({:?}): {:.5}\n}}",
247                self.name,
248                self.sample_count,
249                self.accuracy * 100.0,
250                self.r_squared,
251                self.rmse,
252                self.loss_fn,
253                self.loss
254            )
255        } else {
256            write!(
257                f,
258                "{:?} {{\n\tN: {:?} \n\tAccuracy: {:.2}%\n\tPrecision: {:.2}%\n\tRecall: {:.2}%\n\tF1 Score: {:.2}%\n\tLoss ({:?}): {:.5}\n}}",
259                self.name,
260                self.sample_count,
261                self.accuracy * 100.0,
262                self.precision * 100.0,
263                self.recall * 100.0,
264                self.f1_score * 100.0,
265                self.loss_fn,
266                self.loss
267            )
268        }
269    }
270}
271
272impl<T: OpFloat> Eval<Graph<Op<T>>, Option<AccuracyResult>> for Accuracy<'_, T> {
273    fn eval(&self, graph: &Graph<Op<T>>) -> Option<AccuracyResult> {
274        let mut evaluator = GraphEvaluator::new(graph);
275        Some(self.calc(&mut evaluator))
276    }
277}
278
279impl<T: OpFloat> Eval<Tree<Op<T>>, Option<AccuracyResult>> for Accuracy<'_, T> {
280    fn eval(&self, tree: &Tree<Op<T>>) -> Option<AccuracyResult> {
281        Some(self.calc(&mut tree.clone()))
282    }
283}
284
285impl<T: OpFloat> Eval<Vec<Tree<Op<T>>>, Option<AccuracyResult>> for Accuracy<'_, T> {
286    fn eval(&self, trees: &Vec<Tree<Op<T>>>) -> Option<AccuracyResult> {
287        let mut cloned_trees = trees.clone();
288        Some(self.calc(&mut cloned_trees))
289    }
290}