1use ndarray::{Array1, Array2};
12use solow_core::error::{Error, Result};
13use solow_distributions::norm_sf;
14use solow_glm::{Family, Glm, Link};
15use solow_linalg::{inv, solve};
16
17#[derive(Clone, Copy, Debug, PartialEq)]
19pub enum CovStruct {
20 Independence,
24 Exchangeable,
27}
28
29#[derive(Clone, Debug)]
31pub struct Gee {
32 endog: Array1<f64>,
33 exog: Array2<f64>,
34 groups: Vec<Vec<usize>>,
37 family: Family,
38 link: Link,
39 cov_struct: CovStruct,
40 ddof_scale: f64,
43 maxiter: usize,
44 ctol: f64,
46}
47
48impl Gee {
49 pub fn new(
54 endog: Array1<f64>,
55 exog: Array2<f64>,
56 group_labels: &[i64],
57 family: Family,
58 cov_struct: CovStruct,
59 ) -> Result<Self> {
60 let link = family.default_link();
61 Self::with_link(endog, exog, group_labels, family, link, cov_struct)
62 }
63
64 pub fn with_link(
66 endog: Array1<f64>,
67 exog: Array2<f64>,
68 group_labels: &[i64],
69 family: Family,
70 link: Link,
71 cov_struct: CovStruct,
72 ) -> Result<Self> {
73 let n = endog.len();
74 if n != exog.nrows() {
75 return Err(Error::Shape("endog length != exog rows".into()));
76 }
77 if group_labels.len() != n {
78 return Err(Error::Shape("group_labels length != endog length".into()));
79 }
80 let groups = group_indices(group_labels);
81 let p = exog.ncols();
82 Ok(Gee {
83 endog,
84 exog,
85 groups,
86 family,
87 link,
88 cov_struct,
89 ddof_scale: p as f64,
90 maxiter: 300,
91 ctol: 1e-10,
92 })
93 }
94
95 pub fn maxiter(mut self, m: usize) -> Self {
97 self.maxiter = m;
98 self
99 }
100
101 pub fn ctol(mut self, t: f64) -> Self {
103 self.ctol = t;
104 self
105 }
106
107 fn nobs(&self) -> usize {
109 self.endog.len()
110 }
111
112 fn inverse_deriv(&self, eta: f64) -> f64 {
114 let mu = self.link.inverse(eta);
115 1.0 / self.link.deriv(mu)
116 }
117
118 fn cached_means(&self, params: &Array1<f64>) -> Vec<(Array1<f64>, Array1<f64>)> {
120 self.groups
121 .iter()
122 .map(|idx| {
123 let m = idx.len();
124 let mut eta = Array1::<f64>::zeros(m);
125 let mut mu = Array1::<f64>::zeros(m);
126 for (k, &i) in idx.iter().enumerate() {
127 let mut lp = 0.0;
128 for j in 0..self.exog.ncols() {
129 lp += self.exog[[i, j]] * params[j];
130 }
131 eta[k] = lp;
132 mu[k] = self.link.inverse(lp);
133 }
134 (mu, eta)
135 })
136 .collect()
137 }
138
139 fn mean_deriv(&self, idx: &[usize], eta: &Array1<f64>) -> Array2<f64> {
142 let p = self.exog.ncols();
143 let mut dmat = Array2::<f64>::zeros((idx.len(), p));
144 for (k, &i) in idx.iter().enumerate() {
145 let idl = self.inverse_deriv(eta[k]);
146 for j in 0..p {
147 dmat[[k, j]] = self.exog[[i, j]] * idl;
148 }
149 }
150 dmat
151 }
152
153 fn working_cov(&self, sdev: &Array1<f64>, dep: f64) -> Array2<f64> {
156 let m = sdev.len();
157 let mut v = Array2::<f64>::zeros((m, m));
158 for a in 0..m {
159 for b in 0..m {
160 let r = if a == b {
161 1.0
162 } else {
163 match self.cov_struct {
164 CovStruct::Independence => 0.0,
165 CovStruct::Exchangeable => dep,
166 }
167 };
168 v[[a, b]] = r * sdev[a] * sdev[b];
169 }
170 }
171 v
172 }
173
174 fn update_mean_params(
180 &self,
181 cached: &[(Array1<f64>, Array1<f64>)],
182 dep: f64,
183 ) -> Result<(Array1<f64>, Array1<f64>)> {
184 let p = self.exog.ncols();
185 let mut bmat = Array2::<f64>::zeros((p, p));
186 let mut score = Array1::<f64>::zeros(p);
187 for (gi, idx) in self.groups.iter().enumerate() {
188 let (mu, eta) = &cached[gi];
189 let resid: Array1<f64> = self
190 .endog_group(idx)
191 .iter()
192 .zip(mu.iter())
193 .map(|(y, m)| y - m)
194 .collect();
195 let dmat = self.mean_deriv(idx, eta);
196 let sdev = mu.mapv(|m| self.family.variance(m).sqrt());
197 let vmat = self.working_cov(&sdev, dep);
198
199 let vinv_d = solve_spd(&vmat, &dmat)?;
201 let vinv_r = solve(&vmat, &resid)?;
202
203 bmat += &dmat.t().dot(&vinv_d);
204 score += &dmat.t().dot(&vinv_r);
205 }
206 let update = solve(&bmat, &score)?;
207 Ok((update, score))
208 }
209
210 fn endog_group(&self, idx: &[usize]) -> Array1<f64> {
212 idx.iter().map(|&i| self.endog[i]).collect()
213 }
214
215 fn update_dep(&self, cached: &[(Array1<f64>, Array1<f64>)]) -> f64 {
218 if self.cov_struct == CovStruct::Independence {
219 return 0.0;
220 }
221 let nobs = self.nobs() as f64;
222 let ddof = self.ddof_scale;
223 let mut residsq_sum = 0.0;
224 let mut scale = 0.0;
225 let mut fsum1 = 0.0;
226 let mut fsum2 = 0.0;
227 let mut n_pairs = 0.0;
228 for (gi, idx) in self.groups.iter().enumerate() {
229 let (mu, _) = &cached[gi];
230 let y = self.endog_group(idx);
231 let resid: Array1<f64> = y
232 .iter()
233 .zip(mu.iter())
234 .map(|(yy, m)| (yy - m) / self.family.variance(*m).sqrt())
235 .collect();
236 let ssr: f64 = resid.iter().map(|r| r * r).sum();
237 scale += ssr;
238 fsum1 += idx.len() as f64;
239 let rsum: f64 = resid.sum();
240 residsq_sum += (rsum * rsum - ssr) / 2.0;
241 let ngrp = resid.len() as f64;
242 let npr = 0.5 * ngrp * (ngrp - 1.0);
243 fsum2 += npr;
244 n_pairs += npr;
245 }
246 if n_pairs == 0.0 {
247 return 0.0;
249 }
250 scale /= fsum1 * (nobs - ddof) / nobs;
251 residsq_sum /= scale;
252 residsq_sum / (fsum2 * (n_pairs - ddof) / n_pairs)
253 }
254
255 fn estimate_scale(&self, cached: &[(Array1<f64>, Array1<f64>)]) -> f64 {
257 if self.family.fixed_scale() {
258 return 1.0;
259 }
260 let nobs = self.nobs() as f64;
261 let ddof = self.ddof_scale;
262 let mut scale = 0.0;
263 let mut fsum = 0.0;
264 for (gi, idx) in self.groups.iter().enumerate() {
265 let (mu, _) = &cached[gi];
266 let y = self.endog_group(idx);
267 for (yy, m) in y.iter().zip(mu.iter()) {
268 let r = (yy - m) / self.family.variance(*m).sqrt();
269 scale += r * r;
270 }
271 fsum += idx.len() as f64;
272 }
273 scale /= fsum * (nobs - ddof) / nobs;
274 scale
275 }
276
277 fn covmat(
280 &self,
281 cached: &[(Array1<f64>, Array1<f64>)],
282 dep: f64,
283 ) -> Result<(Array2<f64>, Array2<f64>)> {
284 let p = self.exog.ncols();
285 let mut bmat = Array2::<f64>::zeros((p, p));
286 let mut cmat = Array2::<f64>::zeros((p, p));
287 for (gi, idx) in self.groups.iter().enumerate() {
288 let (mu, eta) = &cached[gi];
289 let resid: Array1<f64> = self
290 .endog_group(idx)
291 .iter()
292 .zip(mu.iter())
293 .map(|(y, m)| y - m)
294 .collect();
295 let dmat = self.mean_deriv(idx, eta);
296 let sdev = mu.mapv(|m| self.family.variance(m).sqrt());
297 let vmat = self.working_cov(&sdev, dep);
298
299 let vinv_d = solve_spd(&vmat, &dmat)?;
300 let vinv_r = solve(&vmat, &resid)?;
301
302 bmat += &dmat.t().dot(&vinv_d);
303 let dvinv_resid = dmat.t().dot(&vinv_r);
304 for a in 0..p {
306 for b in 0..p {
307 cmat[[a, b]] += dvinv_resid[a] * dvinv_resid[b];
308 }
309 }
310 }
311 let scale = self.estimate_scale(cached);
312 let bmati = inv(&bmat)?;
313 let cov_naive = &bmati * scale;
314 let cov_robust = bmati.dot(&cmat).dot(&bmati);
315 Ok((cov_robust, cov_naive))
316 }
317
318 pub fn fit(&self) -> Result<GeeResults> {
320 let p = self.exog.ncols();
321
322 let glm = Glm::with_link(
324 self.endog.clone(),
325 self.exog.clone(),
326 self.family,
327 self.link,
328 )?
329 .fit()?;
330 let mut params = glm.params.clone();
331
332 let mut cached = self.cached_means(¶ms);
333 let mut dep = 0.0;
334 let mut score_norm = f64::INFINITY;
335 let mut num_assoc_updates = 0usize;
336 let mut converged = false;
337
338 for _ in 0..self.maxiter {
339 let (update, score) = self.update_mean_params(&cached, dep)?;
340 params = ¶ms + &update;
341 cached = self.cached_means(¶ms);
342
343 score_norm = score.iter().map(|s| s * s).sum::<f64>().sqrt();
344
345 let update_dep = self.cov_struct != CovStruct::Independence;
346 if score_norm < self.ctol && (num_assoc_updates > 0 || !update_dep) {
347 converged = true;
348 break;
349 }
350
351 if update_dep {
352 dep = self.update_dep(&cached);
353 num_assoc_updates += 1;
354 } else {
355 converged = score_norm < self.ctol;
356 if converged {
357 break;
358 }
359 }
360 }
361
362 let (cov_robust, cov_naive) = self.covmat(&cached, dep)?;
363 let scale = self.estimate_scale(&cached);
364
365 let bse: Array1<f64> = (0..p).map(|j| cov_robust[[j, j]].sqrt()).collect();
366 let bse_naive: Array1<f64> = (0..p).map(|j| cov_naive[[j, j]].sqrt()).collect();
367
368 let tvalues: Array1<f64> = params.iter().zip(bse.iter()).map(|(b, s)| b / s).collect();
369 let pvalues: Array1<f64> = tvalues.mapv(|t| 2.0 * norm_sf(t.abs()));
370
371 let fitted: Array1<f64> = {
372 let mut f = Array1::<f64>::zeros(self.nobs());
373 for (gi, idx) in self.groups.iter().enumerate() {
374 let (mu, _) = &cached[gi];
375 for (k, &i) in idx.iter().enumerate() {
376 f[i] = mu[k];
377 }
378 }
379 f
380 };
381
382 Ok(GeeResults {
383 params,
384 bse,
385 bse_naive,
386 tvalues,
387 pvalues,
388 cov_robust,
389 cov_naive,
390 dep_params: dep,
391 scale,
392 fittedvalues: fitted,
393 score_norm,
394 converged,
395 })
396 }
397}
398
399#[derive(Clone, Debug)]
401pub struct GeeResults {
402 pub params: Array1<f64>,
404 pub bse: Array1<f64>,
406 pub bse_naive: Array1<f64>,
408 pub tvalues: Array1<f64>,
410 pub pvalues: Array1<f64>,
412 pub cov_robust: Array2<f64>,
414 pub cov_naive: Array2<f64>,
416 pub dep_params: f64,
419 pub scale: f64,
421 pub fittedvalues: Array1<f64>,
423 pub score_norm: f64,
425 pub converged: bool,
427}
428
429fn group_indices(labels: &[i64]) -> Vec<Vec<usize>> {
431 let mut order: Vec<i64> = Vec::new();
432 let mut groups: Vec<Vec<usize>> = Vec::new();
433 for (i, &lab) in labels.iter().enumerate() {
434 match order.iter().position(|&l| l == lab) {
435 Some(pos) => groups[pos].push(i),
436 None => {
437 order.push(lab);
438 groups.push(vec![i]);
439 }
440 }
441 }
442 groups
443}
444
445fn solve_spd(a: &Array2<f64>, b: &Array2<f64>) -> Result<Array2<f64>> {
447 let (m, k) = b.dim();
448 let mut out = Array2::<f64>::zeros((m, k));
449 for j in 0..k {
450 let col = b.column(j).to_owned();
451 let sol = solve(a, &col)?;
452 for i in 0..m {
453 out[[i, j]] = sol[i];
454 }
455 }
456 Ok(out)
457}
458
459#[cfg(test)]
460mod tests {
461 use super::*;
462 use ndarray::array;
463
464 #[test]
465 fn group_indices_preserves_order() {
466 let g = group_indices(&[5, 5, 2, 2, 5]);
467 assert_eq!(g, vec![vec![0, 1, 4], vec![2, 3]]);
468 }
469
470 #[test]
471 fn independence_poisson_matches_glm_params() {
472 let x = array![
475 [1.0, 0.0],
476 [1.0, 1.0],
477 [1.0, 2.0],
478 [1.0, 3.0],
479 [1.0, 4.0],
480 [1.0, 5.0],
481 ];
482 let y = array![1.0, 2.0, 3.0, 5.0, 8.0, 13.0];
483 let groups = [0i64, 0, 1, 1, 2, 2];
484 let gee = Gee::new(
485 y.clone(),
486 x.clone(),
487 &groups,
488 Family::Poisson,
489 CovStruct::Independence,
490 )
491 .unwrap();
492 let res = gee.fit().unwrap();
493 let glm = Glm::new(y, x, Family::Poisson).unwrap().fit().unwrap();
494 for j in 0..2 {
495 assert!((res.params[j] - glm.params[j]).abs() < 1e-8);
496 }
497 assert!(res.converged);
498 assert_eq!(res.dep_params, 0.0);
499 }
500
501 #[test]
502 fn exchangeable_reduces_to_independence_when_no_within_corr() {
503 let x = array![
507 [1.0, 0.5],
508 [1.0, -0.5],
509 [1.0, 1.0],
510 [1.0, -1.0],
511 [1.0, 0.2],
512 [1.0, -0.2],
513 ];
514 let y = array![3.0, 1.0, 4.0, 1.0, 5.0, 2.0];
515 let groups = [0i64, 1, 2, 3, 4, 5]; let exch = Gee::new(
517 y.clone(),
518 x.clone(),
519 &groups,
520 Family::Poisson,
521 CovStruct::Exchangeable,
522 )
523 .unwrap()
524 .fit()
525 .unwrap();
526 let indep = Gee::new(y, x, &groups, Family::Poisson, CovStruct::Independence)
527 .unwrap()
528 .fit()
529 .unwrap();
530 assert!(exch.converged && indep.converged);
531 for j in 0..exch.params.len() {
532 assert!((exch.params[j] - indep.params[j]).abs() < 1e-9);
533 }
534 }
535}