use ndarray::{Array1, Array2, ArrayView1};
use statrs::distribution::{ContinuousCDF, Normal};
use statrs::function::gamma::ln_gamma;
use crate::error::{RegressionError, Result};
use crate::linalg::dmatrix_from_rows;
use crate::optimize::{nelder_mead, numerical_hessian};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GlmmFamily {
Poisson,
Binomial,
}
impl GlmmFamily {
fn inverse_link(self, eta: f64) -> f64 {
match self {
GlmmFamily::Poisson => eta.exp(),
GlmmFamily::Binomial => {
if eta >= 0.0 {
1.0 / (1.0 + (-eta).exp())
} else {
let e = eta.exp();
e / (1.0 + e)
}
}
}
}
fn loglik(self, y: f64, eta: f64) -> f64 {
match self {
GlmmFamily::Poisson => y * eta - eta.exp() - ln_gamma(y + 1.0),
GlmmFamily::Binomial => {
let lse = if eta > 0.0 {
eta + (-eta).exp().ln_1p()
} else {
eta.exp().ln_1p()
};
y * eta - lse
}
}
}
fn weight(self, mu: f64) -> f64 {
match self {
GlmmFamily::Poisson => mu,
GlmmFamily::Binomial => mu * (1.0 - mu),
}
}
fn validate(self, y: &Array1<f64>) -> Result<()> {
match self {
GlmmFamily::Poisson => {
for &v in y.iter() {
if !v.is_finite() || v < 0.0 {
return Err(RegressionError::InvalidResponse {
msg: format!("Poisson GLMM response must be a non-negative count, found {v}"),
});
}
}
}
GlmmFamily::Binomial => {
for &v in y.iter() {
if v != 0.0 && v != 1.0 {
return Err(RegressionError::InvalidResponse {
msg: format!("binomial GLMM response must be 0 or 1, found {v}"),
});
}
}
}
}
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct GlmmFit {
family: GlmmFamily,
coefficients: Array1<f64>,
sigma_b: f64,
cov_beta: Array2<f64>,
blups: Array1<f64>,
log_likelihood: f64,
n: usize,
p: usize,
n_groups: usize,
}
impl GlmmFit {
pub fn new(
x: Array2<f64>,
y: Array1<f64>,
groups: &[usize],
family: GlmmFamily,
) -> Result<Self> {
let n = x.nrows();
let p = x.ncols();
if n == 0 || p == 0 {
return Err(RegressionError::EmptyInput { what: "X" });
}
if y.len() != n || groups.len() != n {
return Err(RegressionError::ShapeMismatch {
what: "y/groups length vs X rows",
expected: n,
got: y.len().min(groups.len()),
});
}
family.validate(&y)?;
let mut map = std::collections::BTreeMap::new();
for &g in groups {
let next = map.len();
map.entry(g).or_insert(next);
}
let n_groups = map.len();
if n_groups < 2 {
return Err(RegressionError::InvalidResponse {
msg: "a GLMM needs at least two groups".into(),
});
}
let mut group_rows: Vec<Vec<usize>> = vec![Vec::new(); n_groups];
for (i, &lab) in groups.iter().enumerate() {
group_rows[map[&lab]].push(i);
}
let neg_ll = |theta: &[f64]| -> f64 {
let sigma_b = theta[p].exp();
if !sigma_b.is_finite() || sigma_b <= 0.0 {
return f64::INFINITY;
}
match laplace_loglik(&x, &y, &group_rows, theta, sigma_b, family) {
Some((ll, _)) if ll.is_finite() => -ll,
_ => f64::INFINITY,
}
};
let mut theta0 = vec![0.0; p + 1];
let ybar = y.sum() / n as f64;
theta0[0] = match family {
GlmmFamily::Poisson => ybar.max(1e-3).ln(),
GlmmFamily::Binomial => (ybar.clamp(1e-3, 1.0 - 1e-3)
/ (1.0 - ybar.clamp(1e-3, 1.0 - 1e-3)))
.ln(),
};
theta0[p] = 0.5_f64.ln();
let neg_ll_ref = &neg_ll;
let theta = nelder_mead(neg_ll_ref, &theta0, 0.2, 1e-9, 8000);
let sigma_b = theta[p].exp();
let (ll, blups_vec) =
laplace_loglik(&x, &y, &group_rows, &theta, sigma_b, family).ok_or(
RegressionError::NotConverged {
iterations: 8000,
msg: "GLMM Laplace optimizer failed to find a finite optimum".into(),
},
)?;
let grad = |t: &[f64]| -> Vec<f64> {
let mut g = vec![0.0; p + 1];
for j in 0..=p {
let h = 1e-5 * t[j].abs().max(1.0);
let mut tp = t.to_vec();
let mut tm = t.to_vec();
tp[j] += h;
tm[j] -= h;
g[j] = (neg_ll(&tp) - neg_ll(&tm)) / (2.0 * h);
}
g
};
let hess = numerical_hessian(grad, &theta);
let flat: Vec<f64> = hess.iter().flat_map(|r| r.iter().copied()).collect();
let cov_beta = match dmatrix_from_rows(p + 1, p + 1, &flat).try_inverse() {
Some(inv) => Array2::from_shape_fn((p, p), |(i, j)| inv[(i, j)]),
None => Array2::from_elem((p, p), f64::NAN),
};
let coefficients = Array1::from_shape_fn(p, |j| theta[j]);
let blups = Array1::from(blups_vec);
Ok(Self {
family,
coefficients,
sigma_b,
cov_beta,
blups,
log_likelihood: ll,
n,
p,
n_groups,
})
}
pub fn family(&self) -> GlmmFamily {
self.family
}
pub fn n_observations(&self) -> usize {
self.n
}
pub fn n_parameters(&self) -> usize {
self.p
}
pub fn n_groups(&self) -> usize {
self.n_groups
}
pub fn coefficients(&self) -> ArrayView1<'_, f64> {
self.coefficients.view()
}
pub fn sigma_b(&self) -> f64 {
self.sigma_b
}
pub fn group_variance(&self) -> f64 {
self.sigma_b * self.sigma_b
}
pub fn covariance(&self) -> ndarray::ArrayView2<'_, f64> {
self.cov_beta.view()
}
pub fn coefficient_standard_errors(&self) -> Array1<f64> {
Array1::from_shape_fn(self.p, |j| self.cov_beta[(j, j)].max(0.0).sqrt())
}
pub fn z_values(&self) -> Array1<f64> {
let se = self.coefficient_standard_errors();
Array1::from_shape_fn(self.p, |j| {
if se[j] > 0.0 {
self.coefficients[j] / se[j]
} else {
f64::NAN
}
})
}
pub fn p_values(&self) -> Array1<f64> {
let z = self.z_values();
let normal = Normal::new(0.0, 1.0).expect("standard normal");
Array1::from_shape_fn(self.p, |j| {
if z[j].is_finite() {
2.0 * (1.0 - normal.cdf(z[j].abs()))
} else {
f64::NAN
}
})
}
pub fn random_effects(&self) -> ArrayView1<'_, f64> {
self.blups.view()
}
pub fn log_likelihood(&self) -> f64 {
self.log_likelihood
}
pub fn aic(&self) -> f64 {
-2.0 * self.log_likelihood + 2.0 * (self.p as f64 + 1.0)
}
}
fn laplace_loglik(
x: &Array2<f64>,
y: &Array1<f64>,
group_rows: &[Vec<usize>],
theta: &[f64],
sigma_b: f64,
family: GlmmFamily,
) -> Option<(f64, Vec<f64>)> {
let p = x.ncols();
let s2 = sigma_b * sigma_b;
let mut total = 0.0;
let mut modes = Vec::with_capacity(group_rows.len());
for rows in group_rows {
let eta_fixed: Vec<f64> = rows
.iter()
.map(|&i| (0..p).map(|j| x[(i, j)] * theta[j]).sum::<f64>())
.collect();
let mut u = 0.0;
for _ in 0..100 {
let mut grad = -u / s2;
let mut ws = 0.0;
for (k, &i) in rows.iter().enumerate() {
let eta = eta_fixed[k] + u;
let mu = family.inverse_link(eta);
grad += y[i] - mu;
ws += family.weight(mu);
}
let h = ws + 1.0 / s2;
let step = grad / h;
u += step;
if !u.is_finite() {
return None;
}
if step.abs() < 1e-12 {
break;
}
}
let mut q = -u * u / (2.0 * s2);
let mut w_sum = 0.0;
for (k, &i) in rows.iter().enumerate() {
let eta = eta_fixed[k] + u;
q += family.loglik(y[i], eta);
w_sum += family.weight(family.inverse_link(eta));
}
let contrib = q - 0.5 * (1.0 + s2 * w_sum).ln();
if !contrib.is_finite() {
return None;
}
total += contrib;
modes.push(u);
}
Some((total, modes))
}