#[derive(Debug, Clone)]
pub struct BoxCoxResult {
pub data: Vec<f64>,
pub lambda: f64,
}
impl BoxCoxResult {
pub fn inverse(&self) -> Vec<f64> {
inv_boxcox(&self.data, self.lambda)
}
}
pub fn boxcox(series: &[f64], lambda: f64) -> Vec<f64> {
series
.iter()
.map(|&x| {
if x <= 0.0 {
f64::NAN
} else if lambda.abs() < 1e-10 {
x.ln()
} else {
(x.powf(lambda) - 1.0) / lambda
}
})
.collect()
}
pub fn boxcox_auto(series: &[f64]) -> BoxCoxResult {
let lambda = boxcox_lambda(series);
let data = boxcox(series, lambda);
BoxCoxResult { data, lambda }
}
pub fn inv_boxcox(transformed: &[f64], lambda: f64) -> Vec<f64> {
transformed
.iter()
.map(|&y| {
if lambda.abs() < 1e-10 {
y.exp()
} else {
let val = lambda * y + 1.0;
if val <= 0.0 {
f64::NAN
} else {
val.powf(1.0 / lambda)
}
}
})
.collect()
}
pub fn boxcox_lambda(series: &[f64]) -> f64 {
let positive: Vec<f64> = series.iter().copied().filter(|&x| x > 0.0).collect();
if positive.is_empty() {
return 1.0; }
let mut best_lambda = 1.0;
let mut best_llf = f64::NEG_INFINITY;
for i in -200..=200 {
let lambda = i as f64 / 100.0;
let llf = boxcox_llf(&positive, lambda);
if llf > best_llf {
best_llf = llf;
best_lambda = lambda;
}
}
let start = (best_lambda - 0.1).max(-2.0);
let end = (best_lambda + 0.1).min(2.0);
for i in 0..=100 {
let lambda = start + (end - start) * i as f64 / 100.0;
let llf = boxcox_llf(&positive, lambda);
if llf > best_llf {
best_llf = llf;
best_lambda = lambda;
}
}
best_lambda
}
fn boxcox_llf(series: &[f64], lambda: f64) -> f64 {
let n = series.len();
if n < 2 {
return f64::NEG_INFINITY;
}
let transformed = boxcox(series, lambda);
if transformed.iter().any(|x| x.is_nan()) {
return f64::NEG_INFINITY;
}
let mean = transformed.iter().sum::<f64>() / n as f64;
let variance = transformed.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / n as f64;
if variance <= 0.0 {
return f64::NEG_INFINITY;
}
let log_sum: f64 = series.iter().map(|x| x.ln()).sum();
-0.5 * n as f64 * variance.ln() + (lambda - 1.0) * log_sum
}
pub fn is_boxcox_suitable(series: &[f64]) -> bool {
!series.is_empty() && series.iter().all(|&x| x > 0.0)
}
pub fn boxcox_shifted(series: &[f64], lambda: f64) -> BoxCoxResult {
let min_val = series.iter().copied().fold(f64::INFINITY, f64::min);
let shift = if min_val <= 0.0 { -min_val + 1.0 } else { 0.0 };
let shifted: Vec<f64> = series.iter().map(|&x| x + shift).collect();
let data = boxcox(&shifted, lambda);
BoxCoxResult { data, lambda }
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
#[test]
fn boxcox_lambda_1() {
let series = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let result = boxcox(&series, 1.0);
for (i, &x) in series.iter().enumerate() {
assert_relative_eq!(result[i], x - 1.0, epsilon = 1e-10);
}
}
#[test]
fn boxcox_lambda_0() {
let series = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let result = boxcox(&series, 0.0);
for (i, &x) in series.iter().enumerate() {
assert_relative_eq!(result[i], x.ln(), epsilon = 1e-10);
}
}
#[test]
fn boxcox_lambda_2() {
let series = vec![1.0, 2.0, 3.0];
let result = boxcox(&series, 2.0);
assert_relative_eq!(result[0], 0.0, epsilon = 1e-10); assert_relative_eq!(result[1], 1.5, epsilon = 1e-10); assert_relative_eq!(result[2], 4.0, epsilon = 1e-10); }
#[test]
fn boxcox_negative_values() {
let series = vec![-1.0, 0.0, 1.0, 2.0];
let result = boxcox(&series, 1.0);
assert!(result[0].is_nan());
assert!(result[1].is_nan());
assert!(!result[2].is_nan());
assert!(!result[3].is_nan());
}
#[test]
fn boxcox_empty() {
let result = boxcox(&[], 1.0);
assert!(result.is_empty());
}
#[test]
fn inv_boxcox_roundtrip_lambda_1() {
let series = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let transformed = boxcox(&series, 1.0);
let recovered = inv_boxcox(&transformed, 1.0);
for (orig, rec) in series.iter().zip(recovered.iter()) {
assert_relative_eq!(orig, rec, epsilon = 1e-10);
}
}
#[test]
fn inv_boxcox_roundtrip_lambda_0() {
let series = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let transformed = boxcox(&series, 0.0);
let recovered = inv_boxcox(&transformed, 0.0);
for (orig, rec) in series.iter().zip(recovered.iter()) {
assert_relative_eq!(orig, rec, epsilon = 1e-10);
}
}
#[test]
fn inv_boxcox_roundtrip_lambda_05() {
let series = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let transformed = boxcox(&series, 0.5);
let recovered = inv_boxcox(&transformed, 0.5);
for (orig, rec) in series.iter().zip(recovered.iter()) {
assert_relative_eq!(orig, rec, epsilon = 1e-10);
}
}
#[test]
fn boxcox_lambda_finds_reasonable_value() {
let series = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
let lambda = boxcox_lambda(&series);
assert!((-2.0..=2.0).contains(&lambda));
}
#[test]
fn boxcox_lambda_exponential_data() {
let series: Vec<f64> = (1..=10).map(|i| (i as f64).exp()).collect();
let lambda = boxcox_lambda(&series);
assert!(
lambda.abs() < 0.5,
"Expected lambda near 0 for exponential data, got {}",
lambda
);
}
#[test]
fn boxcox_auto_works() {
let series = vec![1.0, 4.0, 9.0, 16.0, 25.0]; let result = boxcox_auto(&series);
assert!(!result.data.is_empty());
assert!(result.lambda >= -2.0 && result.lambda <= 2.0);
}
#[test]
fn boxcox_auto_inverse() {
let series = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let result = boxcox_auto(&series);
let recovered = result.inverse();
for (orig, rec) in series.iter().zip(recovered.iter()) {
assert_relative_eq!(orig, rec, epsilon = 1e-6);
}
}
#[test]
fn is_suitable_positive() {
assert!(is_boxcox_suitable(&[1.0, 2.0, 3.0]));
}
#[test]
fn is_suitable_with_zero() {
assert!(!is_boxcox_suitable(&[0.0, 1.0, 2.0]));
}
#[test]
fn is_suitable_with_negative() {
assert!(!is_boxcox_suitable(&[-1.0, 1.0, 2.0]));
}
#[test]
fn is_suitable_empty() {
assert!(!is_boxcox_suitable(&[]));
}
#[test]
fn boxcox_shifted_handles_negatives() {
let series = vec![-2.0, -1.0, 0.0, 1.0, 2.0];
let result = boxcox_shifted(&series, 1.0);
assert!(result.data.iter().all(|x| !x.is_nan()));
}
#[test]
fn boxcox_shifted_positive_unchanged() {
let series = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let result = boxcox_shifted(&series, 1.0);
let direct = boxcox(&series, 1.0);
for (s, d) in result.data.iter().zip(direct.iter()) {
assert_relative_eq!(s, d, epsilon = 1e-10);
}
}
}