Skip to content

feat(olink): data-driven per-disease effect-size catalog - #3

Closed
bschilder wants to merge 3 commits into
mainfrom
feat/olink-disease-effect-catalog
Closed

feat(olink): data-driven per-disease effect-size catalog#3
bschilder wants to merge 3 commits into
mainfrom
feat/olink-disease-effect-catalog

Conversation

@bschilder

Copy link
Copy Markdown
Owner

Summary

Ships a curated, source-cited per-disease Olink effect-size catalog so users can simulate realistic disease cohorts with simulate_olink_npx without hand-coding effect sizes. Stacks on #2.

from synthlab import (
    OlinkSimConfig, default_explore_3072_panel, simulate_olink_npx,
    load_disease_effect_catalog,
)
catalog = load_disease_effect_catalog()
effects = catalog.effects_for(["T2D", "CAD"])
cfg = OlinkSimConfig(
    n_samples=900,
    panel=default_explore_3072_panel(),
    group_effects=effects,
    group_assignments=["T2D"]*300 + ["CAD"]*300 + ["baseline"]*300,
    seed=42,
)
df = simulate_olink_npx(cfg)

Coverage (43 rows × 7 diseases in synthlab/data/olink_disease_effects.csv)

Disease Proteins Primary source
T2D IGFBP2, LEP, ADIPOQ, IGFBP1, IL6, CRP, GDF15, TNF (8) Sun 2023 UKB-PPP Nature, Sun 2018 INTERVAL Nature
CAD NPPB, TNNI3, GDF15, MMP12, IL6, CRP, FABP4 (7) Williams 2022 Sci Transl Med, Eldjarn 2023 deCODE Nature
Cancer_broad MUC16, CEACAM5, GDF15, IL6, CRP, KRT19 (6) Cohen 2018 CancerSEEK Science
BRCA_hereditary MUC16, IL6, CRP, CEACAM5 (4, null-hypothesis placeholders) Ahn 2021 Cancers
Alzheimer NEFL, GFAP, CHIT1, GDF15, CRP (5) Guo 2024 Nat Aging UKB
CKD GDF15, REN, TFF3, CST3, SPP1, CRP (6) Dubin 2023 Nat Comm CRIC
IBD CRP, IL6, MMP9, LRG1, S100A8, S100A9, TNF (7) Hu 2025 Nat Comm UKB-PPP

Every row cites a real DOI; effect sizes are log2 NPX units (Olink's native scale); SE defaults to 0.3 when only one source is used.

Sourcing methodology

  • Real DOIs only — no fabricated placeholders. DOIs were verified against PubMed / publisher landing pages before inclusion.
  • Unit conversion — source-reported effect sizes were converted into log2 NPX units per the guidance in docs/olink_disease_catalog.md (fold change → log2; SDs → multiplied by baseline NPX sigma).
  • Magnitude defensibility — max |delta_npx| in the catalog is 1.20 (well within the observed Olink Explore range; sepsis CRP would peak at +3 to +4). A test enforces |delta| <= 3.0.
  • Evidence strength rubric: strong (replicated / MR-supported / clinical), moderate (single-N-large cohort), weak (small-N or null-hypothesis placeholder). See docs/olink_disease_catalog.md.
  • One source per row; if multiple papers cover the same (disease, protein) cell, the largest-N meta-estimate was used and the choice recorded in the meta column.

What shipped

File Notes
synthlab/data/olink_disease_effects.csv 43-row catalog, 8 columns (disease, protein_uniprot, delta_npx, se_delta, source, doi, evidence_strength, meta). UTF-8, no trailing whitespace.
synthlab/data/__init__.py Marks synthlab.data as a package so importlib.resources.files("synthlab.data") works.
synthlab/olink.py Adds DiseaseEffectCatalog (frozen dataclass) + load_disease_effect_catalog (importlib-resources-based loader). Full NumPy-style docstrings with doctest-style examples.
synthlab/__init__.py Re-exports the two new symbols.
tests/test_olink_disease_catalog.py 15 tests — schema, coverage, DOI shape, magnitude sanity, effects_for shape + errors, noise_sd determinism, end-to-end round-trip vs simulate_olink_npx, user-supplied CSV path, error paths.
notebooks/_build_olink_demo.py + notebooks/olink_demo.ipynb New Section 10 ("Disease-conditional generation") with catalog load, 3-group T2D/CAD/baseline cohort (300 samples each), top-5 grouped bar chart, catalog-vs-empirical scatter. Notebook cell count: 24 → 34, all outputs executed + embedded.
README.md Adds a "Disease-conditional effect catalog" subsection with per-disease DOI links.
docs/olink_disease_catalog.md New doc — full schema, unit-conversion / SE guidance, evidence-strength rubric, PR checklist for adding a new disease.
pyproject.toml package-data = ["py.typed", "data/*.csv"] — ships the CSV in the wheel.
.gitignore Carve-out for synthlab/data/*.csv + synthlab/data/*.py so they're tracked while other data/ and *.csv remain ignored.

Verification

$ pytest tests/test_olink_disease_catalog.py -xvs
... 15 passed in 2.86s

$ pytest tests/test_olink.py -q
... 16 passed in 2.89s   # PR #2 — no regression

$ python -c "from synthlab import load_disease_effect_catalog; cat = load_disease_effect_catalog(); print(cat.diseases())"
('Alzheimer', 'BRCA_hereditary', 'CAD', 'CKD', 'Cancer_broad', 'IBD', 'T2D')

$ python -c "import json; nb = json.load(open('notebooks/olink_demo.ipynb')); assert len(nb['cells']) > 24"
# OK (34 cells)

Test plan

  • pytest tests/test_olink_disease_catalog.py -xvs — 15/15 pass
  • pytest tests/test_olink.py -q — 16/16 pass (no PR feat(olink): greenfield NPX-style proteomics simulator #2 regression)
  • Round-trip test: load_disease_effect_catalog()effects_for(["T2D","CAD"])OlinkSimConfigsimulate_olink_npx(500 per arm) → per-row empirical mean shift within 3 SE of catalog delta_npx
  • Demo notebook executes end-to-end (20 code cells, 0 errors) with outputs embedded
  • importlib.resources.files("synthlab.data").joinpath("olink_disease_effects.csv") resolves the shipped CSV

Deferred follow-ups

  • Covariate-adjusted effects — age/sex/BMI/ancestry-conditional deltas. Current catalog rows are marginal case-vs-baseline.
  • Interaction terms — e.g. GDF15 in T2D+CKD is larger than T2D alone; would ship as a separate interactions CSV layered on top of the marginal catalog.
  • Longitudinal effectstime_to_dx_years → delta_npx curves for incidence-cohort simulations.
  • Expand disease coverage — stroke, heart failure, liver disease (MAFLD), sepsis, depression, Parkinson's. Contributions welcome following the PR checklist in docs/olink_disease_catalog.md.

Stacks on #2 via --base feat/olink-npx-simulator.

🤖 Generated with Claude Code

bschilder and others added 3 commits April 23, 2026 15:25
Adds synthlab/olink.py, a small greenfield simulator for Olink-style
proteomics data (subject x protein -> NPX). As of 2026-04 there is no
widely-used open-source Olink simulator — the closest analogue,
MSstatsSampleSize, targets LC-MS/MS peptide intensities rather than
NPX/PEA, and OlinkAnalyze ships demo tables (npx_data1/npx_data2) but
no simulator.

Model: NPX[i,j] = mean[j] + plate_eff[i] + group_shift[i,j] + eps
- plate_eff ~ N(0, plate_effect_sd^2), 96 samples / plate
- group_shift from configurable per-protein {group -> delta} map
- eps ~ N(0, sd[j]^2)

Missingness
- mnar_lod: drops 80% of sub-LOD values (soft LOD, mirrors real Olink
  behaviour where some sub-LOD values are still reported with
  QC_WARN flags)
- mcar: uniform missing_rate drop on top
- mar: currently aliases mcar; full MAR deferred to follow-up
- none: skips missingness

Priors come from aggregate UKB-PPP statistics (Sun et al. 2023,
Nature — 2,923 proteins x ~54k participants) and OlinkAnalyze
npx_data1/npx_data2 demo tables: mean ~ 5 log2-units, sd ~ 0.6,
LOD ~ mean - 2*sd.

Ships a 50-protein preset (default_explore_3072_panel) subsetted from
UKB-PPP Explore 3072, plus parquet read/write helpers. Full NumPy-
style docstrings on every public function / class / method. 16 unit
tests cover determinism, group-effects mean shift, LOD drop,
MCAR overlay, plate variation, QC rate, parquet round-trip, schema
types, validation errors, and empty-frame edge cases.

Scope-deferred (future PR): full MAR missingness, multi-plate batch
effects beyond a flat per-plate intercept, realistic PEA dilution
noise model, and panel-version LOD bridging (Explore 3072 ↔ HT).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds notebooks/olink_demo.ipynb — a 24-cell walkthrough of
synthlab.olink.simulate_olink_npx covering NPX distributions,
LOD-driven missingness, plate batch effects, PCA/UMAP, a
protein-protein correlation heatmap, a case-vs-control volcano
plot, and a parquet round-trip. Runs end-to-end in ~20s on CPU
with outputs embedded. Adds a notebooks/README.md index and a
reproducible builder script (notebooks/_build_olink_demo.py).

pyproject.toml picks up a new [viz] optional-dependency group
(matplotlib, seaborn, scikit-learn, umap-learn) — not a hard dep;
notebook users install via `pip install synthlab[viz]`.
Ships synthlab/data/olink_disease_effects.csv — a curated, source-cited
per-disease protein effect-size catalog covering 7 diseases × 43 rows
(Alzheimer, BRCA_hereditary, CAD, CKD, Cancer_broad, IBD, T2D). Every
row cites a real DOI (Sun et al. 2023 UKB-PPP, Williams et al. 2022
Sci Transl Med, Eldjarn et al. 2023 deCODE, Cohen et al. 2018
CancerSEEK, Dubin et al. 2023 Nat Comm CRIC, Guo et al. 2024 Nat
Aging, Hu et al. 2025 Nat Comm UKB-PPP, Ahn et al. 2021 Cancers).

Public API in synthlab.olink:
- DiseaseEffectCatalog (frozen dataclass wrapping the loaded frame
  with .diseases(), .proteins_for(), .effects_for(noise_sd, seed))
- load_disease_effect_catalog(path=None) — importlib.resources-based
  ship-in-wheel loader

Both re-exported from synthlab/__init__.py.

Tests (tests/test_olink_disease_catalog.py, 15 tests):
- schema, minimum disease coverage, DOI shape, defensible magnitudes
- effects_for shape, KeyError paths, noise determinism / perturbation
- end-to-end round-trip: catalog → effects_for → simulate_olink_npx,
  assert per-disease mean NPX shift matches catalog within 3 SE at
  500 samples/arm × 44 rows

Demo notebook (notebooks/olink_demo.ipynb) gains Section 10 —
disease-conditional generation — with loading, inspection, 3-group
cohort simulation (T2D/CAD/baseline, 300 samples each), top-5
grouped-bar chart, and a catalog-vs-empirical scatter. Rebuilt via
notebooks/_build_olink_demo.py; executed outputs embedded. Cell
count goes from 24 → 34.

Docs:
- README.md gains a "Disease-conditional effect catalog" subsection
  with per-disease row-range citations.
- docs/olink_disease_catalog.md — full schema, unit-conversion
  guidance, SE guidance, evidence-strength rubric, PR checklist
  for adding a new disease.

Ship-in-wheel plumbing:
- synthlab/data/__init__.py + data/*.csv in [tool.setuptools.package-data]
- .gitignore carve-out so synthlab/data/*.csv are tracked while other
  data/ and *.csv remain ignored.

Stacks on PR #2 (feat/olink-npx-simulator).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@bschilder
bschilder changed the base branch from feat/olink-npx-simulator to main April 23, 2026 21:28
@bschilder

Copy link
Copy Markdown
Owner Author

Superseded by #4 — rebased as a clean single-commit PR after #2's squash-merge orphaned this branch's base.

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