///|
/// An observation that can be replayed by a conjugate model ledger.
pub(all) enum Observation {
Binary(successes~ : Int, trials~ : Int)
Counts(sum~ : Int, observations~ : Int)
Continuous(mean~ : Double, observations~ : Int)
Categorical(index~ : Int)
} derive(Debug, Eq, ToJson, FromJson)
///|
/// A deterministic audit trail for online updates. It keeps immutable snapshots
/// so callers can compare revisions or replay a production data window.
pub struct UpdateLedger {
observations : Array[Observation]
binary : BetaBinomial
counts : GammaPoisson
continuous : NormalNormal
categorical : DirichletMultinomial
} derive(Debug, Eq, ToJson, FromJson)
///|
pub fn UpdateLedger::new(
binary : BetaBinomial,
counts : GammaPoisson,
continuous : NormalNormal,
categorical : DirichletMultinomial,
) -> UpdateLedger {
{ observations: [], binary, counts, continuous, categorical }
}
///|
pub fn UpdateLedger::push(
self : UpdateLedger,
observation : Observation,
) -> UpdateLedger {
let next = match observation {
Binary(successes~, trials~) =>
{ ..self, binary: self.binary.update(successes, trials) }
Counts(sum~, observations~) =>
{ ..self, counts: self.counts.update(sum, observations) }
Continuous(mean~, observations~) =>
{ ..self, continuous: self.continuous.update(mean, observations) }
Categorical(index~) =>
{ ..self, categorical: self.categorical.update_one(index) }
}
let observations = self.observations.copy()
observations.push(observation)
{ ..next, observations, }
}
///|
pub fn UpdateLedger::push_all(
self : UpdateLedger,
observations : Array[Observation],
) -> UpdateLedger {
observations.fold(init=self, (ledger, observation) => ledger.push(observation))
}
///|
pub fn UpdateLedger::len(self : UpdateLedger) -> Int {
self.observations.length()
}
///|
pub fn UpdateLedger::observations(self : UpdateLedger) -> Array[Observation] {
self.observations.copy()
}
///|
pub fn UpdateLedger::binary(self : UpdateLedger) -> BetaBinomial {
self.binary
}
///|
pub fn UpdateLedger::counts(self : UpdateLedger) -> GammaPoisson {
self.counts
}
///|
pub fn UpdateLedger::continuous(self : UpdateLedger) -> NormalNormal {
self.continuous
}
///|
pub fn UpdateLedger::categorical(self : UpdateLedger) -> DirichletMultinomial {
self.categorical
}
///|
pub struct BinaryBatch {
successes : Int
trials : Int
} derive(Debug, Eq, ToJson, FromJson)
///|
pub fn BinaryBatch::new(successes : Int, trials : Int) -> BinaryBatch {
if successes < 0 || trials < 0 || successes > trials {
abort("BinaryBatch: invalid counts")
}
{ successes, trials }
}
///|
pub fn BinaryBatch::success_rate(self : BinaryBatch) -> Double {
if self.trials == 0 {
0.0
} else {
self.successes.to_double() / self.trials.to_double()
}
}
///|
pub fn BinaryBatch::combine(
self : BinaryBatch,
other : BinaryBatch,
) -> BinaryBatch {
BinaryBatch::new(self.successes + other.successes, self.trials + other.trials)
}
///|
pub struct CountBatch {
total : Int
observations : Int
} derive(Debug, Eq, ToJson, FromJson)
///|
pub fn CountBatch::new(total : Int, observations : Int) -> CountBatch {
if total < 0 || observations < 0 {
abort("CountBatch: values must be non-negative")
}
{ total, observations }
}
///|
pub fn CountBatch::rate(self : CountBatch) -> Double {
if self.observations == 0 {
0.0
} else {
self.total.to_double() / self.observations.to_double()
}
}
///|
pub fn CountBatch::combine(self : CountBatch, other : CountBatch) -> CountBatch {
CountBatch::new(
self.total + other.total,
self.observations + other.observations,
)
}