///|
/// Streaming histogram and shallow-tree models for bounded online learning.
///
/// These components are deliberately small enough for edge deployment while
/// retaining useful production behavior: bounded memory, incremental updates,
/// quantile queries, adaptive thresholds, and prediction dispersion.
pub struct HistogramBin {
lower : Double
upper : Double
mut count : Double
mut sum : Double
mut square_sum : Double
mut positive : Double
}
///|
pub fn HistogramBin::new(lower : Double, upper : Double) -> HistogramBin {
{ lower, upper, count: 0.0, sum: 0.0, square_sum: 0.0, positive: 0.0 }
}
///|
pub fn HistogramBin::lower(self : HistogramBin) -> Double {
self.lower
}
///|
pub fn HistogramBin::upper(self : HistogramBin) -> Double {
self.upper
}
///|
pub fn HistogramBin::count(self : HistogramBin) -> Double {
self.count
}
///|
pub fn HistogramBin::sum(self : HistogramBin) -> Double {
self.sum
}
///|
pub fn HistogramBin::mean(self : HistogramBin) -> Double {
if self.count <= 0.0 {
0.0
} else {
self.sum / self.count
}
}
///|
pub fn HistogramBin::variance(self : HistogramBin) -> Double {
if self.count <= 0.0 {
0.0
} else {
let value = self.square_sum / self.count - self.mean() * self.mean()
if value < 0.0 {
0.0
} else {
value
}
}
}
///|
pub fn HistogramBin::positive_rate(self : HistogramBin) -> Double {
if self.count <= 0.0 {
0.0
} else {
self.positive / self.count
}
}
///|
pub fn HistogramBin::observe(
self : HistogramBin,
value : Double,
weight : Double,
positive : Bool,
) -> Unit {
let safe_weight = if weight <= 0.0 || weight.is_nan() || weight.is_inf() {
0.0
} else {
weight
}
self.count += safe_weight
self.sum += value * safe_weight
self.square_sum += value * value * safe_weight
if positive {
self.positive += safe_weight
}
}
///|
pub fn HistogramBin::reset(self : HistogramBin) -> Unit {
self.count = 0.0
self.sum = 0.0
self.square_sum = 0.0
self.positive = 0.0
}
///|
pub struct AdaptiveHistogram {
lower : Double
upper : Double
bins : Array[HistogramBin]
mut seen : Double
mut outliers : Int
}
///|
pub fn AdaptiveHistogram::new(
lower : Double,
upper : Double,
bin_count? : Int = 32,
) -> AdaptiveHistogram {
let left = if lower.is_nan() || lower.is_inf() { 0.0 } else { lower }
let right = if upper <= left || upper.is_nan() || upper.is_inf() {
left + 1.0
} else {
upper
}
let count = if bin_count < 1 { 1 } else { bin_count }
let width = (right - left) / count.to_double()
{
lower: left,
upper: right,
bins: Array::makei(count, i => {
HistogramBin::new(
left + i.to_double() * width,
left + (i + 1).to_double() * width,
)
}),
seen: 0.0,
outliers: 0,
}
}
///|
fn adaptive_histogram_index(
histogram : AdaptiveHistogram,
value : Double,
) -> Int {
let width = (histogram.upper - histogram.lower) /
histogram.bins.length().to_double()
if value <= histogram.lower {
0
} else if value >= histogram.upper {
histogram.bins.length() - 1
} else {
((value - histogram.lower) / width)
.to_int()
.max(0)
.min(histogram.bins.length() - 1)
}
}
///|
pub fn AdaptiveHistogram::observe(
self : AdaptiveHistogram,
value : Double,
weight? : Double = 1.0,
positive? : Bool = false,
) -> Bool {
if value.is_nan() || value.is_inf() {
false
} else {
if value < self.lower || value > self.upper {
self.outliers += 1
}
let index = adaptive_histogram_index(self, value)
self.bins[index].observe(value, weight, positive)
self.seen += if weight <= 0.0 { 0.0 } else { weight }
true
}
}
///|
pub fn AdaptiveHistogram::bin(
self : AdaptiveHistogram,
index : Int,
) -> HistogramBin? {
self.bins.get(index)
}
///|
pub fn AdaptiveHistogram::bin_count(self : AdaptiveHistogram) -> Int {
self.bins.length()
}
///|
pub fn AdaptiveHistogram::seen(self : AdaptiveHistogram) -> Double {
self.seen
}
///|
pub fn AdaptiveHistogram::outliers(self : AdaptiveHistogram) -> Int {
self.outliers
}
///|
pub fn AdaptiveHistogram::mean(self : AdaptiveHistogram) -> Double {
if self.seen <= 0.0 {
0.0
} else {
let total = Ref(0.0)
for bin in self.bins {
total.val += bin.sum()
}
total.val / self.seen
}
}
///|
pub fn AdaptiveHistogram::variance(self : AdaptiveHistogram) -> Double {
if self.seen <= 0.0 {
0.0
} else {
let squares = Ref(0.0)
for bin in self.bins {
squares.val += bin.square_sum
}
let value = squares.val / self.seen - self.mean() * self.mean()
if value < 0.0 {
0.0
} else {
value
}
}
}
///|
pub fn AdaptiveHistogram::quantile(
self : AdaptiveHistogram,
probability : Double,
) -> Double {
if self.seen <= 0.0 {
0.0
} else {
let target = clamp(probability, 0.0, 1.0) * self.seen
let running = Ref(0.0)
let result = Ref(self.upper)
for bin in self.bins {
running.val += bin.count()
if running.val >= target && result.val == self.upper {
result.val = (bin.lower() + bin.upper()) / 2.0
}
}
result.val
}
}
///|
pub fn AdaptiveHistogram::cdf(
self : AdaptiveHistogram,
value : Double,
) -> Double {
if self.seen <= 0.0 {
0.0
} else if value <= self.lower {
0.0
} else if value >= self.upper {
1.0
} else {
let index = adaptive_histogram_index(self, value)
let before = Ref(0.0)
for i in 0.. Double {
if self.seen <= 0.0 {
0.0
} else {
let positives = Ref(0.0)
for bin in self.bins {
positives.val += bin.positive
}
positives.val / self.seen
}
}
///|
pub fn AdaptiveHistogram::reset(self : AdaptiveHistogram) -> Unit {
for bin in self.bins {
bin.reset()
}
self.seen = 0.0
self.outliers = 0
}
///|
pub struct StumpStatistics {
mut count : Double
mut sum : Double
mut square_sum : Double
mut positive : Double
}
///|
pub fn StumpStatistics::new() -> StumpStatistics {
{ count: 0.0, sum: 0.0, square_sum: 0.0, positive: 0.0 }
}
///|
pub fn StumpStatistics::observe(
self : StumpStatistics,
label : Double,
positive : Bool,
weight : Double,
) -> Unit {
self.count += weight
self.sum += label * weight
self.square_sum += label * label * weight
if positive {
self.positive += weight
}
}
///|
pub fn StumpStatistics::count(self : StumpStatistics) -> Double {
self.count
}
///|
pub fn StumpStatistics::mean(self : StumpStatistics) -> Double {
if self.count <= 0.0 {
0.0
} else {
self.sum / self.count
}
}
///|
pub fn StumpStatistics::variance(self : StumpStatistics) -> Double {
if self.count <= 0.0 {
0.0
} else {
let value = self.square_sum / self.count - self.mean() * self.mean()
if value < 0.0 {
0.0
} else {
value
}
}
}
///|
pub fn StumpStatistics::positive_rate(self : StumpStatistics) -> Double {
if self.count <= 0.0 {
0.0
} else {
self.positive / self.count
}
}
///|
pub fn StumpStatistics::reset(self : StumpStatistics) -> Unit {
self.count = 0.0
self.sum = 0.0
self.square_sum = 0.0
self.positive = 0.0
}
///|
pub struct StreamingDecisionStump {
feature : Int
threshold : Double
left : StumpStatistics
right : StumpStatistics
mut updates : Int
mut mistakes : Int
}
///|
pub fn StreamingDecisionStump::new(
feature : Int,
threshold : Double,
) -> StreamingDecisionStump {
{
feature: if feature < 0 {
0
} else {
feature
},
threshold,
left: StumpStatistics::new(),
right: StumpStatistics::new(),
updates: 0,
mistakes: 0,
}
}
///|
pub fn StreamingDecisionStump::feature(self : StreamingDecisionStump) -> Int {
self.feature
}
///|
pub fn StreamingDecisionStump::threshold(
self : StreamingDecisionStump,
) -> Double {
self.threshold
}
///|
pub fn StreamingDecisionStump::branch(
self : StreamingDecisionStump,
features : Array[Double],
) -> Bool {
features.get(self.feature).unwrap_or(0.0) >= self.threshold
}
///|
pub fn StreamingDecisionStump::predict(
self : StreamingDecisionStump,
features : Array[Double],
) -> Double {
if self.branch(features) {
self.right.mean()
} else {
self.left.mean()
}
}
///|
pub fn StreamingDecisionStump::predict_probability(
self : StreamingDecisionStump,
features : Array[Double],
) -> Double {
if self.branch(features) {
self.right.positive_rate()
} else {
self.left.positive_rate()
}
}
///|
pub fn StreamingDecisionStump::update(
self : StreamingDecisionStump,
features : Array[Double],
label : Double,
weight? : Double = 1.0,
) -> Bool {
let safe_weight = if weight <= 0.0 || weight.is_nan() { 0.0 } else { weight }
let positive = label >= 0.5
let branch = self.branch(features)
let before = self.predict(features)
if branch {
self.right.observe(label, positive, safe_weight)
} else {
self.left.observe(label, positive, safe_weight)
}
self.updates += 1
if (before >= 0.5) != positive {
self.mistakes += 1
}
true
}
///|
pub fn StreamingDecisionStump::updates(self : StreamingDecisionStump) -> Int {
self.updates
}
///|
pub fn StreamingDecisionStump::mistakes(self : StreamingDecisionStump) -> Int {
self.mistakes
}
///|
pub fn StreamingDecisionStump::accuracy(
self : StreamingDecisionStump,
) -> Double {
if self.updates == 0 {
0.0
} else {
1.0 - self.mistakes.to_double() / self.updates.to_double()
}
}
///|
pub fn StreamingDecisionStump::left(
self : StreamingDecisionStump,
) -> StumpStatistics {
self.left
}
///|
pub fn StreamingDecisionStump::right(
self : StreamingDecisionStump,
) -> StumpStatistics {
self.right
}
///|
pub fn StreamingDecisionStump::reset(self : StreamingDecisionStump) -> Unit {
self.left.reset()
self.right.reset()
self.updates = 0
self.mistakes = 0
}
///|
pub struct StreamingStumpEnsemble {
stumps : Array[StreamingDecisionStump]
mut observations : Int
mut total_loss : Double
}
///|
pub fn StreamingStumpEnsemble::new(
features : Array[(Int, Double)],
) -> StreamingStumpEnsemble {
let stumps = Array::make(0, StreamingDecisionStump::new(0, 0.0))
for item in features {
stumps.push(StreamingDecisionStump::new(item.0, item.1))
}
{ stumps, observations: 0, total_loss: 0.0 }
}
///|
pub fn StreamingStumpEnsemble::predict(
self : StreamingStumpEnsemble,
features : Array[Double],
) -> Double {
if self.stumps.is_empty() {
0.0
} else {
let total = Ref(0.0)
for stump in self.stumps {
total.val += stump.predict(features)
}
total.val / self.stumps.length().to_double()
}
}
///|
pub fn StreamingStumpEnsemble::predict_probability(
self : StreamingStumpEnsemble,
features : Array[Double],
) -> Double {
if self.stumps.is_empty() {
0.0
} else {
let total = Ref(0.0)
for stump in self.stumps {
total.val += stump.predict_probability(features)
}
total.val / self.stumps.length().to_double()
}
}
///|
pub fn StreamingStumpEnsemble::dispersion(
self : StreamingStumpEnsemble,
features : Array[Double],
) -> Double {
if self.stumps.length() <= 1 {
0.0
} else {
let mean = self.predict(features)
let total = Ref(0.0)
for stump in self.stumps {
let difference = stump.predict(features) - mean
total.val += difference * difference
}
(total.val / self.stumps.length().to_double()).sqrt()
}
}
///|
pub fn StreamingStumpEnsemble::update(
self : StreamingStumpEnsemble,
features : Array[Double],
label : Double,
) -> Double {
let prediction = self.predict(features)
let error = label - prediction
for stump in self.stumps {
let _ = stump.update(features, label)
}
self.observations += 1
self.total_loss += error * error
prediction
}
///|
pub fn StreamingStumpEnsemble::stump_count(
self : StreamingStumpEnsemble,
) -> Int {
self.stumps.length()
}
///|
pub fn StreamingStumpEnsemble::observations(
self : StreamingStumpEnsemble,
) -> Int {
self.observations
}
///|
pub fn StreamingStumpEnsemble::mse(self : StreamingStumpEnsemble) -> Double {
if self.observations == 0 {
0.0
} else {
self.total_loss / self.observations.to_double()
}
}
///|
pub fn StreamingStumpEnsemble::stumps(
self : StreamingStumpEnsemble,
) -> Array[StreamingDecisionStump] {
self.stumps.map(value => value)
}
///|
pub fn StreamingStumpEnsemble::reset(self : StreamingStumpEnsemble) -> Unit {
for stump in self.stumps {
stump.reset()
}
self.observations = 0
self.total_loss = 0.0
}
///|
pub struct AdaptiveThreshold {
target_rate : Double
histogram : AdaptiveHistogram
mut threshold : Double
mut updates : Int
}
///|
pub fn AdaptiveThreshold::new(
lower : Double,
upper : Double,
target_rate? : Double = 0.05,
bins? : Int = 64,
) -> AdaptiveThreshold {
{
target_rate: clamp(target_rate, 0.0, 1.0),
histogram: AdaptiveHistogram::new(lower, upper, bin_count=bins),
threshold: upper,
updates: 0,
}
}
///|
pub fn AdaptiveThreshold::observe(
self : AdaptiveThreshold,
value : Double,
) -> Bool {
let accepted = self.histogram.observe(value)
if accepted {
self.updates += 1
self.threshold = self.histogram.quantile(1.0 - self.target_rate)
}
accepted
}
///|
pub fn AdaptiveThreshold::is_anomaly(
self : AdaptiveThreshold,
value : Double,
) -> Bool {
value > self.threshold
}
///|
pub fn AdaptiveThreshold::threshold(self : AdaptiveThreshold) -> Double {
self.threshold
}
///|
pub fn AdaptiveThreshold::target_rate(self : AdaptiveThreshold) -> Double {
self.target_rate
}
///|
pub fn AdaptiveThreshold::updates(self : AdaptiveThreshold) -> Int {
self.updates
}
///|
pub fn AdaptiveThreshold::score(
self : AdaptiveThreshold,
value : Double,
) -> Double {
if self.threshold <= 0.0 {
0.0
} else {
clamp(value / self.threshold, 0.0, 2.0)
}
}
///|
pub fn AdaptiveThreshold::reset(self : AdaptiveThreshold) -> Unit {
self.histogram.reset()
self.threshold = self.histogram.upper
self.updates = 0
}