///|
/// Conjugate Gamma prior for a Poisson segment rate.
pub struct PoissonModel {
prior_shape : Double
prior_rate : Double
}
///|
pub fn PoissonModel::new(
prior_shape : Double,
prior_rate : Double,
) -> Result[PoissonModel, DetectError] {
if !finite(prior_shape) || prior_shape <= 0.0 {
return Err(InvalidPriorStrength)
}
if !finite(prior_rate) || prior_rate <= 0.0 {
return Err(InvalidVariance)
}
Ok({ prior_shape, prior_rate, })
}
///|
/// Bounded BOCPD configuration for non-negative integer observations.
pub struct PoissonConfig {
hazard : Double
hazard_log : Double
survival_log : Double
max_run_length : Int
model : PoissonModel
}
///|
pub fn PoissonConfig::new(
hazard : Double,
max_run_length : Int,
model : PoissonModel,
) -> Result[PoissonConfig, DetectError] {
if !finite(hazard) || hazard <= 0.0 || hazard >= 1.0 {
return Err(InvalidHazard)
}
if max_run_length < 2 {
return Err(InvalidRunLengthLimit)
}
Ok({
hazard,
hazard_log: @math.ln(hazard),
survival_log: @math.ln(1.0 - hazard),
max_run_length,
model,
})
}
///|
/// Posterior result for one count observation.
pub struct CountUpdate {
step : Int
count : Int
change_probability : Double
most_likely_run_length : Int
posterior_rate : Double
}
///|
pub fn CountUpdate::step(self : CountUpdate) -> Int {
self.step
}
///|
pub fn CountUpdate::change_probability(self : CountUpdate) -> Double {
self.change_probability
}
///|
pub fn CountUpdate::most_likely_run_length(self : CountUpdate) -> Int {
self.most_likely_run_length
}
///|
pub fn CountUpdate::posterior_rate(self : CountUpdate) -> Double {
self.posterior_rate
}
///|
/// Separate detector avoids making numeric and count-model callers pay for an enum dispatch.
pub struct PoissonDetector {
config : PoissonConfig
mut step : Int
mut log_posterior : Array[Double]
mut totals : Array[Int]
mut observations : Array[Int]
}
///|
pub fn PoissonDetector::new(config : PoissonConfig) -> PoissonDetector {
{ config, step: 0, log_posterior: [0.0], totals: [0], observations: [0], }
}
///|
pub fn PoissonDetector::steps(self : PoissonDetector) -> Int {
self.step
}
///|
pub fn PoissonDetector::run_length_posterior(
self : PoissonDetector,
) -> Array[Double] {
self.log_posterior.map(fn(value) { @math.exp(value) })
}
///|
/// Gamma-Poisson posterior predictive probability in log form.
fn PoissonDetector::predictive(
self : PoissonDetector,
total : Int,
observations : Int,
value : Int,
value_log_factorial : Double,
) -> Double {
let model = self.config.model
let shape = model.prior_shape + total.to_double()
let rate = model.prior_rate + observations.to_double()
let next_rate = rate + 1.0
let numerator = match log_gamma(shape + value.to_double()) {
Ok(result) => result
Err(_) => negative_infinity
}
let denominator = match log_gamma(shape) {
Ok(result) => result
Err(_) => negative_infinity
}
numerator -
denominator -
value_log_factorial +
shape * @math.ln(rate / next_rate) +
value.to_double() * @math.ln(1.0 / next_rate)
}
///|
/// Update with a non-negative count. The change branch always predicts from the prior.
pub fn PoissonDetector::update(
self : PoissonDetector,
value : Int,
) -> Result[CountUpdate, DetectError] {
if value < 0 {
return Err(InvalidRunLengthLimit)
}
let old_size = self.log_posterior.length()
let new_size = if old_size < self.config.max_run_length {
old_size + 1
} else {
old_size
}
let next_log = Array::make(new_size, negative_infinity)
let next_totals = Array::make(new_size, 0)
let next_observations = Array::make(new_size, 0)
let value_log_factorial = match log_factorial(value) {
Ok(result) => result
Err(error) => return Err(error)
}
let reset_predictive = self.predictive(0, 0, value, value_log_factorial)
// The posterior is normalized after every update, so this is the full
// reset branch without repeatedly reducing the same probability mass.
let changed = self.config.hazard_log + reset_predictive
for run in 0.. Result[Array[CountUpdate], DetectError] {
let result : Array[CountUpdate] = []
for value in values {
match self.update(value) {
Ok(update) => result.push(update)
Err(error) => return Err(error)
}
}
Ok(result)
}