regression_diagnostics/mixed/
glmm.rs1use ndarray::{Array1, Array2, ArrayView1};
2use statrs::distribution::{ContinuousCDF, Normal};
3use statrs::function::gamma::ln_gamma;
4
5use crate::error::{RegressionError, Result};
6use crate::linalg::dmatrix_from_rows;
7use crate::optimize::{nelder_mead, numerical_hessian};
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum GlmmFamily {
12 Poisson,
14 Binomial,
16}
17
18impl GlmmFamily {
19 fn inverse_link(self, eta: f64) -> f64 {
20 match self {
21 GlmmFamily::Poisson => eta.exp(),
22 GlmmFamily::Binomial => {
23 if eta >= 0.0 {
24 1.0 / (1.0 + (-eta).exp())
25 } else {
26 let e = eta.exp();
27 e / (1.0 + e)
28 }
29 }
30 }
31 }
32
33 fn loglik(self, y: f64, eta: f64) -> f64 {
35 match self {
36 GlmmFamily::Poisson => y * eta - eta.exp() - ln_gamma(y + 1.0),
37 GlmmFamily::Binomial => {
38 let lse = if eta > 0.0 {
40 eta + (-eta).exp().ln_1p()
41 } else {
42 eta.exp().ln_1p()
43 };
44 y * eta - lse
45 }
46 }
47 }
48
49 fn weight(self, mu: f64) -> f64 {
52 match self {
53 GlmmFamily::Poisson => mu,
54 GlmmFamily::Binomial => mu * (1.0 - mu),
55 }
56 }
57
58 fn validate(self, y: &Array1<f64>) -> Result<()> {
59 match self {
60 GlmmFamily::Poisson => {
61 for &v in y.iter() {
62 if !v.is_finite() || v < 0.0 {
63 return Err(RegressionError::InvalidResponse {
64 msg: format!("Poisson GLMM response must be a non-negative count, found {v}"),
65 });
66 }
67 }
68 }
69 GlmmFamily::Binomial => {
70 for &v in y.iter() {
71 if v != 0.0 && v != 1.0 {
72 return Err(RegressionError::InvalidResponse {
73 msg: format!("binomial GLMM response must be 0 or 1, found {v}"),
74 });
75 }
76 }
77 }
78 }
79 Ok(())
80 }
81}
82
83#[derive(Debug, Clone)]
100pub struct GlmmFit {
101 family: GlmmFamily,
102 coefficients: Array1<f64>,
103 sigma_b: f64,
104 cov_beta: Array2<f64>,
105 blups: Array1<f64>,
106 log_likelihood: f64,
107 n: usize,
108 p: usize,
109 n_groups: usize,
110}
111
112impl GlmmFit {
113 pub fn new(
127 x: Array2<f64>,
128 y: Array1<f64>,
129 groups: &[usize],
130 family: GlmmFamily,
131 ) -> Result<Self> {
132 let n = x.nrows();
133 let p = x.ncols();
134 if n == 0 || p == 0 {
135 return Err(RegressionError::EmptyInput { what: "X" });
136 }
137 if y.len() != n || groups.len() != n {
138 return Err(RegressionError::ShapeMismatch {
139 what: "y/groups length vs X rows",
140 expected: n,
141 got: y.len().min(groups.len()),
142 });
143 }
144 family.validate(&y)?;
145
146 let mut map = std::collections::BTreeMap::new();
148 for &g in groups {
149 let next = map.len();
150 map.entry(g).or_insert(next);
151 }
152 let n_groups = map.len();
153 if n_groups < 2 {
154 return Err(RegressionError::InvalidResponse {
155 msg: "a GLMM needs at least two groups".into(),
156 });
157 }
158 let mut group_rows: Vec<Vec<usize>> = vec![Vec::new(); n_groups];
159 for (i, &lab) in groups.iter().enumerate() {
160 group_rows[map[&lab]].push(i);
161 }
162
163 let neg_ll = |theta: &[f64]| -> f64 {
165 let sigma_b = theta[p].exp();
166 if !sigma_b.is_finite() || sigma_b <= 0.0 {
167 return f64::INFINITY;
168 }
169 match laplace_loglik(&x, &y, &group_rows, theta, sigma_b, family) {
170 Some((ll, _)) if ll.is_finite() => -ll,
171 _ => f64::INFINITY,
172 }
173 };
174
175 let mut theta0 = vec![0.0; p + 1];
177 let ybar = y.sum() / n as f64;
178 theta0[0] = match family {
179 GlmmFamily::Poisson => ybar.max(1e-3).ln(),
180 GlmmFamily::Binomial => (ybar.clamp(1e-3, 1.0 - 1e-3)
181 / (1.0 - ybar.clamp(1e-3, 1.0 - 1e-3)))
182 .ln(),
183 };
184 theta0[p] = 0.5_f64.ln();
185
186 let neg_ll_ref = &neg_ll;
187 let theta = nelder_mead(neg_ll_ref, &theta0, 0.2, 1e-9, 8000);
188 let sigma_b = theta[p].exp();
189 let (ll, blups_vec) =
190 laplace_loglik(&x, &y, &group_rows, &theta, sigma_b, family).ok_or(
191 RegressionError::NotConverged {
192 iterations: 8000,
193 msg: "GLMM Laplace optimizer failed to find a finite optimum".into(),
194 },
195 )?;
196
197 let grad = |t: &[f64]| -> Vec<f64> {
199 let mut g = vec![0.0; p + 1];
200 for j in 0..=p {
201 let h = 1e-5 * t[j].abs().max(1.0);
202 let mut tp = t.to_vec();
203 let mut tm = t.to_vec();
204 tp[j] += h;
205 tm[j] -= h;
206 g[j] = (neg_ll(&tp) - neg_ll(&tm)) / (2.0 * h);
207 }
208 g
209 };
210 let hess = numerical_hessian(grad, &theta);
211 let flat: Vec<f64> = hess.iter().flat_map(|r| r.iter().copied()).collect();
212 let cov_beta = match dmatrix_from_rows(p + 1, p + 1, &flat).try_inverse() {
213 Some(inv) => Array2::from_shape_fn((p, p), |(i, j)| inv[(i, j)]),
214 None => Array2::from_elem((p, p), f64::NAN),
215 };
216
217 let coefficients = Array1::from_shape_fn(p, |j| theta[j]);
218 let blups = Array1::from(blups_vec);
219
220 Ok(Self {
221 family,
222 coefficients,
223 sigma_b,
224 cov_beta,
225 blups,
226 log_likelihood: ll,
227 n,
228 p,
229 n_groups,
230 })
231 }
232
233 pub fn family(&self) -> GlmmFamily {
235 self.family
236 }
237
238 pub fn n_observations(&self) -> usize {
240 self.n
241 }
242
243 pub fn n_parameters(&self) -> usize {
245 self.p
246 }
247
248 pub fn n_groups(&self) -> usize {
250 self.n_groups
251 }
252
253 pub fn coefficients(&self) -> ArrayView1<'_, f64> {
255 self.coefficients.view()
256 }
257
258 pub fn sigma_b(&self) -> f64 {
260 self.sigma_b
261 }
262
263 pub fn group_variance(&self) -> f64 {
265 self.sigma_b * self.sigma_b
266 }
267
268 pub fn covariance(&self) -> ndarray::ArrayView2<'_, f64> {
271 self.cov_beta.view()
272 }
273
274 pub fn coefficient_standard_errors(&self) -> Array1<f64> {
276 Array1::from_shape_fn(self.p, |j| self.cov_beta[(j, j)].max(0.0).sqrt())
277 }
278
279 pub fn z_values(&self) -> Array1<f64> {
281 let se = self.coefficient_standard_errors();
282 Array1::from_shape_fn(self.p, |j| {
283 if se[j] > 0.0 {
284 self.coefficients[j] / se[j]
285 } else {
286 f64::NAN
287 }
288 })
289 }
290
291 pub fn p_values(&self) -> Array1<f64> {
293 let z = self.z_values();
294 let normal = Normal::new(0.0, 1.0).expect("standard normal");
295 Array1::from_shape_fn(self.p, |j| {
296 if z[j].is_finite() {
297 2.0 * (1.0 - normal.cdf(z[j].abs()))
298 } else {
299 f64::NAN
300 }
301 })
302 }
303
304 pub fn random_effects(&self) -> ArrayView1<'_, f64> {
307 self.blups.view()
308 }
309
310 pub fn log_likelihood(&self) -> f64 {
312 self.log_likelihood
313 }
314
315 pub fn aic(&self) -> f64 {
317 -2.0 * self.log_likelihood + 2.0 * (self.p as f64 + 1.0)
318 }
319}
320
321fn laplace_loglik(
324 x: &Array2<f64>,
325 y: &Array1<f64>,
326 group_rows: &[Vec<usize>],
327 theta: &[f64],
328 sigma_b: f64,
329 family: GlmmFamily,
330) -> Option<(f64, Vec<f64>)> {
331 let p = x.ncols();
332 let s2 = sigma_b * sigma_b;
333 let mut total = 0.0;
334 let mut modes = Vec::with_capacity(group_rows.len());
335
336 for rows in group_rows {
337 let eta_fixed: Vec<f64> = rows
339 .iter()
340 .map(|&i| (0..p).map(|j| x[(i, j)] * theta[j]).sum::<f64>())
341 .collect();
342
343 let mut u = 0.0;
345 for _ in 0..100 {
346 let mut grad = -u / s2;
347 let mut ws = 0.0;
348 for (k, &i) in rows.iter().enumerate() {
349 let eta = eta_fixed[k] + u;
350 let mu = family.inverse_link(eta);
351 grad += y[i] - mu;
352 ws += family.weight(mu);
353 }
354 let h = ws + 1.0 / s2;
355 let step = grad / h;
356 u += step;
357 if !u.is_finite() {
358 return None;
359 }
360 if step.abs() < 1e-12 {
361 break;
362 }
363 }
364
365 let mut q = -u * u / (2.0 * s2);
367 let mut w_sum = 0.0;
368 for (k, &i) in rows.iter().enumerate() {
369 let eta = eta_fixed[k] + u;
370 q += family.loglik(y[i], eta);
371 w_sum += family.weight(family.inverse_link(eta));
372 }
373 let contrib = q - 0.5 * (1.0 + s2 * w_sum).ln();
375 if !contrib.is_finite() {
376 return None;
377 }
378 total += contrib;
379 modes.push(u);
380 }
381 Some((total, modes))
382}