Skip to content

[Survival MoE] Add personalized mixture-of-experts model for survival prediction - #1345

Open
AanshDevpura wants to merge 3 commits into
sunlabuiuc:masterfrom
AanshDevpura:feat/personalized-survival-moe
Open

AanshDevpura wants to merge 3 commits into
sunlabuiuc:masterfrom
AanshDevpura:feat/personalized-survival-moe

Conversation

@AanshDevpura

Copy link
Copy Markdown

Contributors: Aansh Devpura (devpura2@illinois.edu), Sahana Hariharan (sahanah2@illinois.edu), Shrida Shivaram Bhat (shridab2@illinois.edu)

Contribution Type: Model

Description

Add the personalized survival mixture-of-experts model from Let the Experts Speak: Improving Survival Prediction & Calibration via Mixture-of-Experts Heads to PyHealth. Includes a SUPPORT2 example comparing dropout 0, 0.1, 0.3 and 0.5 using C-index, IPCW Brier score and IPCW ECE. The example follows the released SUPPORT2 preprocessing and architecture, runs five paired seeds with early stopping, and reports full-grid calibration and measured results. Across 20 real-data runs, dropout 0.1 had the best mean C-index (0.8105), IPCW Brier (0.1409), and IPCW ECE (0.0413).

Files to review

pyhealth/models/personalized_survival_moe.py - New model implementation including:

PersonalizedSurvivalMoE - Main PyHealth-integrated model class

Categorical embeddings, query/key routing and patient-specific MTLR expert heads

Log-space probability mixing and right-censored negative log likelihood

pyhealth/models/__init__.py - Added import for PersonalizedSurvivalMoE.

docs/api/models/pyhealth.models.personalized_survival_moe.rst - Sphinx API page using the standard PyHealth autoclass layout.

docs/api/models.rst - Added the model documentation to the index.

examples/support2_survival_personalized_moe.py - Downloads missing SUPPORT2 data from UCI, loads it through Support2Dataset, and runs the dropout ablation; includes synthetic mode, metrics and recorded results.

tests/core/test_personalized_survival_moe.py - 21 synthetic model tests covering initialization, forward/output shapes, finite nonzero gradients, categorical embeddings, censoring, dropout, numerical stability, input validation, PyHealth dataloader integration, and checkpoint save/load with temporary-directory cleanup.

@Logiquo Logiquo added the status: automatic The PR will go through automatic review process. label Oct 30, 2026
@Logiquo

Logiquo commented Oct 30, 2026

Copy link
Copy Markdown
Collaborator

/review

@github-actions

Copy link
Copy Markdown

Test Coverage

Total: 73.36% (19693 / 26846 lines)

Modified file Coverage Lines
pyhealth/models/__init__.py 100.00% 53 / 53
pyhealth/models/personalized_survival_moe.py 98.97% 96 / 97

Workflow run · ff11d23

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed commit ff11d23

pending.replace(path)


def load_data(root: str | None, synthetic: bool, seed: int) -> pd.DataFrame:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[medium] Re-implements existing SUPPORT2 loader and KM helper

load_data re-implements a SUPPORT2 loader on top of Support2Dataset, which examples/survival_mdn_data.py::read_support2 already provides. censoring_km (line 149) duplicates the KM logic in examples/survival_mdn_metrics.py::km_fit. The tie handling differs (tied events leave the risk set first here), so the two are not interchangeable. Import the shared loader and KM helper from the sibling MDN example modules, as that example already does, or add a documented reason for the differing tie convention. Keep only the new IPCW Brier/ECE and preprocessing here.

raise ValueError(f"Expected exactly one SUPPORT2 row for {pid}")
rows.append(
{
key: getattr(events[0], key, None)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[low] Missing attributes silently become imputed values

getattr(events[0], key, None) returns None for any absent attribute. A missing or misspelled SUPPORT2 column would then be silently imputed as missing for every patient, with no error. Use events[0][key] or getattr(events[0], key) without the default so a bad column fails loudly.

stale += 1
if stale >= args.patience:
break
model.load_state_dict(state)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[low] Crash if no epoch yields a finite validation NLL

state starts as None and is set only when score < best. If every validation NLL is NaN, model.load_state_dict(None) raises a TypeError. Initialize state from the starting weights, or raise an explicit error when no checkpoint was selected.

train,
batch_size=args.batch_size,
shuffle=True,
generator=torch.Generator().manual_seed(42),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[low] Batch order is identical across seeds; docs imply otherwise

The DataLoader generator is seeded with 42 for every seed, so shuffle order is the same in all runs and only initialization varies. The module docstring says repeated seeds measure optimization variability, which overstates the spread. State that batch order is fixed in the docstring.

if not torch.isfinite(x).all():
raise ValueError("features must be finite; impute missing values first")
if self.embeddings:
categories = torch.as_tensor(kwargs["categories"], device=self.device)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[low] Missing categories input raises KeyError, not ValueError

When the model is built with categorical cardinalities, kwargs["categories"] raises a bare KeyError if the caller omits it. Every other input problem raises ValueError. Check for the key and raise ValueError with a clear message.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AI Review

suggestion: need change
decision: need change

Thank you for this contribution. It adds a personalized mixture-of-experts survival model with query/key routing, MTLR expert heads and a censoring-aware NLL. It also adds a SUPPORT2 dropout-ablation example, API docs, and 21 synthetic tests that cover gradients, a hand-computed censoring likelihood, input validation, dataloader integration and checkpointing. The model looks correct and well tested. It is a self-contained addition, so I don't think a maintainer needs to review it for core-code reasons.

Coverage meets the bar. personalized_survival_moe.py is at 98.97% and models/__init__.py at 100%, against the 80% per-file guideline. The whole repository is at 73.36%, above the 60% bar.

To get this merged:

  1. The example duplicates helpers that already exist in the repo. examples/support2_survival_personalized_moe.py re-implements the SUPPORT2 loading in load_data and the reverse Kaplan-Meier helper censoring_km. examples/survival_mdn_data.py (read_support2) and examples/survival_mdn_metrics.py (km_fit) already cover these. Please reuse them where you can.
  2. If the tie handling or the 0.01 floor in your KM really has to differ to match the reference implementation, keep a thin wrapper or option around the shared helper. Alternatively, add a short comment explaining why a separate version is needed.

Optional suggestions:

  • getattr(..., None) in load_data silently turns a missing SUPPORT2 column into an imputed value. Raising an error would be safer.
  • model.load_state_dict(state) fails with state=None if no epoch gives a finite validation NLL. A clear error would help.
  • A missing categories kwarg raises KeyError. A ValueError would match the other validation errors.
  • The headline results are hardcoded in the docstring, and no output file is committed. Consider committing the JSON output or noting how to reproduce it.
  • SurvivalEVAL is an undeclared dependency. Please say so clearly in the example's docstring or in the dependency metadata.
  • The example's IPCW Brier/ECE code is untested. That is acceptable, but a small test would be nice.

Reviewed commit ff11d23

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

status: automatic The PR will go through automatic review process.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants