///|
/// Production-safe numeric transforms and reversible preprocessing metadata.
pub enum TransformKind {
Identity
Standardize
MinMax
Robust
Log1p
Clip
Quantile
Rank
} derive(Debug, Eq, ToJson, FromJson)
///|
pub struct TransformParameters {
kind : TransformKind
center : Double
scale : Double
lower : Double
upper : Double
fitted_count : Int
} derive(Debug, Eq, ToJson, FromJson)
///|
pub fn TransformParameters::identity() -> TransformParameters {
{
kind: TransformKind::Identity,
center: 0.0,
scale: 1.0,
lower: 0.0,
upper: 0.0,
fitted_count: 0,
}
}
///|
pub fn TransformParameters::fit_standard(
values : Array[Double],
) -> TransformParameters {
if values.length() == 0 {
abort("fit_standard: empty values")
}
{
kind: TransformKind::Standardize,
center: mean(values),
scale: variance(values).sqrt().max(0.000000000001),
lower: 0.0,
upper: 0.0,
fitted_count: values.length(),
}
}
///|
pub fn TransformParameters::fit_minmax(
values : Array[Double],
lower : Double,
upper : Double,
) -> TransformParameters {
if values.length() == 0 || lower >= upper {
abort("fit_minmax: invalid inputs")
}
{
kind: TransformKind::MinMax,
center: values.fold(init=values[0], (current, value) => current.min(value)),
scale: values.fold(init=values[0], (current, value) => current.max(value)) -
values.fold(init=values[0], (current, value) => current.min(value)),
lower,
upper,
fitted_count: values.length(),
}
}
///|
pub fn TransformParameters::fit_robust(
values : Array[Double],
) -> TransformParameters {
if values.length() == 0 {
abort("fit_robust: empty values")
}
{
kind: TransformKind::Robust,
center: empirical_quantile(values, 0.5),
scale: (empirical_quantile(values, 0.75) - empirical_quantile(values, 0.25)).max(
0.000000000001,
),
lower: 0.0,
upper: 0.0,
fitted_count: values.length(),
}
}
///|
pub fn TransformParameters::fit_clip(
values : Array[Double],
lower : Double,
upper : Double,
) -> TransformParameters {
if values.length() == 0 || lower > upper {
abort("fit_clip: invalid inputs")
}
{
kind: TransformKind::Clip,
center: 0.0,
scale: 1.0,
lower,
upper,
fitted_count: values.length(),
}
}
///|
pub fn TransformParameters::apply(
self : TransformParameters,
value : Double,
) -> Double {
match self.kind {
TransformKind::Identity => value
TransformKind::Standardize => (value - self.center) / self.scale
TransformKind::MinMax =>
self.lower +
(value - self.center) / self.scale * (self.upper - self.lower)
TransformKind::Robust => (value - self.center) / self.scale
TransformKind::Log1p =>
if value <= -1.0 {
-1.0
} else {
@math.pow(1.0 + value, 0.5) * 2.0 - 2.0
}
TransformKind::Clip => value.max(self.lower).min(self.upper)
TransformKind::Quantile | TransformKind::Rank => value
}
}
///|
pub fn TransformParameters::inverse(
self : TransformParameters,
value : Double,
) -> Double {
match self.kind {
TransformKind::Identity => value
TransformKind::Standardize => value * self.scale + self.center
TransformKind::MinMax =>
self.center +
(value - self.lower) / (self.upper - self.lower) * self.scale
TransformKind::Robust => value * self.scale + self.center
TransformKind::Log1p => value
TransformKind::Clip => value
TransformKind::Quantile | TransformKind::Rank => value
}
}
///|
pub fn transform_values(
parameters : TransformParameters,
values : Array[Double],
) -> Array[Double] {
values.map(fn(value) { parameters.apply(value) })
}
///|
pub fn inverse_values(
parameters : TransformParameters,
values : Array[Double],
) -> Array[Double] {
values.map(fn(value) { parameters.inverse(value) })
}
///|
pub struct TransformPipeline {
parameters : Array[TransformParameters]
names : Array[String]
} derive(Debug, Eq, ToJson, FromJson)
///|
pub fn TransformPipeline::new() -> TransformPipeline {
{ parameters: [], names: [] }
}
///|
pub fn TransformPipeline::add(
self : TransformPipeline,
name : String,
parameters : TransformParameters,
) -> TransformPipeline {
if self.names.contains(name) {
abort("TransformPipeline: duplicate step")
}
let names = self.names.copy()
names.push(name)
let values = self.parameters.copy()
values.push(parameters)
{ parameters: values, names }
}
///|
pub fn TransformPipeline::apply(
self : TransformPipeline,
values : Array[Double],
) -> Array[Double] {
let mut result = values
for parameters in self.parameters.iter() {
result = transform_values(parameters, result)
}
result
}
///|
pub fn TransformPipeline::inverse(
self : TransformPipeline,
values : Array[Double],
) -> Array[Double] {
let mut result = values
for index in 0.. (Array[Double], MissingValuePolicy) {
if values.length() == 0 {
abort("transform_impute_mean: empty values")
}
let observed = values.filter(fn(value) { value == value })
let fill = if observed.length() == 0 { 0.0 } else { mean(observed) }
let imputed = values.map(fn(value) {
if value != value {
fill
} else {
value
}
})
(
imputed,
{
fill,
lower: fill,
upper: fill,
dropped: values.length() - observed.length(),
},
)
}
///|
pub fn transform_impute_median(
values : Array[Double],
) -> (Array[Double], MissingValuePolicy) {
if values.length() == 0 {
abort("transform_impute_median: empty values")
}
let observed = values.filter(fn(value) { value == value })
let fill = if observed.length() == 0 {
0.0
} else {
empirical_quantile(observed, 0.5)
}
let imputed = values.map(fn(value) {
if value != value {
fill
} else {
value
}
})
(
imputed,
{
fill,
lower: fill,
upper: fill,
dropped: values.length() - observed.length(),
},
)
}
///|
pub fn transform_winsorize(
values : Array[Double],
lower_quantile : Double,
upper_quantile : Double,
) -> Array[Double] {
if values.length() == 0 ||
lower_quantile < 0.0 ||
upper_quantile > 1.0 ||
lower_quantile > upper_quantile {
abort("transform_winsorize: invalid inputs")
}
let lower = empirical_quantile(values, lower_quantile)
let upper = empirical_quantile(values, upper_quantile)
values.map(fn(value) { value.max(lower).min(upper) })
}
///|
pub fn normalize_l1(values : Array[Double]) -> Array[Double] {
let total = values.fold(init=0.0, (sum_value, value) => {
sum_value + value.abs()
})
if total == 0.0 {
values.copy()
} else {
values.map(fn(value) { value / total })
}
}
///|
pub fn normalize_l2(values : Array[Double]) -> Array[Double] {
let total = values
.fold(init=0.0, (sum_value, value) => sum_value + value * value)
.sqrt()
if total == 0.0 {
values.copy()
} else {
values.map(fn(value) { value / total })
}
}
///|
pub fn safe_divide(
numerator : Array[Double],
denominator : Array[Double],
default_value : Double,
) -> Array[Double] {
if numerator.length() != denominator.length() {
abort("safe_divide: lengths must match")
}
let result : Array[Double] = []
for index in 0.. Array[Double] {
if window <= 0 {
abort("rolling_center: window must be positive")
}
let result : Array[Double] = []
for index in 0.. Array[Double] {
if window <= 0 {
abort("rolling_scale: window must be positive")
}
let result : Array[Double] = []
for index in 0.. EncodingMap {
let categories : Array[String] = []
for value in values.iter() {
if !categories.contains(value) {
categories.push(value)
}
}
{ categories, unknown_code: -1, ordered }
}
///|
pub fn EncodingMap::encode(self : EncodingMap, value : String) -> Int {
for index in 0.. String {
if code < 0 || code >= self.categories.length() {
""
} else {
self.categories[code]
}
}
///|
pub fn EncodingMap::encode_all(
self : EncodingMap,
values : Array[String],
) -> Array[Int] {
values.map(fn(value) { self.encode(value) })
}
///|
pub fn transform_one_hot(
values : Array[Int],
classes : Int,
) -> Array[Array[Double]] {
if classes <= 0 {
abort("transform_one_hot: classes must be positive")
}
values.map(fn(value) {
let row : Array[Double] = []
for index in 0.. TransformAudit {
if before.length() != after.length() || before.length() == 0 {
abort("transform_audit: lengths must match")
}
let missing_before = before.filter(fn(value) { value != value }).length()
let missing_after = after.filter(fn(value) { value != value }).length()
let clipped = after
.filter(fn(value) { value <= lower || value >= upper })
.length()
{
rows: before.length(),
missing_before,
missing_after,
clipped,
mean_before: mean(before.filter(fn(value) { value == value })),
mean_after: mean(after.filter(fn(value) { value == value })),
}
}