regression_diagnostics/mixed/
lmm.rs1use ndarray::{Array1, Array2, ArrayView1, ArrayView2};
2
3use crate::error::{RegressionError, Result};
4use crate::linalg::dmatrix_from_rows;
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum Method {
9 Reml,
12 Ml,
15}
16
17#[derive(Debug, Clone)]
33pub struct LinearMixedModel {
34 coefficients: Array1<f64>,
35 cov_beta: Array2<f64>,
36 var_residual: f64,
37 var_group: f64,
38 lambda: f64,
39 blups: Array1<f64>,
40 log_likelihood: f64,
41 method: Method,
42 n: usize,
43 p: usize,
44 n_groups: usize,
45}
46
47impl LinearMixedModel {
48 pub fn new(x: Array2<f64>, y: Array1<f64>, groups: &[usize]) -> Result<Self> {
62 Self::with_method(x, y, groups, Method::Reml)
63 }
64
65 pub fn with_method(
67 x: Array2<f64>,
68 y: Array1<f64>,
69 groups: &[usize],
70 method: Method,
71 ) -> Result<Self> {
72 let n = x.nrows();
73 let p = x.ncols();
74 if n == 0 || p == 0 {
75 return Err(RegressionError::EmptyInput { what: "X" });
76 }
77 if y.len() != n || groups.len() != n {
78 return Err(RegressionError::ShapeMismatch {
79 what: "y/groups length vs X rows",
80 expected: n,
81 got: y.len().min(groups.len()),
82 });
83 }
84 if n <= p {
85 return Err(RegressionError::NoResidualDegreesOfFreedom {
86 n,
87 p,
88 df: n as isize - p as isize,
89 });
90 }
91
92 let mut label_to_idx = std::collections::BTreeMap::new();
94 for &g in groups {
95 let next = label_to_idx.len();
96 label_to_idx.entry(g).or_insert(next);
97 }
98 let g = label_to_idx.len();
99 if g < 2 {
100 return Err(RegressionError::InvalidResponse {
101 msg: "a mixed model needs at least two groups".into(),
102 });
103 }
104 let mut group_rows: Vec<Vec<usize>> = vec![Vec::new(); g];
105 for (i, &lab) in groups.iter().enumerate() {
106 group_rows[label_to_idx[&lab]].push(i);
107 }
108
109 let xtx = x.t().dot(&x); let xty = x.t().dot(&y); let yty: f64 = y.iter().map(|v| v * v).sum();
113 let mut s = vec![vec![0.0f64; p]; g];
115 let mut t = vec![0.0f64; g];
116 let mut sizes = vec![0usize; g];
117 for (j, rows) in group_rows.iter().enumerate() {
118 sizes[j] = rows.len();
119 for &i in rows {
120 t[j] += y[i];
121 for a in 0..p {
122 s[j][a] += x[(i, a)];
123 }
124 }
125 }
126
127 let ctx = ProfileCtx {
128 xtx: &xtx,
129 xty: &xty,
130 yty,
131 s: &s,
132 t: &t,
133 sizes: &sizes,
134 n,
135 p,
136 g,
137 method,
138 };
139
140 let phi = (5.0_f64.sqrt() - 1.0) / 2.0;
143 let (mut lo, mut hi) = (0.0_f64, 1.0 - 1e-9);
144 let mut c = hi - phi * (hi - lo);
145 let mut d = lo + phi * (hi - lo);
146 let mut fc = ctx.objective(eta_to_lambda(c))?;
147 let mut fd = ctx.objective(eta_to_lambda(d))?;
148 for _ in 0..200 {
149 if fc < fd {
150 hi = d;
151 d = c;
152 fd = fc;
153 c = hi - phi * (hi - lo);
154 fc = ctx.objective(eta_to_lambda(c))?;
155 } else {
156 lo = c;
157 c = d;
158 fc = fd;
159 d = lo + phi * (hi - lo);
160 fd = ctx.objective(eta_to_lambda(d))?;
161 }
162 if (hi - lo) < 1e-10 {
163 break;
164 }
165 }
166 let eta_hat = 0.5 * (lo + hi);
167 let lambda = eta_to_lambda(eta_hat);
168
169 let sol = ctx.solve(lambda)?;
171 let dof = match method {
172 Method::Reml => (n - p) as f64,
173 Method::Ml => n as f64,
174 };
175 let var_residual = sol.rmr / dof;
176 let var_group = lambda * var_residual;
177 let cov_beta = &sol.xtmx_inv * var_residual;
178
179 let mut blups = Array1::<f64>::zeros(g);
181 let fitted_fixed = x.dot(&sol.beta);
182 for (j, rows) in group_rows.iter().enumerate() {
183 if rows.is_empty() {
184 continue;
185 }
186 let rbar: f64 = rows.iter().map(|&i| y[i] - fitted_fixed[i]).sum::<f64>()
187 / rows.len() as f64;
188 let nj = rows.len() as f64;
189 blups[j] = (lambda * nj / (1.0 + lambda * nj)) * rbar;
190 }
191
192 let log_likelihood = -0.5 * ctx.objective(lambda)? - ctx.log_const();
193
194 Ok(Self {
195 coefficients: sol.beta,
196 cov_beta,
197 var_residual,
198 var_group,
199 lambda,
200 blups,
201 log_likelihood,
202 method,
203 n,
204 p,
205 n_groups: g,
206 })
207 }
208
209 pub fn n_observations(&self) -> usize {
211 self.n
212 }
213
214 pub fn n_parameters(&self) -> usize {
216 self.p
217 }
218
219 pub fn n_groups(&self) -> usize {
221 self.n_groups
222 }
223
224 pub fn method(&self) -> Method {
226 self.method
227 }
228
229 pub fn coefficients(&self) -> ArrayView1<'_, f64> {
231 self.coefficients.view()
232 }
233
234 pub fn covariance(&self) -> ArrayView2<'_, f64> {
236 self.cov_beta.view()
237 }
238
239 pub fn coefficient_standard_errors(&self) -> Array1<f64> {
241 Array1::from_shape_fn(self.p, |j| self.cov_beta[(j, j)].max(0.0).sqrt())
242 }
243
244 pub fn residual_variance(&self) -> f64 {
246 self.var_residual
247 }
248
249 pub fn group_variance(&self) -> f64 {
251 self.var_group
252 }
253
254 pub fn variance_ratio(&self) -> f64 {
256 self.lambda
257 }
258
259 pub fn icc(&self) -> f64 {
263 let total = self.var_group + self.var_residual;
264 if total > 0.0 {
265 self.var_group / total
266 } else {
267 f64::NAN
268 }
269 }
270
271 pub fn random_effects(&self) -> ArrayView1<'_, f64> {
276 self.blups.view()
277 }
278
279 pub fn log_likelihood(&self) -> f64 {
282 self.log_likelihood
283 }
284
285 pub fn aic(&self) -> f64 {
289 let k = match self.method {
290 Method::Ml => self.p as f64 + 2.0,
291 Method::Reml => 2.0,
292 };
293 -2.0 * self.log_likelihood + 2.0 * k
294 }
295}
296
297fn eta_to_lambda(eta: f64) -> f64 {
298 eta / (1.0 - eta)
299}
300
301struct ProfileCtx<'a> {
303 xtx: &'a Array2<f64>,
304 xty: &'a Array1<f64>,
305 yty: f64,
306 s: &'a [Vec<f64>],
307 t: &'a [f64],
308 sizes: &'a [usize],
309 n: usize,
310 p: usize,
311 g: usize,
312 method: Method,
313}
314
315struct Solve {
316 beta: Array1<f64>,
317 xtmx_inv: Array2<f64>,
318 rmr: f64,
319}
320
321impl ProfileCtx<'_> {
322 fn solve(&self, lambda: f64) -> Result<Solve> {
324 let p = self.p;
325 let mut xtmx = self.xtx.clone();
328 let mut xtmy = self.xty.clone();
329 let mut ytmy = self.yty;
330 for j in 0..self.g {
331 let nj = self.sizes[j] as f64;
332 let cj = lambda / (1.0 + lambda * nj);
333 if cj == 0.0 {
334 continue;
335 }
336 let sj = &self.s[j];
337 let tj = self.t[j];
338 for a in 0..p {
339 xtmy[a] -= cj * sj[a] * tj;
340 for b in 0..p {
341 xtmx[(a, b)] -= cj * sj[a] * sj[b];
342 }
343 }
344 ytmy -= cj * tj * tj;
345 }
346
347 let dm = dmatrix_from_rows(p, p, xtmx.as_standard_layout().as_slice().unwrap());
348 let inv = dm.try_inverse().ok_or(RegressionError::RankDeficient)?;
349 let xtmx_inv = Array2::from_shape_fn((p, p), |(i, j)| inv[(i, j)]);
350 let beta = xtmx_inv.dot(&xtmy);
351 let rmr = ytmy - beta.dot(&xtmy);
353 Ok(Solve {
354 beta,
355 xtmx_inv,
356 rmr,
357 })
358 }
359
360 fn objective(&self, lambda: f64) -> Result<f64> {
363 let sol = self.solve(lambda)?;
364 let dof = match self.method {
365 Method::Reml => (self.n - self.p) as f64,
366 Method::Ml => self.n as f64,
367 };
368 let sigma2 = (sol.rmr / dof).max(1e-300);
369 let ln_det_a: f64 = (0..self.g)
371 .map(|j| (1.0 + lambda * self.sizes[j] as f64).ln())
372 .sum();
373 let mut obj = dof * sigma2.ln() + ln_det_a;
374 if self.method == Method::Reml {
375 let p = self.p;
377 let mut xtmx = self.xtx.clone();
378 for j in 0..self.g {
379 let nj = self.sizes[j] as f64;
380 let cj = lambda / (1.0 + lambda * nj);
381 if cj == 0.0 {
382 continue;
383 }
384 let sj = &self.s[j];
385 for a in 0..p {
386 for b in 0..p {
387 xtmx[(a, b)] -= cj * sj[a] * sj[b];
388 }
389 }
390 }
391 let dm = dmatrix_from_rows(p, p, xtmx.as_standard_layout().as_slice().unwrap());
392 let det = dm.determinant();
393 obj += det.abs().max(1e-300).ln();
394 }
395 Ok(obj)
396 }
397
398 fn log_const(&self) -> f64 {
400 let dof = match self.method {
401 Method::Reml => (self.n - self.p) as f64,
402 Method::Ml => self.n as f64,
403 };
404 0.5 * dof * ((2.0 * std::f64::consts::PI).ln() + 1.0)
405 }
406}