Figure 2: The genetic age interaction changes individual predictions¶

Runs locally against the authors' results (CODE). With RUN = False the cells show the current local figures; set RUN = True to regenerate each panel from the script named. No participant data are stored in this notebook; keep outputs cleared before committing (python ../tools/check_no_ids.py).

Panel a Mechanism: identical at enrollment, different age interaction¶

Script: src/figure_edit_briefs/make_rapid_progressor_mechanism_triplet.py

Environment: PANEL=1

Upstream: triplet backbone (src/train_slope_3way.py)

Script header (inputs, recipe, outputs)
TRIPLET PORT (generated by reparam_fig3/_make_fig12_triplet.py; inputs + output names only).

Rapid-progressor MECHANISM cartoon (Act 1) -- how a rapid vs slow progressor FALLS OUT
of the model's genetic x time interaction, not a hand-drawn curve.

Mechanism (deployed slope model, genetic_scale=1):
    lambda_k(t) = ref_k(t) + (G . gamma_level_k) + (t/51)(G . gamma_slope_k) + delta_k(t)
    theta(t)    = softmax_k( lambda(t) )                     # over 20 signatures + 1 healthy anchor(-5)
    hazard_d(t) = sum_k theta_k(t) * sigmoid( logit_mu_d(t) + psi_kd + eps_kdt )

The genetic x time term is the whole story: for a person whose genetics project POSITIVELY
on gamma_slope of the high-risk (cardiac) signature, the loading on that signature ACCELERATES
with age -> softmax mass concentrates there -> convex, front-loaded incidence = RAPID PROGRESSOR.
Project NEGATIVELY -> the loading decelerates -> mass stays diffuse = SLOW. Same snapshot level
at enrollment, opposite slope -> divergent futures (Y. Ding's "snapshots mislead").

INPUTS (real, deployed -- provenance):
  {DBX}/slope_model_nokappa_v3_delta0/slope_model_batch_0_10000.pt
        (gamma_level[47,21], gamma_slope[47,21], gamma_health[47], psi[21,348], epsilon[21,348,52], delta[10000,21,52])
  {DATA}/reference_trajectories.pt      (signature_refs[20,52], healthy_ref=-5)
  {DBX}/censor_e_batchrun_vectorized_REPARAM_v3_nokappa/...batch_0_10000.pt  (G[10000,47] standardized, logit_prevalence_t[348,52])
  {DATA}/disease_names.csv
RECIPE: signature k*=0 (cardiac: atrial fibrillation / cardiomyopathy). Archetype (level,slope)
  loadings = p5 / p50 / p95 of the population sig-0 projections (aggregate quantiles, no patient rows);
  other signatures' genetic loading = 0 and health anchor alpha=0, to ISOLATE the k* interaction.
  theta sampled with delta_k*(t) ~ GP-smooth N(0, sd of the real delta) -- "sample theta a few times".
OUTPUT: fig_rapid_progressor_mechanism.png/pdf   (schematic built from REAL fitted parameters; tagged as such)
ENV:    /opt/miniconda3/envs/new_env_pyro2/bin/python
No description has been provided for this image

Panel b No-history ASCVD detection¶

Script: src/paper_keystone/Fig3_dynamic_auc/make_fig3_train_wreck_bar_triplet.py

Upstream: held-out predictions: src/triplet_loo_calibration.py

Script header (inputs, recipe, outputs)
TRIPLET PORT (generated by reparam_fig3/_make_fig12_triplet.py; inputs + output names only).
FIG 2 train-wreck with BAR (constant-LR no-slope) as the no-slope arm, on the common 200k footing (bar=NB20).
Same logic as make_fig3_train_wreck_delta0.py; only the no-slope prediction changes cosine->bar (constant-LR),
and N=200000. Bar is batched: load ONLY the 5 ASCVD columns (200k x 5 x 52).
OUTPUT fig3_train_wreck_bar.{png,pdf} + values.csv. RUN via new_env_pyro2. Aggregates + row-index examples only.
No description has been provided for this image

Panel c Non-proportional predictions in two patients¶

Script: src/reparam_fig3/catch_exemplars_mechanism_triplet.py --ex_csv catch_exemplars_1yr_triplet_earlylate.csv --tag _earlylate --diseases "Cholelithiasis,Hypercholesterolemia"

Upstream: patients chosen by src/reparam_fig3/select_exemplars_early_divergence_triplet.py and select_exemplars_by_age_term_triplet.py, combined by make_exemplars_early_late_triplet.py (run from reparam_fig3/)

Script header (inputs, recipe, outputs)
TRIPLET PORT (generated by reparam_fig3/_make_fig12_triplet.py).
MECHANISM for the catch exemplars: WHY the time-varying model re-times these patients' risk.

Locked to the SAME individuals as the trajectory panel (catch_exemplars_1yr_triplet.csv), one column per person, so
the figure reads as an argument rather than a gallery: the trajectory panel shows THAT the risk moved, this
one shows WHAT moved it.

Two compressed rows (the 4-row exemplars_slope_vs_bar_nohistory.py figure is the full version; its rows 1-2
duplicate the trajectory panel, and its genetics-vs-environment row needs its own paragraph -> supplement):

  ROW 1  MAGNIFICATION   log2 pi_slope(t) / pi_bar(t).  It CROSSES ZERO -- that is the visual proof the
                         time-varying term RE-TIMES risk (up early, down late) rather than merely scaling it.
  ROW 2  THE DRIFT       theta_k(t) for the signature the drift moves most, under gamma_level ALONE
                         (dashed) vs gamma_level + t*gamma_slope (solid).  Compared WITHIN the slope arm --
                         plotting slope-arm vs bar-arm here would confound the drift u with the two arms'
                         different gamma_level, which is a LEVEL offset, not the time-varying term.

MODEL (delta0; gamma/psi/epsilon frozen from the LOO pool, delta refit; prediction-time delta fit is CONSTANT
learning rate for BOTH arms, so slope-vs-bar is recipe-matched):
    lambda_ik(t) = sigref_k(t) + s*(Gz_i @ gamma_level_k) + t*s*(Gz_i @ gamma_slope_k) + delta_ikt
    theta_i(t)   = softmax_k lambda_i(t)          <- genetics only REALLOCATES signature occupancy
    pi_idt       = sum_k theta_ikt * sigmoid(phi_kdt)   <- phi = the SHARED, non-genetic disease environment
  u_ik := s*(Gz_i @ gamma_slope_k) is the per-person, per-signature drift. u>0 -> occupancy of k GROWS with
  age, progressively MAGNIFYING that signature's phi; u<0 -> decays, DAMPING it. bar is gamma_slope == 0.
  Gz = G standardized WITHIN the 10k batch (the model does this at construction); time_grid = arange(T)/(T-1),
  NORMALIZED to [0,1] -- so u is the drift over the whole age span and u/(T-1) is per-year.

INPUTS  catch_exemplars_1yr_triplet.csv                      (which people; from catch_exemplars_plot.py)
No description has been provided for this image

Panel d AUC gain by number of prior diagnoses¶

Script: src/reparam_fig3/make_fig3_prior_dx_heatmap_triplet.py

Upstream: scores: src/reparam_fig3/rescore_prior_dx_bar_triplet.py

Script header (inputs, recipe, outputs)
TRIPLET PORT (generated by reparam_fig3/_make_fig12_triplet.py; inputs + output names only).
FIGURE 3b (heatmap): 1-yr AUC by disease x prior-diagnosis-count stratum -- discrimination holds without a problem list.

Rows = diseases, columns = prior-dx burden (0 / 1-2 / 3-5 / 6+ at prediction age). Left heatmap = time-varying
(slope) AUC; right = Delta AUC (slope - no-slope). The slope model stays high (green) across every stratum,
INCLUDING 0 prior dx (no problem list) -- it is not just counting a patient's existing diagnoses; and its
advantage over no-slope persists (right panel) at every burden.

INPUT  results_slope_loo_like_delta0/auc_by_prior_dx_count_3models.csv (disease x stratum x model).
OUTPUT fig3_prior_dx_heatmap.{png,pdf} (+prov).  RUN via new_env_pyro2.  Aggregates only.
RUN    /opt/miniconda3/envs/new_env_pyro2/bin/python claudefile/reparam_fig3/make_fig3_prior_dx_heatmap.py
No description has been provided for this image