use statrs::function::gamma::ln_gamma;
use crate::error::{RegressionError, Result};
pub trait Family: Clone + std::fmt::Debug {
fn name(&self) -> &'static str;
fn validate(&self, y: &[f64]) -> Result<()>;
fn init_mu(&self, y: f64) -> f64;
fn link(&self, mu: f64) -> f64;
fn inverse_link(&self, eta: f64) -> f64;
fn dmu_deta(&self, eta: f64) -> f64;
fn variance(&self, mu: f64) -> f64;
fn unit_deviance(&self, y: f64, mu: f64) -> f64;
fn loglik(&self, y: f64, mu: f64, dispersion: f64) -> f64;
fn dispersion_known(&self) -> bool;
}
const MU_EPS: f64 = 1e-10;
#[derive(Debug, Clone, Copy, Default, PartialEq)]
pub struct Poisson;
impl Family for Poisson {
fn name(&self) -> &'static str {
"Poisson"
}
fn validate(&self, y: &[f64]) -> Result<()> {
for &v in y {
if !v.is_finite() || v < 0.0 {
return Err(RegressionError::InvalidResponse {
msg: format!("Poisson response must be a non-negative count, found {v}"),
});
}
}
Ok(())
}
fn init_mu(&self, y: f64) -> f64 {
(y + 0.1).max(MU_EPS)
}
fn link(&self, mu: f64) -> f64 {
mu.max(MU_EPS).ln()
}
fn inverse_link(&self, eta: f64) -> f64 {
eta.exp()
}
fn dmu_deta(&self, eta: f64) -> f64 {
eta.exp().max(MU_EPS)
}
fn variance(&self, mu: f64) -> f64 {
mu.max(MU_EPS)
}
fn unit_deviance(&self, y: f64, mu: f64) -> f64 {
let mu = mu.max(MU_EPS);
let term = if y > 0.0 { y * (y / mu).ln() } else { 0.0 };
(2.0 * (term - (y - mu))).max(0.0)
}
fn loglik(&self, y: f64, mu: f64, _dispersion: f64) -> f64 {
let mu = mu.max(MU_EPS);
y * mu.ln() - mu - ln_gamma(y + 1.0)
}
fn dispersion_known(&self) -> bool {
true
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq)]
pub struct Gamma;
impl Family for Gamma {
fn name(&self) -> &'static str {
"Gamma"
}
fn validate(&self, y: &[f64]) -> Result<()> {
for &v in y {
if !v.is_finite() || v <= 0.0 {
return Err(RegressionError::InvalidResponse {
msg: format!("Gamma response must be strictly positive, found {v}"),
});
}
}
Ok(())
}
fn init_mu(&self, y: f64) -> f64 {
y.max(MU_EPS)
}
fn link(&self, mu: f64) -> f64 {
mu.max(MU_EPS).ln()
}
fn inverse_link(&self, eta: f64) -> f64 {
eta.exp()
}
fn dmu_deta(&self, eta: f64) -> f64 {
eta.exp().max(MU_EPS)
}
fn variance(&self, mu: f64) -> f64 {
let mu = mu.max(MU_EPS);
mu * mu
}
fn unit_deviance(&self, y: f64, mu: f64) -> f64 {
let mu = mu.max(MU_EPS);
(2.0 * (-(y / mu).ln() + (y - mu) / mu)).max(0.0)
}
fn loglik(&self, y: f64, mu: f64, dispersion: f64) -> f64 {
let mu = mu.max(MU_EPS);
let nu = 1.0 / dispersion.max(MU_EPS);
nu * (nu / mu).ln() + (nu - 1.0) * y.ln() - nu * y / mu - ln_gamma(nu)
}
fn dispersion_known(&self) -> bool {
false
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct NegativeBinomial {
theta: f64,
}
impl NegativeBinomial {
pub fn new(theta: f64) -> Result<Self> {
if !theta.is_finite() || theta <= 0.0 {
return Err(RegressionError::InvalidParameter {
msg: format!("negative-binomial θ must be finite and > 0, got {theta}"),
});
}
Ok(Self { theta })
}
pub fn theta(&self) -> f64 {
self.theta
}
}
impl Family for NegativeBinomial {
fn name(&self) -> &'static str {
"Negative Binomial"
}
fn validate(&self, y: &[f64]) -> Result<()> {
for &v in y {
if !v.is_finite() || v < 0.0 {
return Err(RegressionError::InvalidResponse {
msg: format!(
"negative-binomial response must be a non-negative count, found {v}"
),
});
}
}
Ok(())
}
fn init_mu(&self, y: f64) -> f64 {
(y + 0.1).max(MU_EPS)
}
fn link(&self, mu: f64) -> f64 {
mu.max(MU_EPS).ln()
}
fn inverse_link(&self, eta: f64) -> f64 {
eta.exp()
}
fn dmu_deta(&self, eta: f64) -> f64 {
eta.exp().max(MU_EPS)
}
fn variance(&self, mu: f64) -> f64 {
let mu = mu.max(MU_EPS);
mu + mu * mu / self.theta
}
fn unit_deviance(&self, y: f64, mu: f64) -> f64 {
let mu = mu.max(MU_EPS);
let theta = self.theta;
let term1 = if y > 0.0 { y * (y / mu).ln() } else { 0.0 };
let term2 = (y + theta) * ((y + theta) / (mu + theta)).ln();
(2.0 * (term1 - term2)).max(0.0)
}
fn loglik(&self, y: f64, mu: f64, _dispersion: f64) -> f64 {
let mu = mu.max(MU_EPS);
let theta = self.theta;
let y_term = if y > 0.0 { y * (mu / (mu + theta)).ln() } else { 0.0 };
ln_gamma(y + theta) - ln_gamma(theta) - ln_gamma(y + 1.0)
+ theta * (theta / (theta + mu)).ln()
+ y_term
}
fn dispersion_known(&self) -> bool {
true
}
}