///|
/// Stateful feature transforms for production event pipelines.
///
/// Transforms are kept separate from models so the same preprocessing state
/// can be persisted, monitored, and reused by training and serving paths.
pub(all) enum ScalarTransformKind {
Identity
Clip(lower~ : Double, upper~ : Double)
Log1p
SignedLog
Sqrt
Abs
} derive(Debug, Eq)
///|
pub fn scalar_transform_catalog() -> Array[ScalarTransformKind] {
[Identity, Log1p, SignedLog, Sqrt, Abs]
}
///|
pub fn apply_scalar_transform(
kind : ScalarTransformKind,
value : Double,
) -> Double {
let safe = if value.is_nan() || value.is_inf() { 0.0 } else { value }
match kind {
Identity => safe
Clip(lower~, upper~) => clamp(safe, lower, upper)
Log1p => if safe <= -1.0 { 0.0 } else { @math.ln(1.0 + safe) }
SignedLog =>
if safe >= 0.0 {
@math.ln(1.0 + safe)
} else {
-@math.ln(1.0 - safe)
}
Sqrt => if safe <= 0.0 { 0.0 } else { safe.sqrt() }
Abs => safe.abs()
}
}
///|
pub fn invert_scalar_transform(
kind : ScalarTransformKind,
value : Double,
) -> Double {
match kind {
Identity => value
Clip(lower~, upper~) => clamp(value, lower, upper)
Log1p => @math.exp(value) - 1.0
SignedLog =>
if value >= 0.0 {
@math.exp(value) - 1.0
} else {
1.0 - @math.exp(-value)
}
Sqrt => if value <= 0.0 { 0.0 } else { value * value }
Abs => value.abs()
}
}
///|
pub struct ScalarTransform {
kind : ScalarTransformKind
mut applied : Int
mut invalid : Int
}
///|
pub fn ScalarTransform::new(kind : ScalarTransformKind) -> ScalarTransform {
{ kind, applied: 0, invalid: 0 }
}
///|
pub fn ScalarTransform::kind(self : ScalarTransform) -> ScalarTransformKind {
self.kind
}
///|
pub fn ScalarTransform::apply(self : ScalarTransform, value : Double) -> Double {
if value.is_nan() || value.is_inf() {
self.invalid += 1
} else {
self.applied += 1
}
apply_scalar_transform(self.kind, value)
}
///|
pub fn ScalarTransform::apply_array(
self : ScalarTransform,
values : Array[Double],
) -> Array[Double] {
values.map(value => self.apply(value))
}
///|
pub fn ScalarTransform::inverse(
self : ScalarTransform,
value : Double,
) -> Double {
invert_scalar_transform(self.kind, value)
}
///|
pub fn ScalarTransform::applied(self : ScalarTransform) -> Int {
self.applied
}
///|
pub fn ScalarTransform::invalid(self : ScalarTransform) -> Int {
self.invalid
}
///|
pub fn ScalarTransform::reset(self : ScalarTransform) -> Unit {
self.applied = 0
self.invalid = 0
}
///|
pub struct VectorTransformPipeline {
transforms : Array[ScalarTransform]
expected_dimension : Int
mut rows : Int
mut rejected : Int
}
///|
pub fn VectorTransformPipeline::new(
expected_dimension : Int,
) -> VectorTransformPipeline {
{
transforms: [],
expected_dimension: if expected_dimension < 0 {
0
} else {
expected_dimension
},
rows: 0,
rejected: 0,
}
}
///|
pub fn VectorTransformPipeline::add(
self : VectorTransformPipeline,
transform : ScalarTransform,
) -> Unit {
self.transforms.push(transform)
}
///|
pub fn VectorTransformPipeline::transform(
self : VectorTransformPipeline,
values : Array[Double],
) -> Array[Double] {
if values.length() != self.expected_dimension {
self.rejected += 1
[]
} else {
let output = copy_vector(values)
for transform in self.transforms {
for i in 0.. Array[Double] {
let output = copy_vector(values)
for transform in self.transforms {
for i in 0.. Int {
self.expected_dimension
}
///|
pub fn VectorTransformPipeline::transform_count(
self : VectorTransformPipeline,
) -> Int {
self.transforms.length()
}
///|
pub fn VectorTransformPipeline::rows(self : VectorTransformPipeline) -> Int {
self.rows
}
///|
pub fn VectorTransformPipeline::rejected(self : VectorTransformPipeline) -> Int {
self.rejected
}
///|
pub fn VectorTransformPipeline::reset(self : VectorTransformPipeline) -> Unit {
for transform in self.transforms {
transform.reset()
}
self.rows = 0
self.rejected = 0
}
///|
pub struct OnlineMinMaxScaler {
minimum : Array[Double]
maximum : Array[Double]
initialized : Array[Bool]
mut observations : Int
}
///|
pub fn OnlineMinMaxScaler::new(dimension : Int) -> OnlineMinMaxScaler {
let size = if dimension < 0 { 0 } else { dimension }
{
minimum: Array::make(size, 0.0),
maximum: Array::make(size, 0.0),
initialized: Array::make(size, false),
observations: 0,
}
}
///|
pub fn OnlineMinMaxScaler::update(
self : OnlineMinMaxScaler,
values : Array[Double],
) -> Unit {
let limit = if values.length() < self.minimum.length() {
values.length()
} else {
self.minimum.length()
}
for i in 0.. self.maximum[i] {
self.maximum[i] = value
}
}
}
self.observations += 1
}
///|
pub fn OnlineMinMaxScaler::transform(
self : OnlineMinMaxScaler,
values : Array[Double],
lower? : Double = 0.0,
upper? : Double = 1.0,
) -> Array[Double] {
let output = Array::makei(self.minimum.length(), i => {
if i >= values.length() || !self.initialized[i] {
lower
} else {
let span = self.maximum[i] - self.minimum[i]
if span.abs() <= 1.0e-12 {
(lower + upper) / 2.0
} else {
lower + (values[i] - self.minimum[i]) / span * (upper - lower)
}
}
})
output.map(value => {
clamp(
value,
if lower < upper {
lower
} else {
upper
},
if lower < upper {
upper
} else {
lower
},
)
})
}
///|
pub fn OnlineMinMaxScaler::inverse(
self : OnlineMinMaxScaler,
values : Array[Double],
lower? : Double = 0.0,
upper? : Double = 1.0,
) -> Array[Double] {
Array::makei(self.minimum.length(), i => {
if i >= values.length() || !self.initialized[i] {
0.0
} else {
let span = self.maximum[i] - self.minimum[i]
if span.abs() <= 1.0e-12 {
self.minimum[i]
} else {
self.minimum[i] + (values[i] - lower) / (upper - lower) * span
}
}
})
}
///|
pub fn OnlineMinMaxScaler::minimum(self : OnlineMinMaxScaler) -> Array[Double] {
copy_vector(self.minimum)
}
///|
pub fn OnlineMinMaxScaler::maximum(self : OnlineMinMaxScaler) -> Array[Double] {
copy_vector(self.maximum)
}
///|
pub fn OnlineMinMaxScaler::observations(self : OnlineMinMaxScaler) -> Int {
self.observations
}
///|
pub fn OnlineMinMaxScaler::reset(self : OnlineMinMaxScaler) -> Unit {
self.minimum.fill(0.0)
self.maximum.fill(0.0)
self.initialized.fill(false)
self.observations = 0
}
///|
pub struct OnlineRobustScaler {
sketches : Array[OnlineQuantileSketch]
mut observations : Int
}
///|
pub fn OnlineRobustScaler::new(
dimension : Int,
capacity? : Int = 256,
) -> OnlineRobustScaler {
let size = if dimension < 0 { 0 } else { dimension }
{
sketches: Array::makei(size, _ => OnlineQuantileSketch::new(capacity~)),
observations: 0,
}
}
///|
pub fn OnlineRobustScaler::update(
self : OnlineRobustScaler,
values : Array[Double],
) -> Unit {
let limit = if values.length() < self.sketches.length() {
values.length()
} else {
self.sketches.length()
}
for i in 0.. Array[Double] {
Array::makei(self.sketches.length(), i => {
let median = self.sketches[i].quantile(0.5).unwrap_or(0.0)
let low = self.sketches[i].quantile(0.25).unwrap_or(median)
let high = self.sketches[i].quantile(0.75).unwrap_or(median)
let span = high - low
if i >= values.length() || span.abs() <= 1.0e-12 {
0.0
} else {
(values[i] - median) / span
}
})
}
///|
pub fn OnlineRobustScaler::quantile(
self : OnlineRobustScaler,
feature : Int,
probability : Double,
) -> Double? {
self.sketches
.get(feature)
.map(sketch => sketch.quantile(probability).unwrap_or(0.0))
}
///|
pub fn OnlineRobustScaler::observations(self : OnlineRobustScaler) -> Int {
self.observations
}
///|
pub fn OnlineRobustScaler::reset(self : OnlineRobustScaler) -> Unit {
for sketch in self.sketches {
sketch.reset()
}
self.observations = 0
}
///|
pub struct OneHotEncoder {
values : Map[String, Int]
unknown_index : Int?
mut next_index : Int
mut unknowns : Int
}
///|
pub fn OneHotEncoder::new(unknown_index? : Int) -> OneHotEncoder {
{ values: {}, unknown_index, next_index: 0, unknowns: 0 }
}
///|
pub fn OneHotEncoder::fit(self : OneHotEncoder, value : String) -> Int {
self.values.get_or_init(value, () => {
let index = self.next_index
self.next_index += 1
index
})
}
///|
pub fn OneHotEncoder::encode(self : OneHotEncoder, value : String) -> Int {
match self.values.get(value) {
Some(index) => index
None => {
self.unknowns += 1
self.unknown_index.unwrap_or(self.next_index)
}
}
}
///|
pub fn OneHotEncoder::vector(
self : OneHotEncoder,
value : String,
) -> Array[Double] {
let output = Array::make(self.dimension(), 0.0)
let index = self.encode(value)
if index >= 0 && index < output.length() {
output[index] = 1.0
}
output
}
///|
pub fn OneHotEncoder::dimension(self : OneHotEncoder) -> Int {
match self.unknown_index {
Some(index) =>
if index + 1 > self.next_index {
index + 1
} else {
self.next_index
}
None => self.next_index
}
}
///|
pub fn OneHotEncoder::contains(self : OneHotEncoder, value : String) -> Bool {
self.values.contains(value)
}
///|
pub fn OneHotEncoder::unknowns(self : OneHotEncoder) -> Int {
self.unknowns
}
///|
pub fn OneHotEncoder::entries(self : OneHotEncoder) -> Array[(String, Int)] {
self.values.to_array()
}
///|
pub fn OneHotEncoder::reset(self : OneHotEncoder) -> Unit {
self.values.clear()
self.next_index = 0
self.unknowns = 0
}
///|
pub struct FeatureCrossEncoder {
hasher : FeatureHasher
separator : String
mut crosses : Int
}
///|
pub fn FeatureCrossEncoder::new(
buckets : Int,
separator? : String = "=",
) -> FeatureCrossEncoder {
{ hasher: FeatureHasher::new(buckets), separator, crosses: 0 }
}
///|
pub fn FeatureCrossEncoder::encode(
self : FeatureCrossEncoder,
values : Array[String],
) -> SparseVector {
let features : Array[String] = []
for i in 0.. SparseVector {
let features = generate_feature_crosses(values, order~)
self.crosses += features.length()
self.hasher.encode(features)
}
///|
pub fn FeatureCrossEncoder::buckets(self : FeatureCrossEncoder) -> Int {
self.hasher.buckets()
}
///|
pub fn FeatureCrossEncoder::crosses(self : FeatureCrossEncoder) -> Int {
self.crosses
}
///|
pub fn FeatureCrossEncoder::reset(self : FeatureCrossEncoder) -> Unit {
self.crosses = 0
}
///|
pub struct MissingValueImputer {
policy : MissingValuePolicy
means : Array[Double]
counts : Array[Int]
last : Array[Double]
mut imputed : Int
mut rejected : Int
}
///|
pub fn MissingValueImputer::new(
dimension : Int,
policy? : MissingValuePolicy = Zero,
) -> MissingValueImputer {
let size = if dimension < 0 { 0 } else { dimension }
{
policy,
means: Array::make(size, 0.0),
counts: Array::make(size, 0),
last: Array::make(size, 0.0),
imputed: 0,
rejected: 0,
}
}
///|
pub fn MissingValueImputer::observe(
self : MissingValueImputer,
values : Array[Double],
) -> Unit {
let limit = if values.length() < self.means.length() {
values.length()
} else {
self.means.length()
}
for i in 0.. Array[Double]? {
let mut invalid = values.length() != self.means.length()
let output = Array::make(self.means.length(), 0.0)
for i in 0..= values.length() ||
values[i].is_nan() ||
values[i].is_inf()
if missing {
invalid = true
match self.policy {
Reject => {
self.rejected += 1
output[i] = 0.0
}
Zero => {
self.imputed += 1
output[i] = 0.0
}
Mean => {
self.imputed += 1
output[i] = self.means[i]
}
CarryForward => {
self.imputed += 1
output[i] = self.last[i]
}
}
} else {
output[i] = values[i]
}
}
match self.policy {
Reject => if invalid { None } else { Some(output) }
_ => Some(output)
}
}
///|
pub fn MissingValueImputer::means(self : MissingValueImputer) -> Array[Double] {
copy_vector(self.means)
}
///|
pub fn MissingValueImputer::imputed(self : MissingValueImputer) -> Int {
self.imputed
}
///|
pub fn MissingValueImputer::rejected(self : MissingValueImputer) -> Int {
self.rejected
}
///|
pub fn MissingValueImputer::reset(self : MissingValueImputer) -> Unit {
self.means.fill(0.0)
self.counts.fill(0)
self.last.fill(0.0)
self.imputed = 0
self.rejected = 0
}
///|
pub struct TimeFeatureEncoder {
period : Int64
include_sine : Bool
include_cosine : Bool
include_elapsed : Bool
mut origin : Int64?
}
///|
pub fn TimeFeatureEncoder::new(
period : Int64,
include_sine? : Bool = true,
include_cosine? : Bool = true,
include_elapsed? : Bool = false,
) -> TimeFeatureEncoder {
{
period: if period <= 0 {
1
} else {
period
},
include_sine,
include_cosine,
include_elapsed,
origin: None,
}
}
///|
pub fn TimeFeatureEncoder::transform(
self : TimeFeatureEncoder,
timestamp : Int64,
) -> Array[Double] {
let safe = timestamp % self.period
let phase = 2.0 * @math.PI * safe.to_double() / self.period.to_double()
let output : Array[Double] = []
if self.include_sine {
output.push(@math.sin(phase))
}
if self.include_cosine {
output.push(@math.cos(phase))
}
if self.include_elapsed {
let start = self.origin.unwrap_or(timestamp)
output.push((timestamp - start).to_double() / self.period.to_double())
}
if self.origin is None {
self.origin = Some(timestamp)
}
output
}
///|
pub fn TimeFeatureEncoder::feature_count(self : TimeFeatureEncoder) -> Int {
(if self.include_sine { 1 } else { 0 }) +
(if self.include_cosine { 1 } else { 0 }) +
(if self.include_elapsed { 1 } else { 0 })
}
///|
pub fn TimeFeatureEncoder::period(self : TimeFeatureEncoder) -> Int64 {
self.period
}
///|
pub fn TimeFeatureEncoder::reset(self : TimeFeatureEncoder) -> Unit {
self.origin = None
}
///|
pub struct TextVocabulary {
counts : Map[String, Int]
maximum : Int
mut documents : Int
mut unknowns : Int
}
///|
pub fn TextVocabulary::new(maximum? : Int = 10000) -> TextVocabulary {
{
counts: {},
maximum: if maximum < 1 {
1
} else {
maximum
},
documents: 0,
unknowns: 0,
}
}
///|
pub fn TextVocabulary::observe(
self : TextVocabulary,
tokens : Array[String],
) -> Unit {
self.documents += 1
for token in tokens {
if token != "" {
self.counts.update_or_default(token, 0, previous => previous + 1)
}
}
if self.counts.length() > self.maximum {
let entries = self.counts.to_array()
entries.sort_by((left, right) => left.1 - right.1)
let remove_count = self.counts.length() - self.maximum
for i in 0.. Int? {
let entries = self.counts.keys().to_array()
for i in 0.. Int {
self.counts.length()
}
///|
pub fn TextVocabulary::documents(self : TextVocabulary) -> Int {
self.documents
}
///|
pub fn TextVocabulary::unknowns(self : TextVocabulary) -> Int {
self.unknowns
}
///|
pub fn TextVocabulary::tokens(self : TextVocabulary) -> Array[String] {
self.counts.keys().to_array()
}
///|
pub fn TextVocabulary::reset(self : TextVocabulary) -> Unit {
self.counts.clear()
self.documents = 0
self.unknowns = 0
}