quant_mathema/
transforms.rs

1use num_traits::{Num, NumCast};
2
3#[derive(Debug, Clone, Copy)]
4/// The different types of transformations which can be applied to numeric data.
5pub enum TransformType {
6    /// Applies the square root transformation over the data.
7    ///
8    /// This will compress large values more than small values,
9    /// which can be useful for stabilizing variance or reducing
10    /// the distortion effects of outliers.
11    Sqrt,
12
13    /// Applies the natural logarithm transformation over the data.
14    ///
15    /// This will aggressively compress large values and expand small values
16    /// which can be useful for stabilizing variance, normalizing skewed
17    /// distributions, and linearizing exponential relationships.
18    Log,
19
20    /// Applies the hyperbolic tangent transformation over the data.
21    ///
22    /// This will squash values into the bounded range of (-1, 1)
23    /// preserving the sign and relative magnitude which can be useful
24    /// for dampening tails and emphasizing variations near zero.
25    Tanh,
26}
27
28/// Applies a specified transformation to a numeric data series.
29///
30/// See [`crate::transforms::TransformType`] for information on the
31/// supported transformations.
32///
33/// # Example
34/// ```
35/// use quant_mathema::transforms::{apply_transform, TransformType};
36///
37/// let data = vec![0.0, 1.0, 4.0, 9.0, 16.0];
38/// let data_sqrt_trans = apply_transform(&data, TransformType::Sqrt);
39/// let data_log_trans = apply_transform(&data, TransformType::Log);
40/// let data_tanh_trans = apply_transform(&data, TransformType::Tanh);
41/// ```
42pub fn apply_transform<T>(data: &[T], transform_type: TransformType) -> Vec<f64>
43where
44    T: Num + NumCast + Copy,
45{
46    match transform_type {
47        TransformType::Sqrt => sqrt_transform(data),
48        TransformType::Log => log_transform(data),
49        TransformType::Tanh => tanh_transform(data),
50    }
51}
52
53/// Applies the square root transformation to a numeric data series.
54///
55/// This will compress large values more than small values, which can
56/// be useful for stabilizing variance or reducing the distortion effects
57/// of outliers in the data series.
58///
59/// NOTE: The square root transform is relatively weak at taming tails
60/// and is best used when only slight tail dampening is needed.
61///
62/// # Example
63/// ```
64/// use quant_mathema::transforms::sqrt_transform;
65///
66/// let data = vec![0.0, 1.0, 4.0, 9.0, 16.0];
67/// let data_sqrt_trans = sqrt_transform(&data);
68/// ```
69pub fn sqrt_transform<T>(data: &[T]) -> Vec<f64>
70where
71    T: Num + NumCast + Copy,
72{
73    if data.is_empty() {
74        return Vec::new();
75    }
76
77    data.iter()
78        .filter_map(|&x| NumCast::from(x).map(f64::sqrt))
79        .collect()
80}
81
82/// Applies the natural logarithm transformation to a numeric data series.
83///
84/// This will aggressively compresses large values while expanding small
85/// values, which can be useful for stabilizing variance, normalizing
86/// right-skewed distributions, and linearizing exponential releationships.
87///
88/// NOTE: This transformation is much stronger at tail compression than
89/// the square root transformation [`crate::transforms::sqrt_transform`],
90/// so use that if only light tail dampening is desired.
91///
92/// NOTE: The log transform is undefined for values less than or equal to zero.
93/// Such values will be skipped silently during transformation.
94///
95/// # Example
96/// ```
97/// use quant_mathema::transforms::log_transform;
98///
99/// let data = vec![1.0, 2.0, 10.0, 100.0];
100/// let data_log_trans = log_transform(&data);
101/// ```
102pub fn log_transform<T>(data: &[T]) -> Vec<f64>
103where
104    T: Num + NumCast + Copy,
105{
106    if data.is_empty() {
107        return Vec::new();
108    }
109
110    data.iter()
111        .filter_map(|&x| {
112            let x_f: f64 = NumCast::from(x)?;
113            if x_f > 0.0 { Some(x_f.ln()) } else { None }
114        })
115        .collect()
116}
117
118/// Applies the hyperbolic tangent transformation to a numeric data series.
119///
120/// This is a sigmoid family transform, so it will produce an S curve
121/// squashing values into the bounded range of (-1, 1) preserving the
122/// sign and relative magnitude which can be useful for dampening tails
123/// and emphasizing variations near zero.
124///
125/// # Example
126/// ```
127/// use quant_mathema::transforms::tanh_transform;
128///
129/// let data = vec![1.0, 2.0, 10.0, 100.0];
130/// let data_log_trans = tanh_transform(&data);
131/// ```
132pub fn tanh_transform<T>(data: &[T]) -> Vec<f64>
133where
134    T: Num + NumCast + Copy,
135{
136    if data.is_empty() {
137        return Vec::new();
138    }
139
140    data.iter()
141        .filter_map(|&x| NumCast::from(x).map(f64::tanh))
142        .collect()
143}
144
145#[cfg(test)]
146mod tests {
147    use super::*;
148
149    fn assert_close(a: f64, b: f64, eps: f64) {
150        assert!(
151            (a - b).abs() < eps,
152            "Expected {:.6}, got {:.6}, diff = {:.6}",
153            b,
154            a,
155            (a - b).abs()
156        );
157    }
158
159    #[test]
160    fn test_sqrt_transform_with_f64() {
161        let data = vec![0.0, 1.0, 4.0, 9.0, 16.0];
162        let expected = [0.0, 1.0, 2.0, 3.0, 4.0];
163        let result = sqrt_transform(&data);
164
165        for (a, b) in result.into_iter().zip(expected.into_iter()) {
166            assert_close(a, b, 1e-6);
167        }
168    }
169
170    #[test]
171    fn test_sqrt_transform_with_u32() {
172        let data = vec![0u32, 1, 4, 9, 16, 25];
173        let expected = [0.0, 1.0, 2.0, 3.0, 4.0, 5.0];
174        let result = sqrt_transform(&data);
175
176        for (a, b) in result.into_iter().zip(expected.into_iter()) {
177            assert_close(a, b, 1e-6);
178        }
179    }
180
181    #[test]
182    fn test_sqrt_transform_with_empty_input() {
183        let data: Vec<u32> = vec![];
184        let result = sqrt_transform(&data);
185        assert!(result.is_empty());
186    }
187
188    #[test]
189    fn test_sqrt_transform_skips_unconvertible_values() {
190        let data = vec![1u8, 4u8, 9u8];
191        let result = sqrt_transform(&data);
192        assert_eq!(result.len(), 3);
193    }
194
195    #[test]
196    fn test_log_transform_with_f64() {
197        let data = vec![1.0, std::f64::consts::E, 10.0, 100.0];
198        let expected = [0.0, 1.0, 10.0_f64.ln(), 100.0_f64.ln()];
199        let result = log_transform(&data);
200
201        for (a, b) in result.iter().zip(expected.iter()) {
202            assert_close(*a, *b, 1e-10);
203        }
204    }
205
206    #[test]
207    fn test_log_transform_with_u32() {
208        let data = vec![1u32, 2, 10, 100];
209        let expected = vec![1.0, 2.0, 10.0, 100.0]
210            .into_iter()
211            .map(f64::ln)
212            .collect::<Vec<_>>();
213        let result = log_transform(&data);
214
215        for (a, b) in result.iter().zip(expected.iter()) {
216            assert_close(*a, *b, 1e-10);
217        }
218    }
219
220    #[test]
221    fn test_log_transform_skips_zeros_and_negatives() {
222        let data = vec![0.0, -1.0, -100.0, 1.0];
223        let result = log_transform(&data);
224        assert_eq!(result.len(), 1);
225        assert_close(result[0], 0.0, 1e-10);
226    }
227
228    #[test]
229    fn test_log_transform_empty_input() {
230        let data: Vec<f64> = vec![];
231        let result = log_transform(&data);
232        assert!(result.is_empty());
233    }
234
235    #[test]
236    fn test_tanh_transform_with_f64() {
237        let data: Vec<f64> = vec![0.0, 1.0, -1.0, 10.0, -10.0];
238        let expected: Vec<f64> = data.iter().map(|x| x.tanh()).collect();
239        let result = tanh_transform(&data);
240
241        for (a, b) in result.iter().zip(expected.iter()) {
242            assert_close(*a, *b, 1e-6);
243        }
244    }
245
246    #[test]
247    fn test_tanh_transform_with_integers() {
248        let data = vec![-3, -1, 0, 1, 3];
249        let expected: Vec<f64> = data.iter().map(|&x| (x as f64).tanh()).collect();
250        let result = tanh_transform(&data);
251
252        for (a, b) in result.iter().zip(expected.iter()) {
253            assert_close(*a, *b, 1e-6);
254        }
255    }
256
257    #[test]
258    fn test_tanh_transform_extremes() {
259        let data = vec![1000.0, -1000.0];
260        let result = tanh_transform(&data);
261        assert_close(result[0], 1.0, 1e-6);
262        assert_close(result[1], -1.0, 1e-6);
263    }
264
265    #[test]
266    fn test_tanh_transform_empty_input() {
267        let data: Vec<f64> = vec![];
268        let result = tanh_transform(&data);
269        assert!(result.is_empty());
270    }
271}