use crate::error::{ForecastError, Result};
pub fn yeo_johnson(values: &[f64], lambda: f64) -> Vec<f64> {
values.iter().map(|&x| yj_forward(x, lambda)).collect()
}
pub fn inv_yeo_johnson(values: &[f64], lambda: f64) -> Vec<f64> {
values.iter().map(|&y| yj_inverse(y, lambda)).collect()
}
#[inline]
fn yj_forward(x: f64, lambda: f64) -> f64 {
if x >= 0.0 {
if (lambda).abs() < 1e-12 {
(x + 1.0).ln()
} else {
((x + 1.0).powf(lambda) - 1.0) / lambda
}
} else if (lambda - 2.0).abs() < 1e-12 {
-(-x + 1.0).ln()
} else {
-(((-x + 1.0).powf(2.0 - lambda)) - 1.0) / (2.0 - lambda)
}
}
#[inline]
fn yj_inverse(y: f64, lambda: f64) -> f64 {
if y >= 0.0 {
if lambda.abs() < 1e-12 {
y.exp() - 1.0
} else {
(lambda * y + 1.0).powf(1.0 / lambda) - 1.0
}
} else if (lambda - 2.0).abs() < 1e-12 {
1.0 - (-y).exp()
} else {
1.0 - (1.0 - (2.0 - lambda) * y).powf(1.0 / (2.0 - lambda))
}
}
pub fn yeo_johnson_lambda(values: &[f64]) -> f64 {
if values.len() < 2 {
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 = yj_llf(values, 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 = yj_llf(values, lambda);
if llf > best_llf {
best_llf = llf;
best_lambda = lambda;
}
}
best_lambda
}
fn yj_llf(values: &[f64], lambda: f64) -> f64 {
let n = values.len();
let transformed = yeo_johnson(values, lambda);
if transformed.iter().any(|x| !x.is_finite()) {
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 signed_log_sum: f64 = values
.iter()
.map(|&x| x.signum() * (x.abs() + 1.0).ln())
.sum();
-0.5 * n as f64 * variance.ln() + (lambda - 1.0) * signed_log_sum
}
pub fn yeo_johnson_auto(values: &[f64]) -> Result<(Vec<f64>, f64)> {
if values.is_empty() {
return Err(ForecastError::EmptyData);
}
let lambda = yeo_johnson_lambda(values);
Ok((yeo_johnson(values, lambda), lambda))
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
#[test]
fn forward_inverse_round_trip_positive() {
let xs = vec![0.0, 0.5, 1.0, 2.5, 10.0];
for &lambda in &[-1.5_f64, -0.5, 0.0, 0.7, 1.5, 2.0] {
let y = yeo_johnson(&xs, lambda);
let back = inv_yeo_johnson(&y, lambda);
for (x, b) in xs.iter().zip(&back) {
assert_relative_eq!(*x, *b, epsilon = 1e-10);
}
}
}
#[test]
fn forward_inverse_round_trip_mixed() {
let xs = vec![-3.0, -1.0, -0.1, 0.0, 0.1, 1.0, 3.0];
for &lambda in &[-1.0, -0.3, 0.0, 0.5, 1.2, 1.7, 2.0] {
let y = yeo_johnson(&xs, lambda);
let back = inv_yeo_johnson(&y, lambda);
for (x, b) in xs.iter().zip(&back) {
assert_relative_eq!(*x, *b, epsilon = 1e-9);
}
}
}
#[test]
fn lambda_one_is_identity_shift() {
let xs = vec![-2.0, -0.5, 0.0, 1.0, 5.0];
let y = yeo_johnson(&xs, 1.0);
for (x, yi) in xs.iter().zip(&y) {
assert_relative_eq!(*x, *yi, epsilon = 1e-10);
}
}
#[test]
fn lambda_zero_log1p_for_positives() {
let xs = vec![0.0, 1.0, 2.0, 5.0];
let y = yeo_johnson(&xs, 0.0);
for (x, yi) in xs.iter().zip(&y) {
assert_relative_eq!((x + 1.0_f64).ln(), *yi, epsilon = 1e-12);
}
}
#[test]
fn lambda_two_neg_log_for_negatives() {
let xs = vec![-3.0_f64, -1.5, -0.5];
let y = yeo_johnson(&xs, 2.0);
for (x, yi) in xs.iter().zip(&y) {
assert_relative_eq!(-(-x + 1.0).ln(), *yi, epsilon = 1e-12);
}
}
#[test]
fn auto_lambda_handles_zero_inclusive_series() {
let xs: Vec<f64> = (0..50)
.map(|i| {
if i % 5 == 0 {
0.0
} else {
(i as f64).powf(2.0)
}
})
.collect();
let (transformed, lambda) = yeo_johnson_auto(&xs).unwrap();
assert!(lambda < 1.0, "expected compressing lambda, got {}", lambda);
let back = inv_yeo_johnson(&transformed, lambda);
for (x, b) in xs.iter().zip(&back) {
assert_relative_eq!(*x, *b, epsilon = 1e-6);
}
}
#[test]
fn auto_lambda_handles_negatives_only() {
let xs: Vec<f64> = (1..30).map(|i| -(i as f64).sqrt()).collect();
let (_transformed, lambda) = yeo_johnson_auto(&xs).unwrap();
assert!(lambda.is_finite());
assert!((-2.0..=2.0).contains(&lambda));
}
}