Skip to content

Commit 1abc88f

Browse files
oglegorelf
andauthored
feat: add symmetric mean absolute percentage error (sMAPE) (#437)
* feat: add smape metric * refactor: update sMAPE to Adjusted version and add multi target test Updated sMAPE formula to Adjusted (Makridakis, 1993) version Changed output scaling to [0, 200] Added test_symmetric_mean_absolute_percentage_error_multi_target * refactor: use F::epsilon instead of f64 cast Co-authored-by: Rémi Lafage <remi.lafage@onera.fr> --------- Co-authored-by: Rémi Lafage <remi.lafage@onera.fr>
1 parent 12c6c73 commit 1abc88f

1 file changed

Lines changed: 78 additions & 2 deletions

File tree

src/metrics_regression.rs

Lines changed: 78 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use crate::{
88
Float,
99
};
1010
use ndarray::prelude::*;
11-
use ndarray::Data;
11+
use ndarray::{Data, Zip};
1212
use std::ops::{Div, Sub};
1313

1414
/// Regression metrices trait for single targets.
@@ -80,6 +80,29 @@ pub trait SingleTargetRegression<F: Float, T: AsSingleTargets<Elem = F>>:
8080
.ok_or(Error::NotEnoughSamples)
8181
}
8282

83+
/// Symmetric mean absolute percentage error between two continuous variables.
84+
/// This implementation follows the Adjusted sMAPE (Makridakis, 1993)
85+
/// sMAPE = (200 / n) * SUM(abs(y_hat - y) / (abs(y) + abs(y_hat)))
86+
fn symmetric_mean_absolute_percentage_error(&self, compare_to: &T) -> Result<F> {
87+
let y = self.as_single_targets();
88+
let y_hat = compare_to.as_single_targets();
89+
if y.is_empty() {
90+
return Err(Error::NotEnoughSamples);
91+
}
92+
let sum: F = Zip::from(&y)
93+
.and(&y_hat)
94+
.fold(F::cast(0.0), |acc, &yi, &yhi| {
95+
let num = (yhi - yi).abs();
96+
let den = yi.abs() + yhi.abs();
97+
if den <= F::epsilon() {
98+
acc
99+
} else {
100+
acc + (num / den)
101+
}
102+
});
103+
Ok((F::cast(200.0) / F::cast(y.len())) * sum)
104+
}
105+
83106
/// R squared coefficient, is the proportion of the variance in the dependent variable that is
84107
/// predictable from the independent variable
85108
// r2 = 1 - sum((pred_i - y_i)^2)/sum((mean_y - y_i)^2)
@@ -193,6 +216,17 @@ pub trait MultiTargetRegression<F: Float, T: AsMultiTargets<Elem = F>>:
193216
.collect()
194217
}
195218

219+
/// Symmetric mean absolute percentage error between two continuous variables
220+
/// This implementation follows the Adjusted sMAPE (Makridakis, 1993)
221+
/// sMAPE = (200 / n) * SUM(abs(y_hat - y) / (abs(y) + abs(y_hat)))
222+
fn symmetric_mean_absolute_percentage_error(&self, other: &T) -> Result<Array1<F>> {
223+
self.as_multi_targets()
224+
.axis_iter(Axis(1))
225+
.zip(other.as_multi_targets().axis_iter(Axis(1)))
226+
.map(|(a, b)| a.symmetric_mean_absolute_percentage_error(&b))
227+
.collect()
228+
}
229+
196230
/// R squared coefficient, is the proportion of the variance in the dependent variable that is
197231
/// predictable from the independent variable
198232
fn r2(&self, other: &T) -> Result<Array1<F>> {
@@ -225,7 +259,7 @@ impl<F: Float, T: AsMultiTargets<Elem = F>, T2: AsMultiTargets<Elem = F>, D: Dat
225259

226260
#[cfg(test)]
227261
mod tests {
228-
use super::SingleTargetRegression;
262+
use super::{MultiTargetRegression, SingleTargetRegression};
229263
use crate::dataset::DatasetBase;
230264
use approx::assert_abs_diff_eq;
231265
use ndarray::prelude::*;
@@ -242,6 +276,10 @@ mod tests {
242276
assert_abs_diff_eq!(a.r2(&a).unwrap(), 1.0f32);
243277
assert_abs_diff_eq!(a.explained_variance(&a).unwrap(), 1.0f32);
244278
assert_abs_diff_eq!(a.mean_absolute_percentage_error(&a).unwrap(), 0.0f32);
279+
assert_abs_diff_eq!(
280+
a.symmetric_mean_absolute_percentage_error(&a).unwrap(),
281+
0.0f32
282+
);
245283
}
246284

247285
#[test]
@@ -281,6 +319,18 @@ mod tests {
281319
);
282320
}
283321

322+
#[test]
323+
fn test_symmetric_mean_absolute_percentage_error() {
324+
let a = array![0.5, 0.1, 0.2, 0.3, 0.4];
325+
let b = array![0.1, 0.2, 0.3, 0.4, 0.5];
326+
327+
assert_abs_diff_eq!(
328+
a.symmetric_mean_absolute_percentage_error(&b).unwrap(),
329+
58.15873014693111,
330+
epsilon = 1e-5
331+
);
332+
}
333+
284334
#[test]
285335
fn test_max_error_for_single_targets() {
286336
let records = array![[0.0, 0.0], [0.1, 0.1], [0.2, 0.2], [0.3, 0.3], [0.4, 0.4]];
@@ -339,6 +389,23 @@ mod tests {
339389
assert_abs_diff_eq!(pct_err_from_arr1, pct_err_from_ds);
340390
}
341391

392+
#[test]
393+
fn test_symmetric_mean_absolute_percentage_error_for_single_targets() {
394+
let records = array![[0.0, 0.0], [0.1, 0.1], [0.2, 0.2], [0.3, 0.3], [0.4, 0.4]];
395+
let targets = array![0.0, 0.1, 0.2, 0.3, 0.4];
396+
let st_dataset: DatasetBase<_, _> = (records.view(), targets).into();
397+
let prediction = array![0.1, 0.3, 0.2, 0.5, 0.7];
398+
let err_from_arr = prediction
399+
.symmetric_mean_absolute_percentage_error(st_dataset.targets())
400+
.unwrap();
401+
let prediction_ds: DatasetBase<_, _> = (records.view(), prediction).into();
402+
let err_from_ds = prediction_ds
403+
.symmetric_mean_absolute_percentage_error(&st_dataset)
404+
.unwrap();
405+
assert_abs_diff_eq!(err_from_arr, 80.90909086184916, epsilon = 1e-5);
406+
assert_abs_diff_eq!(err_from_arr, err_from_ds);
407+
}
408+
342409
#[test]
343410
fn test_mean_squared_log_error_for_single_targets() {
344411
let records = array![[0.0, 0.0], [0.1, 0.1], [0.2, 0.2], [0.3, 0.3], [0.4, 0.4]];
@@ -410,4 +477,13 @@ mod tests {
410477
assert_abs_diff_eq!(abs_err_from_arr1, 0.8, epsilon = 1e-5);
411478
assert_abs_diff_eq!(abs_err_from_arr1, abs_err_from_ds);
412479
}
480+
481+
#[test]
482+
fn test_symmetric_mean_absolute_percentage_error_multi_target() {
483+
let a = array![[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]];
484+
let b = array![[1.1, 1.9], [2.9, 4.1], [5.1, 5.9]];
485+
let err = a.symmetric_mean_absolute_percentage_error(&b).unwrap();
486+
let expected = array![4.964612684, 3.092671067];
487+
assert_abs_diff_eq!(err, expected, epsilon = 1e-5);
488+
}
413489
}

0 commit comments

Comments
 (0)