///|
/// Online moments and extrema for long-running RR ingestion.
pub(all) struct OnlineStats {
mut count : Int
mut mean : Double
mut m2 : Double
mut minimum : Double
mut maximum : Double
mut last : Double
} derive(FromJson, ToJson, Debug, Eq)
///|
/// Create an empty streaming accumulator.
pub fn OnlineStats::new() -> OnlineStats {
{ count: 0, mean: 0.0, m2: 0.0, minimum: 0.0, maximum: 0.0, last: 0.0 }
}
///|
/// Add one observation using Welford's stable update.
pub fn OnlineStats::push(self : OnlineStats, value : Double) -> Unit {
if self.count == 0 {
self.count = 1
self.mean = value
self.minimum = value
self.maximum = value
self.last = value
} else {
self.count += 1
let delta = value - self.mean
self.mean += delta / self.count.to_double()
let delta2 = value - self.mean
self.m2 += delta * delta2
if value < self.minimum {
self.minimum = value
}
if value > self.maximum {
self.maximum = value
}
self.last = value
}
}
///|
/// Add all observations in order.
pub fn OnlineStats::push_all(
self : OnlineStats,
values : Array[Double],
) -> Unit {
for value in values {
self.push(value)
}
}
///|
/// Return sample variance from an online accumulator.
pub fn OnlineStats::variance(self : OnlineStats) -> Double {
if self.count <= 1 {
0.0
} else {
self.m2 / (self.count - 1).to_double()
}
}
///|
/// Return sample standard deviation from an online accumulator.
pub fn OnlineStats::standard_deviation(self : OnlineStats) -> Double {
self.variance().sqrt()
}
///|
/// Merge another accumulator using Chan's parallel moments formula.
pub fn OnlineStats::merge(self : OnlineStats, other : OnlineStats) -> Unit {
if other.count == 0 {
return
}
if self.count == 0 {
self.count = other.count
self.mean = other.mean
self.m2 = other.m2
self.minimum = other.minimum
self.maximum = other.maximum
self.last = other.last
return
}
let total = self.count + other.count
let delta = other.mean - self.mean
self.m2 += other.m2 +
delta *
delta *
self.count.to_double() *
other.count.to_double() /
total.to_double()
self.mean += delta * other.count.to_double() / total.to_double()
self.count = total
if other.minimum < self.minimum {
self.minimum = other.minimum
}
if other.maximum > self.maximum {
self.maximum = other.maximum
}
self.last = other.last
}
///|
/// Return an immutable snapshot of the current accumulator.
pub fn OnlineStats::snapshot(self : OnlineStats) -> DistributionStats {
{
count: self.count,
sum: self.mean * self.count.to_double(),
mean: self.mean,
median: self.mean,
variance: self.variance(),
standard_deviation: self.standard_deviation(),
minimum: self.minimum,
maximum: self.maximum,
q1: self.minimum,
q3: self.maximum,
interquartile_range: self.maximum - self.minimum,
median_absolute_deviation: 0.0,
}
}
///|
/// Calculate an exponentially weighted moving average.
pub fn exponential_moving_average(
values : Array[Double],
alpha : Double,
) -> Array[Double] {
let result = []
if values.length() == 0 {
return result
}
let weight = alpha.clamp(min=0.0, max=1.0)
let mut current = values[0]
result.push(current)
for i in 1.. Array[Double] {
let result = []
if values.length() == 0 {
return result
}
let weight = alpha.clamp(min=0.0, max=1.0)
let mut baseline = values[0]
let mut variance = 0.0
result.push(0.0)
for i in 1.. Double {
let mut last = 0.0
for value in values {
if !value.is_nan() && !value.is_inf() {
last = value
}
}
last
}