Skip to main content

gamlss_transform/transforms/
asinh_scale.rs

1use crate::{TargetTransform, TransformError, median_sorted, validate_non_empty_finite};
2
3/// Signed inverse-hyperbolic-sine transform with a fitted robust scale.
4///
5/// `transform(y) = asinh(y / scale)`. Unlike a log transform, this is defined
6/// for negative, zero and positive finite targets while still compressing large
7/// magnitudes.
8#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
9pub struct AsinhScale;
10
11/// State for [`AsinhScale`].
12#[derive(Debug, Clone, Copy, PartialEq)]
13pub struct AsinhScaleState {
14    /// Positive scale used before `asinh`.
15    pub scale: f64,
16}
17
18impl TargetTransform for AsinhScale {
19    type State = AsinhScaleState;
20
21    fn fit(y: &[f64]) -> Result<Self::State, TransformError> {
22        validate_non_empty_finite(y)?;
23
24        let mut abs_values: Vec<_> = y.iter().map(|value| value.abs()).collect();
25        abs_values.sort_by(f64::total_cmp);
26        let scale = median_sorted(&abs_values)
27            .filter(|value| value.is_finite() && *value > 0.0)
28            .unwrap_or(1.0);
29
30        Ok(AsinhScaleState { scale })
31    }
32
33    #[inline(always)]
34    fn transform(state: &Self::State, y: f64) -> f64 {
35        (y / state.scale).asinh()
36    }
37
38    #[inline(always)]
39    fn inverse(state: &Self::State, value: f64) -> f64 {
40        value.sinh() * state.scale
41    }
42}
43
44#[cfg(test)]
45mod tests {
46    use approx::assert_relative_eq;
47
48    use crate::{AsinhScale, TargetTransform};
49
50    #[test]
51    fn round_trips_signed_values() {
52        let y = [-100.0, -1.0, 0.0, 2.0, 50.0];
53        let (state, transformed) = AsinhScale::fit_transform(&y).unwrap();
54        let restored = AsinhScale::inverse_slice(&state, &transformed).unwrap();
55
56        assert!(state.scale > 0.0);
57        for (actual, expected) in restored.iter().zip(y) {
58            assert_relative_eq!(*actual, expected, epsilon = 1.0e-10);
59        }
60    }
61
62    #[test]
63    fn uses_unit_scale_for_all_zero_targets() {
64        let state = AsinhScale::fit(&[0.0, 0.0]).unwrap();
65
66        assert_relative_eq!(state.scale, 1.0);
67        assert_relative_eq!(AsinhScale::transform(&state, 0.0), 0.0);
68    }
69}