regression_diagnostics/mixed/
general.rs1use nalgebra::{DMatrix, DVector};
2use ndarray::{Array1, Array2, ArrayView1, ArrayView2};
3
4use super::Method;
5use crate::error::{RegressionError, Result};
6use crate::linalg::{dmatrix_from_rows, dvector_from_slice};
7use crate::optimize::nelder_mead;
8
9#[derive(Debug, Clone)]
21pub struct RandomEffect {
22 groups: Vec<usize>,
23 z: Array2<f64>,
24}
25
26impl RandomEffect {
27 pub fn intercept(groups: &[usize]) -> Self {
30 let z = Array2::<f64>::ones((groups.len(), 1));
31 Self {
32 groups: groups.to_vec(),
33 z,
34 }
35 }
36
37 pub fn new(groups: &[usize], z: Array2<f64>) -> Self {
41 Self {
42 groups: groups.to_vec(),
43 z,
44 }
45 }
46}
47
48struct TermLayout {
50 group_of: Vec<usize>,
52 z: Array2<f64>,
54 k: usize,
55 n_groups: usize,
56 offset: usize,
58 param_offset: usize,
60}
61
62#[derive(Debug, Clone)]
82pub struct MixedModel {
83 coefficients: Array1<f64>,
84 cov_beta: Array2<f64>,
85 var_residual: f64,
86 term_cov: Vec<Array2<f64>>,
88 term_blups: Vec<Array2<f64>>,
90 log_likelihood: f64,
91 method: Method,
92 n: usize,
93 p: usize,
94 q: usize,
95}
96
97impl MixedModel {
98 pub fn new(x: Array2<f64>, y: Array1<f64>, terms: Vec<RandomEffect>) -> Result<Self> {
108 Self::with_method(x, y, terms, Method::Reml)
109 }
110
111 pub fn with_method(
113 x: Array2<f64>,
114 y: Array1<f64>,
115 terms: Vec<RandomEffect>,
116 method: Method,
117 ) -> Result<Self> {
118 let n = x.nrows();
119 let p = x.ncols();
120 if n == 0 || p == 0 {
121 return Err(RegressionError::EmptyInput { what: "X" });
122 }
123 if y.len() != n {
124 return Err(RegressionError::ShapeMismatch {
125 what: "y length vs X rows",
126 expected: n,
127 got: y.len(),
128 });
129 }
130 if n <= p {
131 return Err(RegressionError::NoResidualDegreesOfFreedom {
132 n,
133 p,
134 df: n as isize - p as isize,
135 });
136 }
137 if terms.is_empty() {
138 return Err(RegressionError::InvalidResponse {
139 msg: "a mixed model needs at least one random-effect term".into(),
140 });
141 }
142
143 let mut layouts = Vec::with_capacity(terms.len());
145 let mut q = 0usize;
146 let mut n_theta = 0usize;
147 for term in &terms {
148 if term.groups.len() != n || term.z.nrows() != n {
149 return Err(RegressionError::ShapeMismatch {
150 what: "random-effect term length vs X rows",
151 expected: n,
152 got: term.groups.len().min(term.z.nrows()),
153 });
154 }
155 let group_of = densify(&term.groups);
156 let n_groups = group_of.iter().copied().max().map_or(0, |m| m + 1);
157 let k = term.z.ncols();
158 let n_params = k * (k + 1) / 2;
159 layouts.push(TermLayout {
160 group_of,
161 z: term.z.clone(),
162 k,
163 n_groups,
164 offset: q,
165 param_offset: n_theta,
166 });
167 q += n_groups * k;
168 n_theta += n_params;
169 }
170
171 let mut z_full = DMatrix::<f64>::zeros(n, q);
174 for lay in &layouts {
175 for i in 0..n {
176 let g = lay.group_of[i];
177 for c in 0..lay.k {
178 z_full[(i, lay.offset + g * lay.k + c)] = lay.z[(i, c)];
179 }
180 }
181 }
182
183 let xd = dmatrix_from_rows(n, p, x.as_standard_layout().as_slice().unwrap());
184 let yd = dvector_from_slice(y.as_standard_layout().as_slice().unwrap());
185
186 let ctx = Ctx {
187 x: &xd,
188 y: &yd,
189 z: &z_full,
190 layouts: &layouts,
191 n,
192 p,
193 q,
194 method,
195 };
196
197 let mut theta0 = vec![0.0; n_theta];
200 for lay in &layouts {
201 let mut idx = lay.param_offset;
203 for r in 0..lay.k {
204 for c in 0..=r {
205 theta0[idx] = if r == c { 1.0 } else { 0.0 };
206 idx += 1;
207 }
208 }
209 }
210 let obj = |t: &[f64]| ctx.objective(t).unwrap_or(f64::INFINITY);
211 let theta = nelder_mead(obj, &theta0, 0.2, 1e-10, 5000);
212
213 let sol = ctx.solve(&theta)?;
215 let dof = match method {
216 Method::Reml => (n - p) as f64,
217 Method::Ml => n as f64,
218 };
219 let var_residual = sol.rmr / dof;
220
221 let coefficients = Array1::from_shape_fn(p, |j| sol.beta[j]);
222 let cov_beta = Array2::from_shape_fn((p, p), |(i, j)| sol.a_inv[(i, j)] * var_residual);
223
224 let b = &sol.d * ctx.z.transpose() * &sol.minv_r; let mut term_cov = Vec::with_capacity(layouts.len());
227 let mut term_blups = Vec::with_capacity(layouts.len());
228 for lay in &layouts {
229 let delta = relative_covariance(&theta, lay);
230 let sigma = Array2::from_shape_fn((lay.k, lay.k), |(i, j)| delta[(i, j)] * var_residual);
231 term_cov.push(sigma);
232 let blup = Array2::from_shape_fn((lay.n_groups, lay.k), |(g, c)| {
233 b[lay.offset + g * lay.k + c]
234 });
235 term_blups.push(blup);
236 }
237
238 let log_likelihood = -0.5 * ctx.objective(&theta)? - ctx.log_const();
239
240 Ok(Self {
241 coefficients,
242 cov_beta,
243 var_residual,
244 term_cov,
245 term_blups,
246 log_likelihood,
247 method,
248 n,
249 p,
250 q,
251 })
252 }
253
254 pub fn n_observations(&self) -> usize {
256 self.n
257 }
258
259 pub fn n_parameters(&self) -> usize {
261 self.p
262 }
263
264 pub fn n_random_effects(&self) -> usize {
266 self.q
267 }
268
269 pub fn n_terms(&self) -> usize {
271 self.term_cov.len()
272 }
273
274 pub fn method(&self) -> Method {
276 self.method
277 }
278
279 pub fn coefficients(&self) -> ArrayView1<'_, f64> {
281 self.coefficients.view()
282 }
283
284 pub fn covariance(&self) -> ArrayView2<'_, f64> {
286 self.cov_beta.view()
287 }
288
289 pub fn coefficient_standard_errors(&self) -> Array1<f64> {
291 Array1::from_shape_fn(self.p, |j| self.cov_beta[(j, j)].max(0.0).sqrt())
292 }
293
294 pub fn residual_variance(&self) -> f64 {
296 self.var_residual
297 }
298
299 pub fn term_covariance(&self, t: usize) -> ArrayView2<'_, f64> {
303 self.term_cov[t].view()
304 }
305
306 pub fn random_effects(&self, t: usize) -> ArrayView2<'_, f64> {
309 self.term_blups[t].view()
310 }
311
312 pub fn log_likelihood(&self) -> f64 {
314 self.log_likelihood
315 }
316
317 pub fn aic(&self) -> f64 {
321 let n_cov: usize = self.term_cov.iter().map(|c| {
322 let k = c.nrows();
323 k * (k + 1) / 2
324 }).sum();
325 let k = match self.method {
326 Method::Ml => self.p as f64 + n_cov as f64 + 1.0,
327 Method::Reml => n_cov as f64 + 1.0,
328 };
329 -2.0 * self.log_likelihood + 2.0 * k
330 }
331}
332
333struct Ctx<'a> {
335 x: &'a DMatrix<f64>,
336 y: &'a DVector<f64>,
337 z: &'a DMatrix<f64>,
338 layouts: &'a [TermLayout],
339 n: usize,
340 p: usize,
341 q: usize,
342 method: Method,
343}
344
345struct GlsSolve {
346 beta: DVector<f64>,
347 a_inv: DMatrix<f64>,
348 rmr: f64,
349 minv_r: DVector<f64>,
351 d: DMatrix<f64>,
353}
354
355impl Ctx<'_> {
356 fn solve(&self, theta: &[f64]) -> Result<GlsSolve> {
358 let mut d = DMatrix::<f64>::zeros(self.q, self.q);
360 for lay in self.layouts {
361 let delta = relative_covariance(theta, lay);
362 for g in 0..lay.n_groups {
363 let base = lay.offset + g * lay.k;
364 for a in 0..lay.k {
365 for b in 0..lay.k {
366 d[(base + a, base + b)] = delta[(a, b)];
367 }
368 }
369 }
370 }
371 let mut m = self.z * &d * self.z.transpose();
373 for i in 0..self.n {
374 m[(i, i)] += 1.0;
375 }
376 let chol = m.clone().cholesky().ok_or(RegressionError::RankDeficient)?;
377 let minv = chol.inverse();
378
379 let xtminv = self.x.transpose() * &minv; let a = &xtminv * self.x; let a_inv = a.try_inverse().ok_or(RegressionError::RankDeficient)?;
382 let beta = &a_inv * (&xtminv * self.y);
383 let r = self.y - self.x * β
384 let minv_r = &minv * &r;
385 let rmr = (r.transpose() * &minv_r)[(0, 0)];
386
387 Ok(GlsSolve {
388 beta,
389 a_inv,
390 rmr,
391 minv_r,
392 d,
393 })
394 }
395
396 fn objective(&self, theta: &[f64]) -> Result<f64> {
398 let mut d = DMatrix::<f64>::zeros(self.q, self.q);
400 for lay in self.layouts {
401 let delta = relative_covariance(theta, lay);
402 for g in 0..lay.n_groups {
403 let base = lay.offset + g * lay.k;
404 for a in 0..lay.k {
405 for b in 0..lay.k {
406 d[(base + a, base + b)] = delta[(a, b)];
407 }
408 }
409 }
410 }
411 let mut m = self.z * &d * self.z.transpose();
412 for i in 0..self.n {
413 m[(i, i)] += 1.0;
414 }
415 let chol = m.cholesky().ok_or(RegressionError::RankDeficient)?;
416 let ln_det_m = 2.0 * chol.l().diagonal().iter().map(|v| v.ln()).sum::<f64>();
417
418 let sol = self.solve(theta)?;
419 let dof = match self.method {
420 Method::Reml => (self.n - self.p) as f64,
421 Method::Ml => self.n as f64,
422 };
423 let sigma2 = (sol.rmr / dof).max(1e-300);
424 let mut obj = dof * sigma2.ln() + ln_det_m;
425 if self.method == Method::Reml {
426 let det_ainv = sol.a_inv.determinant();
428 obj -= det_ainv.abs().max(1e-300).ln();
429 }
430 Ok(obj)
431 }
432
433 fn log_const(&self) -> f64 {
434 let dof = match self.method {
435 Method::Reml => (self.n - self.p) as f64,
436 Method::Ml => self.n as f64,
437 };
438 0.5 * dof * ((2.0 * std::f64::consts::PI).ln() + 1.0)
439 }
440}
441
442fn relative_covariance(theta: &[f64], lay: &TermLayout) -> DMatrix<f64> {
445 let k = lay.k;
446 let mut l = DMatrix::<f64>::zeros(k, k);
447 let mut idx = lay.param_offset;
448 for r in 0..k {
449 for c in 0..=r {
450 l[(r, c)] = theta[idx];
451 idx += 1;
452 }
453 }
454 &l * l.transpose()
455}
456
457fn densify(labels: &[usize]) -> Vec<usize> {
459 let mut map = std::collections::BTreeMap::new();
460 labels
461 .iter()
462 .map(|&l| {
463 let next = map.len();
464 *map.entry(l).or_insert(next)
465 })
466 .collect()
467}