Skip to content

Add bootstrap CI and age adjustment to calculate_c_index - #214

Open
ipezygj wants to merge 1 commit into
bio-learn:masterfrom
ipezygj:feat/c-index-ci-and-age-adjustment
Open

Add bootstrap CI and age adjustment to calculate_c_index#214
ipezygj wants to merge 1 commit into
bio-learn:masterfrom
ipezygj:feat/c-index-ci-and-age-adjustment

Conversation

@ipezygj

@ipezygj ipezygj commented Aug 8, 2026

Copy link
Copy Markdown

Why

Two gaps in calculate_c_index, both acknowledged in the codebase itself: the docstring notes it works "without adjusting for age", and the returned point estimates carry no uncertainty. A C-index of 0.75 from 100 deaths and from 5,000 deaths support very different conclusions — and since chronological age alone predicts mortality, an unadjusted C-index rewards a clock merely for correlating with age (the concern behind #100's age-control question, applied to the C-index path).

What

Backwards-compatible — existing callers get the exact same output:

  • ci_bootstrap_samples=N: seeded percentile-bootstrap 95% CI per clock (CI95_low/CI95_high), subject-level resampling, resamples with zero observed events skipped.
  • adjust_for_age=True: C-index computed on the residuals of each predictor after regressing out age — mirroring the standardization calculate_mortality_hazard_ratios already applies. The adjusted value answers what does this clock add beyond chronological age.

Tests

biolearn/test/test_mortality_c_index.py (4 passing): exact parity with a direct lifelines.concordance_index call on the default path; CI brackets the estimate and reproduces under the seed; CI narrows with more subjects; and a synthetic age-plus-assay-noise clock whose adjusted C-index collapses to ~0.5 while a genuinely informative clock retains signal beyond age. (One test-design subtlety documented in the file: an exact age copy leaves only float-epsilon regression residuals that are still monotone in age, so the synthetic uses realistic assay noise.) test_model.py's collection error on my machine is a pre-existing missing-local-data issue, verified identical on the untouched base.

Context

I work on measurement-integrity tooling for evaluation claims. This is a first small piece of what I'd like to contribute here: reliability-aware reporting (ICC/SEM/minimal-detectable-change utilities, and registering a technical-replicate reference dataset like GSE55763) so that biolearn users can report clock changes against a detection floor. If that direction is welcome, I'll follow up — happy to shape it to the roadmap.

A C-index of 0.75 from 100 deaths and from 5,000 deaths support very
different conclusions, and chronological age alone predicts mortality -
so an unadjusted point estimate both hides its uncertainty and rewards a
clock merely for correlating with age.

- ci_bootstrap_samples=N adds seeded percentile-bootstrap 95% CI columns
  (subject-level resampling; resamples with zero events are skipped)
- adjust_for_age=True computes the C-index on residuals after regressing
  each predictor on chronological age, mirroring the standardization
  already used in calculate_mortality_hazard_ratios
- defaults unchanged: existing callers get the exact same output
- tests: parity with direct lifelines concordance_index, CI brackets and
  determinism, narrowing with n, and an age-only synthetic clock whose
  adjusted C-index collapses to 0.5 while a genuinely informative clock
  retains signal
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.

1 participant