1use ndarray::{Array1, Array2};
29use solow_core::error::{Error, Result};
30use solow_distributions::norm_sf;
31use solow_glm::{Family, Glm, Link};
32use solow_linalg::{inv, solve};
33
34#[derive(Clone, Copy, Debug, PartialEq, Eq)]
36pub enum CategoricalCov {
37 Independence,
40 GlobalOddsRatio,
45}
46
47#[derive(Clone, Copy, Debug, PartialEq, Eq)]
49enum Kind {
50 Nominal,
51 Ordinal,
52}
53
54#[derive(Clone, Debug)]
56pub struct NominalGee {
57 inner: CategoricalGee,
58}
59
60#[derive(Clone, Debug)]
63pub struct OrdinalGee {
64 inner: CategoricalGee,
65}
66
67impl NominalGee {
68 pub fn new(
75 endog: Array1<f64>,
76 exog: Array2<f64>,
77 group_labels: &[i64],
78 cov: CategoricalCov,
79 ) -> Result<Self> {
80 Ok(NominalGee {
81 inner: CategoricalGee::new(Kind::Nominal, endog, exog, group_labels, cov)?,
82 })
83 }
84
85 pub fn maxiter(mut self, m: usize) -> Self {
87 self.inner.maxiter = m;
88 self
89 }
90
91 pub fn ctol(mut self, t: f64) -> Self {
93 self.inner.ctol = t;
94 self
95 }
96
97 pub fn fit(&self) -> Result<CategoricalGeeResults> {
99 self.inner.fit()
100 }
101}
102
103impl OrdinalGee {
104 pub fn new(
110 endog: Array1<f64>,
111 exog: Array2<f64>,
112 group_labels: &[i64],
113 cov: CategoricalCov,
114 ) -> Result<Self> {
115 Ok(OrdinalGee {
116 inner: CategoricalGee::new(Kind::Ordinal, endog, exog, group_labels, cov)?,
117 })
118 }
119
120 pub fn maxiter(mut self, m: usize) -> Self {
122 self.inner.maxiter = m;
123 self
124 }
125
126 pub fn ctol(mut self, t: f64) -> Self {
128 self.inner.ctol = t;
129 self
130 }
131
132 pub fn fit(&self) -> Result<CategoricalGeeResults> {
134 self.inner.fit()
135 }
136}
137
138#[derive(Clone, Debug)]
140struct CategoricalGee {
141 kind: Kind,
142 cov: CategoricalCov,
143 ncut: usize,
145 nparam: usize,
147 exog: Array2<f64>,
150 endog: Array1<f64>,
152 groups: Vec<Vec<usize>>,
155 group_nobs: Vec<usize>,
157 nrows: usize,
159 maxiter: usize,
160 ctol: f64,
161}
162
163impl CategoricalGee {
164 fn new(
165 kind: Kind,
166 endog: Array1<f64>,
167 exog: Array2<f64>,
168 group_labels: &[i64],
169 cov: CategoricalCov,
170 ) -> Result<Self> {
171 let n = endog.len();
172 if n != exog.nrows() {
173 return Err(Error::Shape("endog length != exog rows".into()));
174 }
175 if group_labels.len() != n {
176 return Err(Error::Shape("group_labels length != endog length".into()));
177 }
178
179 let mut levels: Vec<f64> = endog.iter().copied().collect();
181 levels.sort_by(|a, b| a.total_cmp(b));
182 levels.dedup();
183 if levels.len() < 2 {
184 return Err(Error::Shape("endog must have at least two levels".into()));
185 }
186 let ncut = levels.len() - 1;
187 let cuts = &levels[..ncut];
188 let p = exog.ncols();
189
190 let nparam = match kind {
191 Kind::Nominal => ncut * p,
192 Kind::Ordinal => ncut + p,
193 };
194
195 let mut order: Vec<i64> = group_labels.to_vec();
198 order.sort_unstable();
199 order.dedup();
200 let mut orig_groups: Vec<Vec<usize>> = vec![Vec::new(); order.len()];
201 for (i, &lab) in group_labels.iter().enumerate() {
202 let pos = order.binary_search(&lab).unwrap();
203 orig_groups[pos].push(i);
204 }
205
206 let width = match kind {
209 Kind::Nominal => ncut * p,
210 Kind::Ordinal => ncut + p,
211 };
212 let nrows = ncut * n;
213 let mut exog_out = Array2::<f64>::zeros((nrows, width));
214 let mut endog_out = Array1::<f64>::zeros(nrows);
215 let mut groups: Vec<Vec<usize>> = Vec::with_capacity(order.len());
216 let mut group_nobs: Vec<usize> = Vec::with_capacity(order.len());
217
218 let mut jrow = 0usize;
219 for og in &orig_groups {
220 let mut rows: Vec<usize> = Vec::with_capacity(og.len() * ncut);
221 for &i in og {
222 let yval = endog[i];
223 for (cix, &cut) in cuts.iter().enumerate() {
224 match kind {
225 Kind::Ordinal => {
226 exog_out[[jrow, cix]] = 1.0;
228 for c in 0..p {
229 exog_out[[jrow, ncut + c]] = exog[[i, c]];
230 }
231 endog_out[jrow] = if yval > cut { 1.0 } else { 0.0 };
232 }
233 Kind::Nominal => {
234 let base = cix * p;
236 for c in 0..p {
237 exog_out[[jrow, base + c]] = exog[[i, c]];
238 }
239 endog_out[jrow] = if yval == cut { 1.0 } else { 0.0 };
240 }
241 }
242 rows.push(jrow);
243 jrow += 1;
244 }
245 }
246 group_nobs.push(og.len());
247 groups.push(rows);
248 }
249
250 Ok(CategoricalGee {
251 kind,
252 cov,
253 ncut,
254 nparam,
255 exog: exog_out,
256 endog: endog_out,
257 groups,
258 group_nobs,
259 nrows,
260 maxiter: 300,
261 ctol: 1e-10,
262 })
263 }
264
265 fn lin_pred(&self, idx: &[usize], params: &Array1<f64>) -> Array1<f64> {
267 let mut lpr = Array1::<f64>::zeros(idx.len());
268 for (k, &r) in idx.iter().enumerate() {
269 let mut s = 0.0;
270 for j in 0..self.nparam {
271 s += self.exog[[r, j]] * params[j];
272 }
273 lpr[k] = s;
274 }
275 lpr
276 }
277
278 fn mean(&self, lpr: &Array1<f64>) -> Array1<f64> {
284 match self.kind {
285 Kind::Ordinal => lpr.mapv(|e| 1.0 / (1.0 + (-e).exp())),
286 Kind::Nominal => {
287 let mut mu = Array1::<f64>::zeros(lpr.len());
288 let nobs = lpr.len() / self.ncut;
289 for o in 0..nobs {
290 let base = o * self.ncut;
291 let mut denom = 1.0;
292 for k in 0..self.ncut {
293 denom += lpr[base + k].exp();
294 }
295 for k in 0..self.ncut {
296 mu[base + k] = lpr[base + k].exp() / denom;
297 }
298 }
299 mu
300 }
301 }
302 }
303
304 fn mean_deriv(&self, idx: &[usize], mu: &Array1<f64>) -> Array2<f64> {
312 let m = idx.len();
313 let mut d = Array2::<f64>::zeros((m, self.nparam));
314 for (k, &r) in idx.iter().enumerate() {
315 let idl = mu[k] * (1.0 - mu[k]);
316 for j in 0..self.nparam {
317 d[[k, j]] = self.exog[[r, j]] * idl;
318 }
319 }
320 d
321 }
322
323 fn working_cov(&self, gi: usize, mu: &Array1<f64>, dep: f64) -> Array2<f64> {
331 let m = mu.len();
332 let nobs = self.group_nobs[gi];
333 let mut v = Array2::<f64>::zeros((m, m));
334
335 if self.cov == CategoricalCov::GlobalOddsRatio {
336 let eyy = self.get_eyy(mu, dep);
340 for a in 0..m {
341 for b in 0..m {
342 v[[a, b]] = eyy[[a, b]] - mu[a] * mu[b];
343 }
344 }
345 }
346
347 for o in 0..nobs {
349 let base = o * self.ncut;
350 for a in 0..self.ncut {
351 for b in 0..self.ncut {
352 let ea = mu[base + a];
353 let eb = mu[base + b];
354 let val = match self.kind {
355 Kind::Ordinal => ea.min(eb) - ea * eb,
356 Kind::Nominal => {
357 if a == b {
358 ea - ea * ea
359 } else {
360 -ea * eb
361 }
362 }
363 };
364 v[[base + a, base + b]] = val;
365 }
366 }
367 }
368 v
369 }
370
371 fn get_eyy(&self, mu: &Array1<f64>, dep: f64) -> Array2<f64> {
374 let m = mu.len();
375 let mut eyy = Array2::<f64>::zeros((m, m));
376 if dep == 1.0 {
377 for a in 0..m {
378 for b in 0..m {
379 eyy[[a, b]] = mu[a] * mu[b];
380 }
381 }
382 return eyy;
383 }
384 let or = dep;
385 for a in 0..m {
386 for b in 0..m {
387 let psum = mu[a] + mu[b];
388 let pprod = mu[a] * mu[b];
389 let pfac =
390 ((1.0 + psum * (or - 1.0)).powi(2) + 4.0 * or * (1.0 - or) * pprod).sqrt();
391 eyy[[a, b]] = (1.0 + psum * (or - 1.0) - pfac) / (2.0 * (or - 1.0));
392 }
393 }
394 eyy
395 }
396
397 fn update_mean_params(
400 &self,
401 params: &Array1<f64>,
402 dep: f64,
403 ) -> Result<(Array1<f64>, Array1<f64>)> {
404 let (bmat, _, score) = self.accumulate(params, dep)?;
405 let update = solve(&bmat, &score)?;
406 Ok((update, score))
407 }
408
409 fn accumulate(
412 &self,
413 params: &Array1<f64>,
414 dep: f64,
415 ) -> Result<(Array2<f64>, Array2<f64>, Array1<f64>)> {
416 let p = self.nparam;
417 let mut bmat = Array2::<f64>::zeros((p, p));
418 let mut cmat = Array2::<f64>::zeros((p, p));
419 let mut score = Array1::<f64>::zeros(p);
420
421 for (gi, idx) in self.groups.iter().enumerate() {
422 if idx.is_empty() {
423 continue;
424 }
425 let lpr = self.lin_pred(idx, params);
426 let mu = self.mean(&lpr);
427 let resid: Array1<f64> = idx
428 .iter()
429 .zip(mu.iter())
430 .map(|(&r, m)| self.endog[r] - m)
431 .collect();
432 let dmat = self.mean_deriv(idx, &mu);
433 let vmat = self.working_cov(gi, &mu, dep);
434
435 let vinv_d = solve_mat(&vmat, &dmat)?;
436 let vinv_r = solve(&vmat, &resid)?;
437
438 bmat += &dmat.t().dot(&vinv_d);
439 let dvinv_resid = dmat.t().dot(&vinv_r);
440 score += &dvinv_resid;
441 for a in 0..p {
442 for b in 0..p {
443 cmat[[a, b]] += dvinv_resid[a] * dvinv_resid[b];
444 }
445 }
446 }
447 Ok((bmat, cmat, score))
448 }
449
450 fn observed_crude_oddsratio(&self) -> f64 {
454 let mut tables = self.empty_tables();
456 for (gi, idx) in self.groups.iter().enumerate() {
457 let nobs = self.group_nobs[gi];
458 let y: Array1<f64> = idx.iter().map(|&r| self.endog[r]).collect();
459 self.accumulate_tables(&mut tables, &y, &y, nobs);
460 }
461 pooled_odds_ratio(&tables)
462 }
463
464 fn empty_tables(&self) -> Vec<[[f64; 2]; 2]> {
466 let mut n = 0;
467 for k1 in 0..self.ncut {
468 n += k1 + 1;
469 }
470 vec![[[0.0; 2]; 2]; n]
471 }
472
473 fn pair_index(&self, k2: usize, k1: usize) -> usize {
476 let mut base = 0;
478 for k in 0..k1 {
479 base += k + 1;
480 }
481 base + k2
482 }
483
484 fn accumulate_tables(
491 &self,
492 tables: &mut [[[f64; 2]; 2]],
493 ya: &Array1<f64>,
494 yb: &Array1<f64>,
495 nobs: usize,
496 ) {
497 for i1 in 0..nobs {
499 for i2 in 0..i1 {
500 for k1 in 0..self.ncut {
501 for k2 in 0..=k1 {
502 let a = i1 * self.ncut + k1;
503 let b = i2 * self.ncut + k2;
504 let p11 = ya[a] * yb[b];
505 let p10 = ya[a] * (1.0 - yb[b]);
506 let p01 = (1.0 - ya[a]) * yb[b];
507 let p00 = (1.0 - ya[a]) * (1.0 - yb[b]);
508 let t = &mut tables[self.pair_index(k2, k1)];
509 t[1][1] += p11;
510 t[1][0] += p10;
511 t[0][1] += p01;
512 t[0][0] += p00;
513 }
514 }
515 }
516 }
517 }
518
519 fn update_dep(&self, params: &Array1<f64>, dep: f64, crude_or: f64) -> f64 {
523 if self.group_nobs.iter().all(|&m| m <= 1) {
525 return dep;
526 }
527 let mut tables = self.empty_tables();
528 for (gi, idx) in self.groups.iter().enumerate() {
529 let nobs = self.group_nobs[gi];
530 if nobs <= 1 {
531 continue;
532 }
533 let lpr = self.lin_pred(idx, params);
534 let mu = self.mean(&lpr);
535 let eyy = self.get_eyy(&mu, dep);
536 for i1 in 0..nobs {
538 for i2 in 0..i1 {
539 for k1 in 0..self.ncut {
540 for k2 in 0..=k1 {
541 let a = i1 * self.ncut + k1;
542 let b = i2 * self.ncut + k2;
543 let e11 = eyy[[a, b]];
544 let e10 = mu[a] - e11;
545 let e01 = mu[b] - e11;
546 let e00 = 1.0 - (e11 + e10 + e01);
547 let t = &mut tables[self.pair_index(k2, k1)];
548 t[1][1] += e11;
549 t[1][0] += e10;
550 t[0][1] += e01;
551 t[0][0] += e00;
552 }
553 }
554 }
555 }
556 }
557 let cor_expval = pooled_odds_ratio(&tables);
558 let new_dep = dep * crude_or / cor_expval;
559 if new_dep.is_finite() {
560 new_dep
561 } else {
562 1.0
563 }
564 }
565
566 fn starting_params(&self) -> Result<Array1<f64>> {
570 let glm = Glm::with_link(
571 self.endog.clone(),
572 self.exog.clone(),
573 Family::Binomial,
574 Link::Logit,
575 )?
576 .fit()?;
577 Ok(glm.params)
578 }
579
580 fn fit(&self) -> Result<CategoricalGeeResults> {
582 let mut params = self.starting_params()?;
583
584 let update_dep =
585 self.cov == CategoricalCov::GlobalOddsRatio && self.group_nobs.iter().any(|&m| m > 1);
586 let crude_or = if update_dep {
588 self.observed_crude_oddsratio()
589 } else {
590 1.0
591 };
592 let mut dep = if update_dep { crude_or } else { 1.0 };
593
594 let mut score_norm = f64::INFINITY;
595 let mut num_assoc_updates = 0usize;
596 let mut converged = false;
597
598 for _ in 0..self.maxiter {
599 let (update, score) = self.update_mean_params(¶ms, dep)?;
600 params = ¶ms + &update;
601 score_norm = score.iter().map(|s| s * s).sum::<f64>().sqrt();
602
603 if score_norm < self.ctol && (num_assoc_updates > 0 || !update_dep) {
604 converged = true;
605 break;
606 }
607
608 if update_dep {
609 dep = self.update_dep(¶ms, dep, crude_or);
610 num_assoc_updates += 1;
611 } else {
612 converged = score_norm < self.ctol;
613 if converged {
614 break;
615 }
616 }
617 }
618
619 let (bmat, cmat, _) = self.accumulate(¶ms, dep)?;
622 let bmati = inv(&bmat)?;
623 let cov_naive = bmati.clone();
624 let cov_robust = bmati.dot(&cmat).dot(&bmati);
625
626 let p = self.nparam;
627 let bse: Array1<f64> = (0..p).map(|j| cov_robust[[j, j]].sqrt()).collect();
628 let bse_naive: Array1<f64> = (0..p).map(|j| cov_naive[[j, j]].sqrt()).collect();
629 let tvalues: Array1<f64> = params.iter().zip(bse.iter()).map(|(b, s)| b / s).collect();
630 let pvalues: Array1<f64> = tvalues.mapv(|t| 2.0 * norm_sf(t.abs()));
631
632 let mut fitted = Array1::<f64>::zeros(self.nrows);
634 for idx in &self.groups {
635 let lpr = self.lin_pred(idx, ¶ms);
636 let mu = self.mean(&lpr);
637 for (k, &r) in idx.iter().enumerate() {
638 fitted[r] = mu[k];
639 }
640 }
641
642 Ok(CategoricalGeeResults {
643 params,
644 bse,
645 bse_naive,
646 tvalues,
647 pvalues,
648 cov_robust,
649 cov_naive,
650 dep_params: if update_dep { dep } else { 0.0 },
651 scale: 1.0,
652 fittedvalues: fitted,
653 ncut: self.ncut,
654 score_norm,
655 converged,
656 })
657 }
658}
659
660#[derive(Clone, Debug)]
662pub struct CategoricalGeeResults {
663 pub params: Array1<f64>,
667 pub bse: Array1<f64>,
669 pub bse_naive: Array1<f64>,
671 pub tvalues: Array1<f64>,
673 pub pvalues: Array1<f64>,
675 pub cov_robust: Array2<f64>,
677 pub cov_naive: Array2<f64>,
679 pub dep_params: f64,
682 pub scale: f64,
684 pub fittedvalues: Array1<f64>,
686 pub ncut: usize,
688 pub score_norm: f64,
690 pub converged: bool,
692}
693
694fn pooled_odds_ratio(tables: &[[[f64; 2]; 2]]) -> f64 {
696 if tables.is_empty() {
697 return 1.0;
698 }
699 let mut log_or = Vec::with_capacity(tables.len());
700 let mut var = Vec::with_capacity(tables.len());
701 for t in tables {
702 let lor = t[1][1].ln() + t[0][0].ln() - t[0][1].ln() - t[1][0].ln();
703 log_or.push(lor);
704 var.push(1.0 / t[1][1] + 1.0 / t[0][0] + 1.0 / t[0][1] + 1.0 / t[1][0]);
705 }
706 let wts: Vec<f64> = var.iter().map(|v| 1.0 / v).collect();
707 let wtsum: f64 = wts.iter().sum();
708 let log_pooled: f64 = wts
709 .iter()
710 .zip(log_or.iter())
711 .map(|(w, e)| (w / wtsum) * e)
712 .sum();
713 log_pooled.exp()
714}
715
716fn solve_mat(a: &Array2<f64>, b: &Array2<f64>) -> Result<Array2<f64>> {
718 let (m, k) = b.dim();
719 let mut out = Array2::<f64>::zeros((m, k));
720 for j in 0..k {
721 let col = b.column(j).to_owned();
722 let sol = solve(a, &col)?;
723 for i in 0..m {
724 out[[i, j]] = sol[i];
725 }
726 }
727 Ok(out)
728}
729
730#[cfg(test)]
731mod tests {
732 use super::*;
733 use ndarray::array;
734
735 #[test]
736 fn ordinal_expands_indicators() {
737 let y = array![0.0, 1.0, 2.0];
739 let x = array![[0.5], [1.0], [-0.5]];
740 let groups = [0i64, 0, 1];
741 let m = CategoricalGee::new(Kind::Ordinal, y, x, &groups, CategoricalCov::Independence)
742 .unwrap();
743 assert_eq!(m.ncut, 2);
744 assert_eq!(m.endog.to_vec(), vec![0.0, 0.0, 1.0, 0.0, 1.0, 1.0]);
746 assert_eq!(m.nparam, 2 + 1);
748 }
749
750 #[test]
751 fn nominal_expands_indicators() {
752 let y = array![0.0, 1.0, 2.0];
754 let x = array![[1.0, 0.5], [1.0, 1.0], [1.0, -0.5]];
755 let groups = [0i64, 0, 1];
756 let m = CategoricalGee::new(Kind::Nominal, y, x, &groups, CategoricalCov::Independence)
757 .unwrap();
758 assert_eq!(m.ncut, 2);
759 assert_eq!(m.nparam, 2 * 2);
760 assert_eq!(m.endog.to_vec(), vec![1.0, 0.0, 0.0, 1.0, 0.0, 0.0]);
762 }
763
764 #[test]
765 fn nominal_mean_matches_softmax() {
766 let y = array![0.0, 1.0, 2.0];
769 let x = array![[1.0], [1.0], [1.0]];
770 let groups = [0i64, 0, 0];
771 let m = CategoricalGee::new(Kind::Nominal, y, x, &groups, CategoricalCov::Independence)
772 .unwrap();
773 let lpr = array![0.5_f64, -0.3];
775 let mu = m.mean(&lpr);
776 let denom = 1.0 + 0.5_f64.exp() + (-0.3_f64).exp();
777 assert!((mu[0] - 0.5_f64.exp() / denom).abs() < 1e-12);
778 assert!((mu[1] - (-0.3_f64).exp() / denom).abs() < 1e-12);
779 assert!(mu[0] + mu[1] < 1.0);
780 }
781
782 #[test]
783 fn ordinal_mean_is_logit() {
784 let y = array![0.0, 1.0, 2.0];
785 let x = array![[0.5], [1.0], [-0.5]];
786 let groups = [0i64, 0, 1];
787 let m = CategoricalGee::new(Kind::Ordinal, y, x, &groups, CategoricalCov::Independence)
788 .unwrap();
789 let lpr = array![0.7_f64, -1.2];
790 let mu = m.mean(&lpr);
791 assert!((mu[0] - 1.0 / (1.0 + (-0.7_f64).exp())).abs() < 1e-12);
792 assert!((mu[1] - 1.0 / (1.0 + 1.2_f64.exp())).abs() < 1e-12);
793 }
794}