#![forbid(unsafe_code)]
use thiserror::Error;
pub mod transforms;
pub use transforms::{
AsinhScale, AsinhScaleState, IdentityPositive, IdentityPositiveState, Log, Log1pShift,
Log1pShiftState, LogState, Standardize, StandardizeState,
};
#[derive(Debug, Clone, PartialEq, Eq, Error)]
pub enum TransformError {
#[error("target vector must contain at least one value")]
EmptyInput,
#[error("target contains a non-finite value")]
NonFiniteValue,
#[error("target value must be finite and > 0")]
NonPositiveValue,
#[error("target value is below the fitted lower bound")]
BelowLowerBound,
#[error("target scale must be positive")]
ZeroScale,
#[error("output length is {actual}, expected {expected}")]
LengthMismatch {
expected: usize,
actual: usize,
},
}
pub trait TargetTransform {
type State;
fn fit(y: &[f64]) -> Result<Self::State, TransformError>;
fn transform(state: &Self::State, y: f64) -> f64;
fn inverse(state: &Self::State, value: f64) -> f64;
#[inline]
fn fit_transform(y: &[f64]) -> Result<(Self::State, Vec<f64>), TransformError> {
let state = Self::fit(y)?;
let transformed = Self::transform_slice(&state, y)?;
Ok((state, transformed))
}
#[inline]
fn transform_slice(state: &Self::State, y: &[f64]) -> Result<Vec<f64>, TransformError> {
let mut out = vec![0.0; y.len()];
Self::transform_into(state, y, &mut out)?;
Ok(out)
}
#[inline]
fn transform_into(
state: &Self::State,
y: &[f64],
out: &mut [f64],
) -> Result<(), TransformError> {
map_slice_into(y, out, validate_finite, |value| {
Self::transform(state, value)
})
}
#[inline]
fn inverse_slice(state: &Self::State, values: &[f64]) -> Result<Vec<f64>, TransformError> {
let mut out = vec![0.0; values.len()];
Self::inverse_into(state, values, &mut out)?;
Ok(out)
}
#[inline]
fn inverse_into(
state: &Self::State,
values: &[f64],
out: &mut [f64],
) -> Result<(), TransformError> {
map_slice_into(values, out, validate_finite, |value| {
Self::inverse(state, value)
})
}
}
pub(crate) fn map_slice_into(
values: &[f64],
out: &mut [f64],
validate: impl FnOnce(&[f64]) -> Result<(), TransformError>,
mut map: impl FnMut(f64) -> f64,
) -> Result<(), TransformError> {
validate_output_len(values.len(), out.len())?;
validate(values)?;
for (out, value) in out.iter_mut().zip(values.iter().copied()) {
*out = map(value);
}
Ok(())
}
pub(crate) fn validate_output_len(expected: usize, actual: usize) -> Result<(), TransformError> {
if actual == expected {
Ok(())
} else {
Err(TransformError::LengthMismatch { expected, actual })
}
}
pub(crate) fn validate_non_empty_finite(values: &[f64]) -> Result<(), TransformError> {
if values.is_empty() {
return Err(TransformError::EmptyInput);
}
validate_finite(values)
}
pub(crate) fn validate_finite(values: &[f64]) -> Result<(), TransformError> {
if values.iter().all(|value| value.is_finite()) {
Ok(())
} else {
Err(TransformError::NonFiniteValue)
}
}
pub(crate) fn validate_positive(values: &[f64]) -> Result<(), TransformError> {
validate_non_empty_finite(values)?;
if values.iter().all(|value| *value > 0.0) {
Ok(())
} else {
Err(TransformError::NonPositiveValue)
}
}
pub(crate) fn validate_shifted_non_negative(
values: &[f64],
shift: f64,
) -> Result<(), TransformError> {
validate_finite(values)?;
if values.iter().all(|value| *value + shift >= 0.0) {
Ok(())
} else {
Err(TransformError::BelowLowerBound)
}
}
pub(crate) fn median_sorted(values: &[f64]) -> Option<f64> {
match values.len() {
0 => None,
len if len % 2 == 1 => Some(values[len / 2]),
len => {
let upper = len / 2;
Some((values[upper - 1] + values[upper]) / 2.0)
}
}
}
pub mod prelude {
pub use crate::{
AsinhScale, AsinhScaleState, IdentityPositive, IdentityPositiveState, Log, Log1pShift,
Log1pShiftState, LogState, Standardize, StandardizeState, TargetTransform, TransformError,
};
}