pub const MILDLY_LOW_VALUE: f64 = 1e-8;
pub const POISSON_REGIME: f64 = 1e-4;
pub const GAMMA_REGIME: f64 = 1e6;
#[inline]
pub fn unit_nb_deviance(y: f64, mu: f64, phi: f64) -> f64 {
let y = y + MILDLY_LOW_VALUE;
let mu = mu + MILDLY_LOW_VALUE;
let out = if phi < POISSON_REGIME {
let resid = y - mu;
2.0 * (y * (y / mu).ln() - resid - 0.5 * resid * resid * phi * (1.0 - phi * y))
} else {
let product = mu * phi;
if product > GAMMA_REGIME {
2.0 * ((y - mu) / mu - (y / mu).ln()) * mu / (1.0 + product)
} else {
let inv_phi = 1.0 / phi;
2.0 * (y * (y / mu).ln() + (y + inv_phi) * ((mu + inv_phi) / (y + inv_phi)).ln())
}
};
out.max(0.0)
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
#[test]
fn test_unit_deviance_matches_edger() {
let cases: [(f64, f64, f64); 8] = [
(10.0, 10.0, 0.0),
(10.001, 10.0, 4.999_749_966_85e-08),
(10.1, 10.0, 0.000_497_514_489_479),
(12.0, 10.0, 0.182_069_451_405),
(0.0, 10.0, 13.862_943_200_6),
(3.0, 10.0, 3.976_518_983_98),
(100.0, 90.0, 0.103_863_574_593),
(1.0, 0.5, 0.362_854_011_037),
];
for (y, mu, expected) in cases {
let got = unit_nb_deviance(y, mu, 0.1);
if expected == 0.0 {
assert!(got.abs() < 1e-14, "y={y} mu={mu} gave {got}");
} else {
assert_relative_eq!(got, expected, max_relative = 1e-9);
}
}
}
fn naive(y: f64, mu: f64, phi: f64) -> f64 {
if y > 0.0 {
2.0 * (y * (y / mu).ln() - (y + 1.0 / phi) * ((1.0 + phi * y) / (1.0 + phi * mu)).ln())
} else {
2.0 / phi * (1.0 + phi * mu).ln()
}
}
#[test]
fn test_regime_switch_beats_the_naive_form_near_the_mean() {
let (y, mu, phi) = (10.000_001, 10.0, 0.1);
let ours = unit_nb_deviance(y, mu, phi);
let theirs = naive(y, mu, phi);
let relative = (ours - theirs).abs() / ours.abs();
assert!(
relative > 1e-6,
"expected a visible disagreement with the naive form, got {relative:e}"
);
}
#[test]
fn test_zero_count_is_finite_without_a_special_case() {
let d = unit_nb_deviance(0.0, 10.0, 0.1);
assert_relative_eq!(d, 13.862_943_200_6, max_relative = 1e-9);
assert!(d.is_finite());
}
#[test]
fn test_poisson_correction_matches_edger() {
let cases: [(f64, f64, f64, f64); 5] = [
(1e6, 1.001e6, 1e-5, 90.999_333_833),
(1e6, 1e6 + 1.0, 1e-6, 9.998_995_586_21e-7),
(100.0, 110.0, 1e-5, 0.936_965_039_047),
(5.0, 7.0, 1e-6, 0.635_273_632_793),
(0.0, 10.0, 1e-5, 19.998_999_585_5),
];
for (y, mu, phi, expected) in cases {
assert_relative_eq!(unit_nb_deviance(y, mu, phi), expected, max_relative = 1e-10);
}
}
#[test]
fn test_poisson_regime_matches_the_poisson_deviance() {
let phi = 1e-10;
for (y, mu) in [
(0.0, 5.0),
(1.0, 5.0),
(3.0, 5.0),
(5.0, 5.0),
(100.0, 90.0),
] {
let ours = unit_nb_deviance(y, mu, phi);
let yn = y + MILDLY_LOW_VALUE;
let mn = mu + MILDLY_LOW_VALUE;
let poisson = 2.0 * (yn * (yn / mn).ln() - (yn - mn));
assert_relative_eq!(ours, poisson, max_relative = 1e-8);
}
}
#[test]
fn test_gamma_regime_agrees_across_the_boundary() {
let mu = 1e7;
let below = unit_nb_deviance(1.2e7, mu, GAMMA_REGIME / mu * 0.999);
let above = unit_nb_deviance(1.2e7, mu, GAMMA_REGIME / mu * 1.001);
assert_relative_eq!(below, above, max_relative = 5e-3);
}
#[test]
fn test_deviance_is_never_negative() {
for y in [0.0, 0.5, 1.0, 10.0, 1e6] {
for mu in [1e-8, 0.5, 10.0, 1e6] {
for phi in [0.0, 1e-6, 0.01, 0.5, 10.0] {
let d = unit_nb_deviance(y, mu, phi);
assert!(d >= 0.0, "y={y} mu={mu} phi={phi} gave {d}");
assert!(d.is_finite(), "y={y} mu={mu} phi={phi} gave {d}");
}
}
}
}
}