Skip to content

Fit joint Poisson GLMs per layer in build_netparams (#61)#67

Merged
smjenness merged 1 commit intomainfrom
feature/joint-gcomp-netparams
Apr 19, 2026
Merged

Fit joint Poisson GLMs per layer in build_netparams (#61)#67
smjenness merged 1 commit intomainfrom
feature/joint-gcomp-netparams

Conversation

@smjenness
Copy link
Copy Markdown
Contributor

Summary

Adds method = c("existing", "joint") to build_netparams(). Under method = "joint", fits one joint Poisson GLM per layer (main / casl / inst) that predicts degree (or one-off count) from all available attributes simultaneously — age.grp + sqrt(age.grp), the concurrent-layer degree, hiv2, race (when enabled), and geogYN (when geography-stratified) — with AIC-based interaction selection over age.grp:race.cat.num and age.grp:<cross_deg>. Fitted models are exposed at netparams$<layer>$joint_model as an additive output.

Default method = "existing" is byte-identical to pre-refactor behavior.

Closes #61. Unblocks #62 and #65.

What changed

  • R/NetParams.R — new private helper fit_joint_poisson() and joint-fit call sites at the end of each layer block. Helper records selected vs. candidate interactions on attr().
  • R/ARTnet-package.R@importFrom stats extended with AIC, formula, reformulate, update.
  • tests/testthat/test-joint-model.R — new; 7 test_that() blocks, 69 assertions. Skips when ARTnetData is not installed. Seeds RNG to 20260419 because build_netparams() imputes missing partner races via sample().
  • man/build_netparams.Rd and NAMESPACE regenerated via devtools::document().

Validation

Pre/post regression against the inst/validation/ snapshot harness (3 parameter sets):

Call Result
compare_to_snapshot() (default) ALL MATCH 3/3
compare_to_snapshot(method = "existing") ALL MATCH 3/3

Joint-method checks (Atlanta + race):

Check Result
Convergence (3 layers) ✅ all converged = TRUE, no warnings, nobs 4863/4863/4855
Marginal recovery mean(predict) = mean(obs) at 0.000% for all layers
Age slope age.grp = -1.19, sqrt(age.grp) = +3.48 (peaked-then-declining)
Race coefficients ✅ NH Black ref < Hispanic (+0.45) < W/O (+0.47); aligns with ARTnet literature
Unit tests ✅ 69/69 assertions pass

Finding for PI review

AIC-based selection disagrees with the CLAUDE.md prior on which interactions to keep:

Layer Selected interactions
main age.grp:deg.casl only
casl age.grp:deg.main only
inst age.grp:race.cat.num and age.grp:deg.tot3

CLAUDE.md flagged age.grp × race as "most plausible to matter given AMIS compositional shifts." The data (2017-18 ARTnet, Atlanta + race = TRUE) says otherwise for main/casl. The candidate_interactions / selected_interactions attributes on each fitted model make this decision transparent, and a future iteration can add a force_interactions parameter if we want to override AIC.

Test plan

  • Backward-compat: default and explicit method = "existing" match snapshots byte-for-byte on all 3 param sets
  • Joint convergence: all 3 layers converge without warnings
  • Marginal recovery: predicted mean within 1% of observed mean per layer
  • Coefficient sanity: age and race directions as expected
  • Unit tests: 69/69 pass
  • After merge: refresh inst/validation/snapshots/ is not required — joint fields are additive and are stripped before comparison.

Ref: CLAUDE.md §4.5 (#61 acceptance criteria), §4.7 (validation harness).

Adds a `method = c("existing", "joint")` argument to `build_netparams()`.
Under `method = "joint"`, one joint Poisson GLM is fit per network layer
(main / casl / inst) predicting degree (or one-off partner count) from
age.grp + sqrt(age.grp), the concurrent-layer degree, hiv2, race (when
enabled), and geogYN (when geography is stratified). Candidate
interactions (`age.grp:race.cat.num` when race = TRUE, and
`age.grp:<cross_deg>`) are evaluated via AIC and retained only when they
improve fit; the selected and candidate sets are stored as attributes on
the fitted object.

The fitted GLMs are exposed at `netparams$<layer>$joint_model` as an
additive output. Default `method = "existing"` is byte-identical to the
pre-refactor behavior — verified against the backward-compat snapshot
harness on all three parameter sets (Atlanta+race, national no-geog,
Atlanta no-race).

Closes #61.

Validation (Atlanta + race):
- All three joint models converge without warnings (nobs 4863/4863/4855).
- Marginal recovery exact: mean(predict) = mean(observed) at 0.000% diff
  for all layers (expected for Poisson GLM with intercept fit on the
  training data).
- Coefficient signs as expected: age.grp -1.19, sqrt(age.grp) +3.48
  (peaked-then-declining profile); race levels 2 and 3 above Black (ref);
  deg.casl main -0.55 with age.grp:deg.casl +0.10 interaction.
- AIC-based selection picked age.grp:deg.casl for main, age.grp:deg.main
  for casl, and both age.grp:race and age.grp:deg.tot3 for inst. Age×race
  was not selected for main or casl (substantive finding worth PI review).

New unit tests: tests/testthat/test-joint-model.R (7 test blocks, 69
assertions). Seeds RNG to 20260419 to handle the stochastic partner-race
NA imputation. Require ARTnetData and skip silently otherwise.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@smjenness smjenness merged commit 1a29e8e into main Apr 19, 2026
@smjenness smjenness deleted the feature/joint-gcomp-netparams branch April 19, 2026 19:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Phase 1.1] Refactor build_netparams() to fit joint Poisson GLM across attributes

1 participant