///|
/// Online reliability and uncertainty calibration utilities.
///
/// Calibrated probabilities and conformal-style intervals are useful at the
/// final serving boundary: they turn a raw score into an operational signal
/// with explicit coverage, confidence, and alerting statistics.
pub struct ReliabilityBin {
lower : Double
upper : Double
mut count : Int
mut predicted_sum : Double
mut observed_sum : Double
mut squared_error : Double
}
///|
pub fn ReliabilityBin::new(lower : Double, upper : Double) -> ReliabilityBin {
{
lower,
upper,
count: 0,
predicted_sum: 0.0,
observed_sum: 0.0,
squared_error: 0.0,
}
}
///|
pub fn ReliabilityBin::lower(self : ReliabilityBin) -> Double {
self.lower
}
///|
pub fn ReliabilityBin::upper(self : ReliabilityBin) -> Double {
self.upper
}
///|
pub fn ReliabilityBin::count(self : ReliabilityBin) -> Int {
self.count
}
///|
pub fn ReliabilityBin::mean_prediction(self : ReliabilityBin) -> Double {
if self.count == 0 {
0.0
} else {
self.predicted_sum / self.count.to_double()
}
}
///|
pub fn ReliabilityBin::empirical_rate(self : ReliabilityBin) -> Double {
if self.count == 0 {
0.0
} else {
self.observed_sum / self.count.to_double()
}
}
///|
pub fn ReliabilityBin::error(self : ReliabilityBin) -> Double {
(self.mean_prediction() - self.empirical_rate()).abs()
}
///|
pub fn ReliabilityBin::brier(self : ReliabilityBin) -> Double {
if self.count == 0 {
0.0
} else {
self.squared_error / self.count.to_double()
}
}
///|
pub fn ReliabilityBin::observe(
self : ReliabilityBin,
probability : Double,
label : Double,
) -> Unit {
self.count += 1
self.predicted_sum += probability
self.observed_sum += label
let difference = probability - label
self.squared_error += difference * difference
}
///|
pub fn ReliabilityBin::reset(self : ReliabilityBin) -> Unit {
self.count = 0
self.predicted_sum = 0.0
self.observed_sum = 0.0
self.squared_error = 0.0
}
///|
pub struct OnlineReliabilityMonitor {
bins : Array[ReliabilityBin]
mut observations : Int
mut positives : Int
mut log_loss : Double
}
///|
pub fn OnlineReliabilityMonitor::new(
bin_count? : Int = 10,
) -> OnlineReliabilityMonitor {
let count = if bin_count < 1 { 1 } else { bin_count }
{
bins: Array::makei(count, i => {
ReliabilityBin::new(
i.to_double() / count.to_double(),
(i + 1).to_double() / count.to_double(),
)
}),
observations: 0,
positives: 0,
log_loss: 0.0,
}
}
///|
fn reliability_index(
monitor : OnlineReliabilityMonitor,
probability : Double,
) -> Int {
let value = clamp_probability(probability)
let index = (value * monitor.bins.length().to_double()).to_int()
index.min(monitor.bins.length() - 1).max(0)
}
///|
pub fn OnlineReliabilityMonitor::observe(
self : OnlineReliabilityMonitor,
probability : Double,
label : Double,
) -> Bool {
if probability.is_nan() ||
probability.is_inf() ||
label.is_nan() ||
label.is_inf() {
false
} else {
let score = clamp_probability(probability)
let target = clamp(label, 0.0, 1.0)
self.bins[reliability_index(self, score)].observe(score, target)
self.observations += 1
if target >= 0.5 {
self.positives += 1
}
let safe = clamp(score, 1.0e-12, 1.0 - 1.0e-12)
self.log_loss += -(target * @math.ln(safe) +
(1.0 - target) * @math.ln(1.0 - safe))
true
}
}
///|
pub fn OnlineReliabilityMonitor::bins(
self : OnlineReliabilityMonitor,
) -> Array[ReliabilityBin] {
self.bins.map(value => value)
}
///|
pub fn OnlineReliabilityMonitor::observations(
self : OnlineReliabilityMonitor,
) -> Int {
self.observations
}
///|
pub fn OnlineReliabilityMonitor::positive_rate(
self : OnlineReliabilityMonitor,
) -> Double {
if self.observations == 0 {
0.0
} else {
self.positives.to_double() / self.observations.to_double()
}
}
///|
pub fn OnlineReliabilityMonitor::log_loss(
self : OnlineReliabilityMonitor,
) -> Double {
if self.observations == 0 {
0.0
} else {
self.log_loss / self.observations.to_double()
}
}
///|
pub fn OnlineReliabilityMonitor::brier_score(
self : OnlineReliabilityMonitor,
) -> Double {
if self.observations == 0 {
0.0
} else {
let total = Ref(0.0)
for bin in self.bins {
total.val += bin.squared_error
}
total.val / self.observations.to_double()
}
}
///|
pub fn OnlineReliabilityMonitor::expected_calibration_error(
self : OnlineReliabilityMonitor,
) -> Double {
if self.observations == 0 {
0.0
} else {
let total = Ref(0.0)
for bin in self.bins {
total.val += bin.count.to_double() * bin.error()
}
total.val / self.observations.to_double()
}
}
///|
pub fn OnlineReliabilityMonitor::maximum_calibration_error(
self : OnlineReliabilityMonitor,
) -> Double {
let maximum = Ref(0.0)
for bin in self.bins {
if bin.error() > maximum.val {
maximum.val = bin.error()
}
}
maximum.val
}
///|
pub fn OnlineReliabilityMonitor::reset(self : OnlineReliabilityMonitor) -> Unit {
for bin in self.bins {
bin.reset()
}
self.observations = 0
self.positives = 0
self.log_loss = 0.0
}
///|
pub struct CalibratedInterval {
center : Double
radius : Double
confidence : Double
}
///|
pub fn CalibratedInterval::new(
center : Double,
radius : Double,
confidence : Double,
) -> CalibratedInterval {
{
center,
radius: if radius < 0.0 {
0.0
} else {
radius
},
confidence: clamp_probability(confidence),
}
}
///|
pub fn CalibratedInterval::center(self : CalibratedInterval) -> Double {
self.center
}
///|
pub fn CalibratedInterval::radius(self : CalibratedInterval) -> Double {
self.radius
}
///|
pub fn CalibratedInterval::confidence(self : CalibratedInterval) -> Double {
self.confidence
}
///|
pub fn CalibratedInterval::lower(self : CalibratedInterval) -> Double {
self.center - self.radius
}
///|
pub fn CalibratedInterval::upper(self : CalibratedInterval) -> Double {
self.center + self.radius
}
///|
pub fn CalibratedInterval::contains(
self : CalibratedInterval,
value : Double,
) -> Bool {
value >= self.lower() && value <= self.upper()
}
///|
pub struct OnlineConformalRegressor {
confidence : Double
scores : OnlineQuantileSketch
mut observations : Int
mut covered : Int
mut misses : Int
}
///|
pub fn OnlineConformalRegressor::new(
confidence? : Double = 0.9,
capacity? : Int = 512,
) -> OnlineConformalRegressor {
{
confidence: clamp(confidence, 0.0, 1.0),
scores: OnlineQuantileSketch::new(capacity~),
observations: 0,
covered: 0,
misses: 0,
}
}
///|
pub fn OnlineConformalRegressor::observe(
self : OnlineConformalRegressor,
prediction : Double,
actual : Double,
) -> Double {
let score = (actual - prediction).abs()
if !score.is_nan() && !score.is_inf() {
self.scores.update(score)
}
self.observations += 1
let interval = self.interval(prediction)
if interval.contains(actual) {
self.covered += 1
} else {
self.misses += 1
}
score
}
///|
pub fn OnlineConformalRegressor::interval(
self : OnlineConformalRegressor,
prediction : Double,
) -> CalibratedInterval {
let probability = clamp(self.confidence, 0.0, 1.0)
let radius = self.scores.quantile(probability).unwrap_or(0.0)
CalibratedInterval::new(prediction, radius, self.confidence)
}
///|
pub fn OnlineConformalRegressor::confidence(
self : OnlineConformalRegressor,
) -> Double {
self.confidence
}
///|
pub fn OnlineConformalRegressor::radius(
self : OnlineConformalRegressor,
) -> Double {
self.scores.quantile(self.confidence).unwrap_or(0.0)
}
///|
pub fn OnlineConformalRegressor::observations(
self : OnlineConformalRegressor,
) -> Int {
self.observations
}
///|
pub fn OnlineConformalRegressor::covered(
self : OnlineConformalRegressor,
) -> Int {
self.covered
}
///|
pub fn OnlineConformalRegressor::misses(self : OnlineConformalRegressor) -> Int {
self.misses
}
///|
pub fn OnlineConformalRegressor::coverage(
self : OnlineConformalRegressor,
) -> Double {
if self.observations == 0 {
0.0
} else {
self.covered.to_double() / self.observations.to_double()
}
}
///|
pub fn OnlineConformalRegressor::reset(self : OnlineConformalRegressor) -> Unit {
self.scores.reset()
self.observations = 0
self.covered = 0
self.misses = 0
}
///|
pub struct ProbabilityThresholdPolicy {
target_rate : Double
mut threshold : Double
mut observations : Int
mut positives : Int
}
///|
pub fn ProbabilityThresholdPolicy::new(
target_rate? : Double = 0.1,
) -> ProbabilityThresholdPolicy {
{
target_rate: clamp(target_rate, 0.0, 1.0),
threshold: 0.5,
observations: 0,
positives: 0,
}
}
///|
pub fn ProbabilityThresholdPolicy::observe(
self : ProbabilityThresholdPolicy,
probability : Double,
) -> Bool {
if probability.is_nan() || probability.is_inf() {
false
} else {
let value = clamp_probability(probability)
self.observations += 1
if value >= self.threshold {
self.positives += 1
}
let desired = 1.0 - self.target_rate
if self.observations >= 10 {
self.threshold = clamp(
self.threshold +
(
self.positives.to_double() / self.observations.to_double() -
self.target_rate
) *
0.05,
0.01,
0.99,
)
}
value >= desired
}
}
///|
pub fn ProbabilityThresholdPolicy::threshold(
self : ProbabilityThresholdPolicy,
) -> Double {
self.threshold
}
///|
pub fn ProbabilityThresholdPolicy::target_rate(
self : ProbabilityThresholdPolicy,
) -> Double {
self.target_rate
}
///|
pub fn ProbabilityThresholdPolicy::observations(
self : ProbabilityThresholdPolicy,
) -> Int {
self.observations
}
///|
pub fn ProbabilityThresholdPolicy::positive_rate(
self : ProbabilityThresholdPolicy,
) -> Double {
if self.observations == 0 {
0.0
} else {
self.positives.to_double() / self.observations.to_double()
}
}
///|
pub fn ProbabilityThresholdPolicy::reset(
self : ProbabilityThresholdPolicy,
) -> Unit {
self.threshold = 0.5
self.observations = 0
self.positives = 0
}