///|
/// A reusable online feature pipeline: optional clipping, Welford
/// standardization, and deterministic sparse hashing.
pub struct FeaturePipeline {
standardizer : Standardizer
lower : Array[Double]
upper : Array[Double]
mut clip_enabled : Bool
hasher : FeatureHasher?
mut transformed : Int
}
///|
pub fn FeaturePipeline::new(
dimension : Int,
clip_lower? : Double = -1.0e12,
clip_upper? : Double = 1.0e12,
hashing_buckets? : Int,
) -> FeaturePipeline {
let size = if dimension < 0 { 0 } else { dimension }
let hasher = match hashing_buckets {
Some(value) => Some(FeatureHasher::new(value))
None => None
}
{
standardizer: Standardizer::new(size),
lower: Array::make(size, clip_lower),
upper: Array::make(size, clip_upper),
clip_enabled: clip_lower < clip_upper,
hasher,
transformed: 0,
}
}
///|
pub fn FeaturePipeline::dimension(self : FeaturePipeline) -> Int {
self.lower.length()
}
///|
pub fn FeaturePipeline::transform(
self : FeaturePipeline,
features : Array[Double],
) -> Array[Double] {
let clipped = Array::makei(self.dimension(), i => {
let value = features.get(i).unwrap_or(0.0)
if self.clip_enabled {
clamp(value, self.lower[i], self.upper[i])
} else {
value
}
})
self.transformed += 1
self.standardizer.update_and_transform(clipped)
}
///|
pub fn FeaturePipeline::fit_only(
self : FeaturePipeline,
features : Array[Double],
) -> Unit {
let _ = self.transform(features)
}
///|
pub fn FeaturePipeline::transformed(self : FeaturePipeline) -> Int {
self.transformed
}
///|
pub fn FeaturePipeline::mean(self : FeaturePipeline) -> Array[Double] {
self.standardizer.mean()
}
///|
pub fn FeaturePipeline::variance(self : FeaturePipeline) -> Array[Double] {
self.standardizer.variance()
}
///|
pub fn FeaturePipeline::transform_tokens(
self : FeaturePipeline,
tokens : Array[String],
) -> SparseVector? {
match self.hasher {
Some(hasher) => Some(hasher.encode(tokens))
None => None
}
}
///|
pub fn FeaturePipeline::set_clip(
self : FeaturePipeline,
lower : Double,
upper : Double,
) -> Bool {
if lower >= upper {
false
} else {
self.lower.fill(lower)
self.upper.fill(upper)
self.clip_enabled = true
true
}
}
///|
pub fn FeaturePipeline::disable_clip(self : FeaturePipeline) -> Unit {
self.clip_enabled = false
}
///|
pub fn FeaturePipeline::reset(self : FeaturePipeline) -> Unit {
self.standardizer.reset()
self.transformed = 0
}
///|
pub struct QualityGate {
dimension : Int
min_weight : Double
max_weight : Double
allow_nan_like : Bool
}
///|
pub fn QualityGate::new(
dimension : Int,
min_weight? : Double = 0.0,
max_weight? : Double = 1.0e12,
) -> QualityGate {
{
dimension: if dimension < 0 {
0
} else {
dimension
},
min_weight,
max_weight: if max_weight < min_weight {
min_weight
} else {
max_weight
},
allow_nan_like: false,
}
}
///|
pub fn QualityGate::validate_features(
self : QualityGate,
features : Array[Double],
) -> ValidationReport {
validate_vector(features, self.dimension)
}
///|
pub fn QualityGate::validate_weight(
self : QualityGate,
weight : Double,
) -> ValidationReport {
if weight < self.min_weight || weight > self.max_weight {
ValidationReport::error("sample weight outside configured range")
} else {
ValidationReport::ok()
}
}
///|
pub fn QualityGate::validate_sample(
self : QualityGate,
features : Array[Double],
weight : Double,
) -> ValidationReport {
let feature_report = self.validate_features(features)
if !feature_report.is_valid() {
feature_report
} else {
self.validate_weight(weight)
}
}
///|
pub fn QualityGate::dimension(self : QualityGate) -> Int {
self.dimension
}
///|
pub struct RollingWindow {
capacity : Int
features : Array[Array[Double]]
labels : Array[Double]
weights : Array[Double]
mut dropped : Int
}
///|
pub fn RollingWindow::new(capacity : Int) -> RollingWindow {
{
capacity: if capacity < 0 {
0
} else {
capacity
},
features: [],
labels: [],
weights: [],
dropped: 0,
}
}
///|
pub fn RollingWindow::push(
self : RollingWindow,
features : Array[Double],
label : Double,
weight? : Double = 1.0,
) -> Bool {
if self.capacity == 0 {
self.dropped += 1
false
} else {
self.features.push(copy_vector(features))
self.labels.push(label)
self.weights.push(weight)
if self.features.length() > self.capacity {
let _ = self.features.remove(0)
let _ = self.labels.remove(0)
let _ = self.weights.remove(0)
self.dropped += 1
}
true
}
}
///|
pub fn RollingWindow::size(self : RollingWindow) -> Int {
self.features.length()
}
///|
pub fn RollingWindow::capacity(self : RollingWindow) -> Int {
self.capacity
}
///|
pub fn RollingWindow::dropped(self : RollingWindow) -> Int {
self.dropped
}
///|
pub fn RollingWindow::batch(self : RollingWindow) -> DataBatch {
{
features: self.features.map(row => copy_vector(row)),
labels: copy_vector(self.labels),
weights: copy_vector(self.weights),
}
}
///|
pub fn RollingWindow::clear(self : RollingWindow) -> Unit {
self.features.clear()
self.labels.clear()
self.weights.clear()
self.dropped = 0
}
///|
pub struct EvaluationSummary {
samples : Double
log_loss : Double
mse : Double
accuracy : Double
precision : Double
recall : Double
f1 : Double
auc : Double
ece : Double
} derive(ToJson, FromJson, Debug)
///|
pub fn EvaluationSummary::samples(self : EvaluationSummary) -> Double {
self.samples
}
///|
pub fn EvaluationSummary::log_loss(self : EvaluationSummary) -> Double {
self.log_loss
}
///|
pub fn EvaluationSummary::mse(self : EvaluationSummary) -> Double {
self.mse
}
///|
pub fn EvaluationSummary::accuracy(self : EvaluationSummary) -> Double {
self.accuracy
}
///|
pub fn EvaluationSummary::precision(self : EvaluationSummary) -> Double {
self.precision
}
///|
pub fn EvaluationSummary::recall(self : EvaluationSummary) -> Double {
self.recall
}
///|
pub fn EvaluationSummary::f1(self : EvaluationSummary) -> Double {
self.f1
}
///|
pub fn EvaluationSummary::auc(self : EvaluationSummary) -> Double {
self.auc
}
///|
pub fn EvaluationSummary::ece(self : EvaluationSummary) -> Double {
self.ece
}
///|
pub struct OnlineEvaluationSession {
mut metrics : MetricsTracker
mut confusion : ConfusionMatrix
mut auc_tracker : AucTracker
mut calibration : CalibrationTracker
mut regression : RegressionMetrics
}
///|
pub fn OnlineEvaluationSession::new() -> OnlineEvaluationSession {
{
metrics: MetricsTracker::new(),
confusion: ConfusionMatrix::new(),
auc_tracker: AucTracker::new(),
calibration: CalibrationTracker::new(),
regression: RegressionMetrics::new(),
}
}
///|
pub fn OnlineEvaluationSession::observe_binary(
self : OnlineEvaluationSession,
probability : Double,
label : Double,
) -> Unit {
self.metrics.update(probability, label)
self.confusion.update(probability, label)
self.auc_tracker.update(probability, label)
self.calibration.update(probability, label)
}
///|
pub fn OnlineEvaluationSession::observe_regression(
self : OnlineEvaluationSession,
prediction : Double,
label : Double,
) -> Unit {
self.regression.update(prediction, label)
}
///|
pub fn OnlineEvaluationSession::summary(
self : OnlineEvaluationSession,
) -> EvaluationSummary {
{
samples: self.regression.count() + self.auc_tracker.size().to_double(),
log_loss: self.metrics.log_loss(),
mse: self.metrics.mse(),
accuracy: self.confusion.accuracy(),
precision: self.confusion.precision(),
recall: self.confusion.recall(),
f1: self.confusion.f1(),
auc: self.auc_tracker.auc(),
ece: self.calibration.ece(),
}
}
///|
pub fn OnlineEvaluationSession::classification(
self : OnlineEvaluationSession,
) -> ConfusionMatrix {
self.confusion
}
///|
pub fn OnlineEvaluationSession::regression(
self : OnlineEvaluationSession,
) -> RegressionMetrics {
self.regression
}
///|
pub fn OnlineEvaluationSession::reset(self : OnlineEvaluationSession) -> Unit {
self.metrics = MetricsTracker::new()
self.confusion = ConfusionMatrix::new()
self.auc_tracker = AucTracker::new()
self.calibration = CalibrationTracker::new()
self.regression = RegressionMetrics::new()
}
///|
pub struct TrainingReport {
mut samples : Int
mut accepted : Int
mut rejected : Int
mut total_loss : Double
mut final_loss : Double
mut elapsed_steps : Int
} derive(ToJson, FromJson, Debug)
///|
pub fn TrainingReport::new() -> TrainingReport {
{
samples: 0,
accepted: 0,
rejected: 0,
total_loss: 0.0,
final_loss: 0.0,
elapsed_steps: 0,
}
}
///|
pub fn TrainingReport::record(
self : TrainingReport,
accepted : Bool,
loss : Double,
) -> Unit {
self.samples += 1
if accepted {
self.accepted += 1
} else {
self.rejected += 1
}
self.total_loss += loss
self.final_loss = loss
self.elapsed_steps += 1
}
///|
pub fn TrainingReport::samples(self : TrainingReport) -> Int {
self.samples
}
///|
pub fn TrainingReport::accepted(self : TrainingReport) -> Int {
self.accepted
}
///|
pub fn TrainingReport::rejected(self : TrainingReport) -> Int {
self.rejected
}
///|
pub fn TrainingReport::mean_loss(self : TrainingReport) -> Double {
if self.accepted == 0 {
0.0
} else {
self.total_loss / self.accepted.to_double()
}
}
///|
pub fn TrainingReport::final_loss(self : TrainingReport) -> Double {
self.final_loss
}
///|
pub fn TrainingReport::reset(self : TrainingReport) -> Unit {
self.samples = 0
self.accepted = 0
self.rejected = 0
self.total_loss = 0.0
self.final_loss = 0.0
self.elapsed_steps = 0
}