Skip to main content

scirs2_special/
boxcox.rs

1//! Box-Cox transformations
2//!
3//! This module implements the Box-Cox family of power transformations,
4//! commonly used in statistics for data normalization and variance stabilization.
5//!
6//! ## Mathematical Theory
7//!
8//! The Box-Cox transformation is defined as:
9//! ```text
10//! y(λ) = {
11//!   (x^λ - 1) / λ    if λ ≠ 0
12//!   log(x)           if λ = 0
13//! }
14//! ```
15//!
16//! ### Properties
17//! 1. **Continuity**: The transformation is continuous at λ = 0
18//! 2. **Monotonicity**: For x > 0, y(λ) is strictly increasing in x
19//! 3. **Variance Stabilization**: Can help achieve homoscedasticity
20//! 4. **Normality**: Often helps achieve approximate normality
21//!
22//! ### Applications
23//! - Linear regression with non-normal residuals
24//! - Time series analysis
25//! - ANOVA with heteroscedastic errors
26//! - Data preprocessing for machine learning
27
28#![allow(dead_code)]
29
30use crate::{SpecialError, SpecialResult};
31use scirs2_core::ndarray::{Array1, ArrayView1};
32use scirs2_core::numeric::{Float, FromPrimitive};
33use scirs2_core::validation::{check_finite, check_positive};
34use std::fmt::{Debug, Display};
35
36/// Box-Cox transformation
37///
38/// Applies the Box-Cox power transformation:
39/// - For λ ≠ 0: (x^λ - 1) / λ
40/// - For λ = 0: log(x)
41///
42/// # Arguments
43/// * `x` - Input value (must be positive)
44/// * `lmbda` - Transformation parameter λ
45///
46/// # Returns
47/// Transformed value
48///
49/// # Examples
50/// ```
51/// use scirs2_special::boxcox;
52/// use std::f64::consts::E;
53///
54/// // λ = 0 case (logarithmic)
55/// let result = boxcox(E, 0.0).expect("Operation failed");
56/// assert!((result - 1.0f64).abs() < 1e-10);
57///
58/// // λ = 1 case (identity minus 1)
59/// let result = boxcox(5.0, 1.0).expect("Operation failed");
60/// assert!((result - 4.0f64).abs() < 1e-10);
61///
62/// // λ = 0.5 case (square root transformation)
63/// let result = boxcox(4.0, 0.5).expect("Operation failed");
64/// assert!((result - 2.0f64).abs() < 1e-10);
65/// ```
66#[allow(dead_code)]
67pub fn boxcox<T>(x: T, lmbda: T) -> SpecialResult<T>
68where
69    T: Float + FromPrimitive + Display + Copy + Debug,
70{
71    check_positive(x, "x")?;
72    check_finite(x, "x value")?;
73    check_finite(lmbda, "lmbda value")?;
74
75    let _zero = T::from_f64(0.0).expect("Operation failed");
76    let one = T::one();
77
78    if lmbda.abs() < T::from_f64(1e-10).expect("Operation failed") {
79        // λ ≈ 0: use logarithmic transformation
80        Ok(x.ln())
81    } else {
82        // λ ≠ 0: use power transformation
83        Ok((x.powf(lmbda) - one) / lmbda)
84    }
85}
86
87/// Box-Cox transformation of 1 + x
88///
89/// Applies the Box-Cox transformation to (1 + x):
90/// - For λ ≠ 0: ((1+x)^λ - 1) / λ
91/// - For λ = 0: log(1+x)
92///
93/// This variant is useful when x can be close to zero.
94///
95/// # Arguments
96/// * `x` - Input value (must be > -1)
97/// * `lmbda` - Transformation parameter λ
98///
99/// # Examples
100/// ```
101/// use scirs2_special::boxcox1p;
102///
103/// // λ = 0 case
104/// let result = boxcox1p(1.718, 0.0).expect("Operation failed");
105/// assert!((result - (1.0 + 1.718_f64).ln()).abs() < 1e-10);
106///
107/// // Small x values
108/// let result: f64 = boxcox1p(0.01, 0.5).expect("Operation failed");
109/// assert!(result.is_finite());
110/// ```
111#[allow(dead_code)]
112pub fn boxcox1p<T>(x: T, lmbda: T) -> SpecialResult<T>
113where
114    T: Float + FromPrimitive + Display + Copy + Debug,
115{
116    check_finite(x, "x value")?;
117    check_finite(lmbda, "lmbda value")?;
118
119    let neg_one = T::from_f64(-1.0).expect("Operation failed");
120    if x <= neg_one {
121        return Err(SpecialError::DomainError(
122            "x must be greater than -1".to_string(),
123        ));
124    }
125
126    let _zero = T::from_f64(0.0).expect("Operation failed");
127    let one = T::one();
128    let one_plus_x = one + x;
129
130    if lmbda.abs() < T::from_f64(1e-10).expect("Operation failed") {
131        // λ ≈ 0: use log(1+x)
132        Ok(one_plus_x.ln())
133    } else {
134        // λ ≠ 0: use power transformation
135        Ok((one_plus_x.powf(lmbda) - one) / lmbda)
136    }
137}
138
139/// Inverse Box-Cox transformation
140///
141/// Computes the inverse of the Box-Cox transformation:
142/// - For λ ≠ 0: (λ*y + 1)^(1/λ)
143/// - For λ = 0: exp(y)
144///
145/// # Arguments
146/// * `y` - Transformed value
147/// * `lmbda` - Transformation parameter λ
148///
149/// # Examples
150/// ```
151/// use scirs2_special::{boxcox, inv_boxcox};
152///
153/// let x = 5.0f64;
154/// let lmbda = 0.3f64;
155/// let y = boxcox(x, lmbda).expect("Operation failed");
156/// let x_recovered = inv_boxcox(y, lmbda).expect("Operation failed");
157/// assert!((x - x_recovered).abs() < 1e-10);
158/// ```
159#[allow(dead_code)]
160pub fn inv_boxcox<T>(y: T, lmbda: T) -> SpecialResult<T>
161where
162    T: Float + FromPrimitive + Display + Copy + Debug,
163{
164    check_finite(y, "y value")?;
165    check_finite(lmbda, "lmbda value")?;
166
167    let zero = T::from_f64(0.0).expect("Operation failed");
168    let one = T::one();
169
170    if lmbda.abs() < T::from_f64(1e-10).expect("Operation failed") {
171        // λ ≈ 0: inverse of log is exp
172        Ok(y.exp())
173    } else {
174        // λ ≠ 0: (λ*y + 1)^(1/λ)
175        let lambda_y_plus_1 = lmbda * y + one;
176
177        if lambda_y_plus_1 <= zero {
178            return Err(SpecialError::DomainError(
179                "Invalid argument for inverse transformation".to_string(),
180            ));
181        }
182
183        Ok(lambda_y_plus_1.powf(one / lmbda))
184    }
185}
186
187/// Inverse Box-Cox transformation of 1 + x form
188///
189/// Computes the inverse of boxcox1p:
190/// - For λ ≠ 0: (λ*y + 1)^(1/λ) - 1
191/// - For λ = 0: exp(y) - 1
192///
193/// # Arguments
194/// * `y` - Transformed value
195/// * `lmbda` - Transformation parameter λ
196///
197/// # Examples
198/// ```
199/// use scirs2_special::{boxcox1p, inv_boxcox1p};
200///
201/// let x = 0.5f64;
202/// let lmbda = -0.2f64;
203/// let y = boxcox1p(x, lmbda).expect("Operation failed");
204/// let x_recovered = inv_boxcox1p(y, lmbda).expect("Operation failed");
205/// assert!((x - x_recovered).abs() < 1e-10);
206/// ```
207#[allow(dead_code)]
208pub fn inv_boxcox1p<T>(y: T, lmbda: T) -> SpecialResult<T>
209where
210    T: Float + FromPrimitive + Display + Copy + Debug,
211{
212    check_finite(y, "y value")?;
213    check_finite(lmbda, "lmbda value")?;
214
215    let zero = T::from_f64(0.0).expect("Operation failed");
216    let one = T::one();
217
218    if lmbda.abs() < T::from_f64(1e-10).expect("Operation failed") {
219        // λ ≈ 0: inverse of log(1+x) is exp(y) - 1
220        Ok(y.exp() - one)
221    } else {
222        // λ ≠ 0: (λ*y + 1)^(1/λ) - 1
223        let lambda_y_plus_1 = lmbda * y + one;
224
225        if lambda_y_plus_1 <= zero {
226            return Err(SpecialError::DomainError(
227                "Invalid argument for inverse transformation".to_string(),
228            ));
229        }
230
231        Ok(lambda_y_plus_1.powf(one / lmbda) - one)
232    }
233}
234
235/// Box-Cox transformation for arrays
236///
237/// Applies Box-Cox transformation element-wise to an array.
238///
239/// # Arguments
240/// * `x` - Input array (all elements must be positive)
241/// * `lmbda` - Transformation parameter λ
242///
243/// # Examples
244/// ```
245/// use scirs2_core::ndarray::array;
246/// use scirs2_special::boxcox_array;
247///
248/// let x = array![1.0, 2.0, 4.0, 8.0];
249/// let result = boxcox_array(&x.view(), 0.0).expect("Operation failed");
250/// // Should be approximately [0, ln(2), ln(4), ln(8)]
251/// ```
252#[allow(dead_code)]
253pub fn boxcox_array<T>(x: &ArrayView1<T>, lmbda: T) -> SpecialResult<Array1<T>>
254where
255    T: Float + FromPrimitive + Display + Copy + Debug,
256{
257    let mut result = Array1::zeros(x.len());
258
259    for (i, &val) in x.iter().enumerate() {
260        result[i] = boxcox(val, lmbda)?;
261    }
262
263    Ok(result)
264}
265
266/// Box-Cox1p transformation for arrays
267#[allow(dead_code)]
268pub fn boxcox1p_array<T>(x: &ArrayView1<T>, lmbda: T) -> SpecialResult<Array1<T>>
269where
270    T: Float + FromPrimitive + Display + Copy + Debug,
271{
272    let mut result = Array1::zeros(x.len());
273
274    for (i, &val) in x.iter().enumerate() {
275        result[i] = boxcox1p(val, lmbda)?;
276    }
277
278    Ok(result)
279}
280
281/// Inverse Box-Cox transformation for arrays
282#[allow(dead_code)]
283pub fn inv_boxcox_array<T>(y: &ArrayView1<T>, lmbda: T) -> SpecialResult<Array1<T>>
284where
285    T: Float + FromPrimitive + Display + Copy + Debug,
286{
287    let mut result = Array1::zeros(y.len());
288
289    for (i, &val) in y.iter().enumerate() {
290        result[i] = inv_boxcox(val, lmbda)?;
291    }
292
293    Ok(result)
294}
295
296/// Inverse Box-Cox1p transformation for arrays
297#[allow(dead_code)]
298pub fn inv_boxcox1p_array<T>(y: &ArrayView1<T>, lmbda: T) -> SpecialResult<Array1<T>>
299where
300    T: Float + FromPrimitive + Display + Copy + Debug,
301{
302    let mut result = Array1::zeros(y.len());
303
304    for (i, &val) in y.iter().enumerate() {
305        result[i] = inv_boxcox1p(val, lmbda)?;
306    }
307
308    Ok(result)
309}
310
311#[cfg(test)]
312mod tests {
313    use super::*;
314    use approx::assert_relative_eq;
315    use scirs2_core::ndarray::array;
316
317    #[test]
318    fn test_boxcox_basic() {
319        // Test λ = 0 (logarithmic case)
320        let result = boxcox(std::f64::consts::E, 0.0).expect("Operation failed");
321        assert_relative_eq!(result, 1.0, epsilon = 1e-10);
322
323        // Test λ = 1 (linear case minus 1)
324        let result = boxcox(5.0, 1.0).expect("Operation failed");
325        assert_relative_eq!(result, 4.0, epsilon = 1e-10);
326
327        // Test λ = 0.5 (square root case)
328        let result = boxcox(4.0, 0.5).expect("Operation failed");
329        assert_relative_eq!(result, 2.0, epsilon = 1e-10);
330    }
331
332    #[test]
333    fn test_boxcox1p_basic() {
334        // Test λ = 0
335        let result = boxcox1p(1.718, 0.0).expect("Operation failed");
336        let expected = (1.0 + 1.718_f64).ln();
337        assert_relative_eq!(result, expected, epsilon = 1e-10);
338
339        // Test small values
340        let result = boxcox1p(0.01, 0.5).expect("Operation failed");
341        assert!(result.is_finite());
342    }
343
344    #[test]
345    fn test_inverse_properties() {
346        let test_values = [0.1, 1.0, 2.0, 5.0, 10.0];
347        let lambdas = [-0.5, -0.1, 0.0, 0.1, 0.5, 1.0, 2.0];
348
349        for &x in &test_values {
350            for &lmbda in &lambdas {
351                // Test boxcox and inv_boxcox
352                let y = boxcox(x, lmbda).expect("Operation failed");
353                let x_recovered = inv_boxcox(y, lmbda).expect("Operation failed");
354                assert_relative_eq!(x, x_recovered, epsilon = 1e-10);
355
356                // Test boxcox1p and inv_boxcox1p
357                let y1p = boxcox1p(x, lmbda).expect("Operation failed");
358                let x1p_recovered = inv_boxcox1p(y1p, lmbda).expect("Operation failed");
359                assert_relative_eq!(x, x1p_recovered, epsilon = 1e-10);
360            }
361        }
362    }
363
364    #[test]
365    fn test_array_operations() {
366        let x = array![1.0, 2.0, 4.0, 8.0];
367        let lmbda = 0.5;
368
369        let result = boxcox_array(&x.view(), lmbda).expect("Operation failed");
370        assert_eq!(result.len(), 4);
371
372        // Test round-trip
373        let recovered = inv_boxcox_array(&result.view(), lmbda).expect("Operation failed");
374        for i in 0..4 {
375            assert_relative_eq!(x[i], recovered[i], epsilon = 1e-10);
376        }
377    }
378
379    #[test]
380    fn test_error_conditions() {
381        // Negative input for boxcox
382        assert!(boxcox(-1.0, 0.5).is_err());
383
384        // Input <= -1 for boxcox1p
385        assert!(boxcox1p(-1.0, 0.5).is_err());
386        assert!(boxcox1p(-1.1, 0.5).is_err());
387    }
388
389    #[test]
390    fn test_continuity_at_lambda_zero() {
391        let x = 2.0;
392        let small_lambda = 1e-12;
393
394        let result_zero = boxcox(x, 0.0).expect("Operation failed");
395        let result_small = boxcox(x, small_lambda).expect("Operation failed");
396
397        // Should be very close
398        assert!((result_zero - result_small).abs() < 1e-6);
399    }
400}