Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,12 @@ data/
*.parquet
*.lzo

# ...but keep bundled ship-in-wheel data (e.g. disease-effect catalog).
# Only re-include source files; pycache stays ignored via the global rule above.
!synthlab/data/
!synthlab/data/*.py
!synthlab/data/*.csv

# OS
.DS_Store
Thumbs.db
Expand Down
73 changes: 73 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,79 @@ df = load_meds_events("~/.cache/synthlab/meds/synthea_100")

Install the optional extra: `pip install synthlab[meds]`.

### Olink NPX simulator (NEW)

Simulate case/control Olink proteomics data with LOD-driven missingness
and configurable group effects — the first greenfield open-source
simulator targeted at Olink's NPX / PEA readout (existing tools like
[MSstatsSampleSize](https://bioconductor.org/packages/MSstatsSampleSize/)
target LC-MS/MS, and [OlinkAnalyze](https://github.com/Olink-Proteomics/OlinkRPackage)
ships demo data but no simulator). Priors reflect UKB-PPP
([Sun et al. 2023](https://www.nature.com/articles/s41586-023-06592-6))
and OlinkAnalyze `npx_data1` / `npx_data2` baseline distributions:

```python
from synthlab import OlinkSimConfig, default_explore_3072_panel, simulate_olink_npx

cfg = OlinkSimConfig(
n_samples=500,
panel=default_explore_3072_panel(),
group_effects={"CRP": {"case": 1.8}, "IL6": {"case": 1.2}},
group_assignments=["case"] * 250 + ["control"] * 250,
seed=42,
)
df = simulate_olink_npx(cfg)
```

See [`synthlab/olink.py`](synthlab/olink.py) for the full API
(`OlinkPanelConfig`, `OlinkSimConfig`, `simulate_olink_npx`,
`default_explore_3072_panel`, `write_olink_parquet`,
`load_olink_parquet`).

#### Disease-conditional effect catalog (NEW)

Rather than hand-pick effect sizes, plug in a curated, source-cited
catalog of per-disease protein NPX shifts mined from published plasma
proteomics literature. Every row in
[`synthlab/data/olink_disease_effects.csv`](synthlab/data/olink_disease_effects.csv)
cites a real DOI — so a downstream user always knows *where* a given
effect-size estimate came from. See
[`docs/olink_disease_catalog.md`](docs/olink_disease_catalog.md) for the
schema and a "how to add a new disease" checklist.

```python
from synthlab import (
OlinkSimConfig, default_explore_3072_panel, simulate_olink_npx,
load_disease_effect_catalog,
)

catalog = load_disease_effect_catalog() # bundled with package
print(catalog.diseases()) # ('Alzheimer', 'BRCA_hereditary', 'CAD', 'CKD', 'Cancer_broad', 'IBD', 'T2D')
effects = catalog.effects_for(["T2D", "CAD"]) # {protein: {disease: delta_npx}}
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)
```

Disease-group row ranges in the shipped CSV (see
[`synthlab/data/olink_disease_effects.csv`](synthlab/data/olink_disease_effects.csv)):

- **T2D**: rows 2-9 ([Sun et al. 2023 UKB-PPP](https://doi.org/10.1038/s41586-023-06592-6),
[Sun et al. 2018 INTERVAL](https://doi.org/10.1038/s41586-018-0175-2))
- **CAD**: rows 10-16 ([Williams et al. 2022 Sci Transl Med](https://doi.org/10.1126/scitranslmed.abj9625),
[Eldjarn et al. 2023 deCODE](https://doi.org/10.1038/s41586-023-06563-x))
- **Cancer (broad)**: rows 17-22 ([Cohen et al. 2018 CancerSEEK Science](https://doi.org/10.1126/science.aar3247))
- **BRCA hereditary**: rows 23-26 (null-hypothesis placeholders from
[Ahn et al. 2021 Cancers](https://doi.org/10.3390/cancers13102300))
- **Alzheimer's**: rows 27-31 ([Guo et al. 2024 Nat Aging](https://doi.org/10.1038/s43587-023-00565-0))
- **CKD**: rows 32-37 ([Dubin et al. 2023 Nat Comm CRIC](https://doi.org/10.1038/s41467-023-41642-7))
- **IBD**: rows 38-44 ([Hu et al. 2025 Nat Comm UKB-PPP](https://doi.org/10.1038/s41467-025-57879-3))

## Installation

```bash
Expand Down
139 changes: 139 additions & 0 deletions docs/olink_disease_catalog.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
# Olink disease-effect catalog

This document walks through the schema, sourcing rules, and contribution
checklist for
[`synthlab/data/olink_disease_effects.csv`](../synthlab/data/olink_disease_effects.csv) —
the curated per-disease protein NPX shift catalog that powers
[`synthlab.olink.load_disease_effect_catalog`](../synthlab/olink.py).

The goal is to give users **realistic, source-cited** effect sizes for
simulating disease cohorts with
[`synthlab.simulate_olink_npx`](../synthlab/olink.py), without requiring
them to re-mine the literature themselves.

## Schema

| Column | Type | Meaning |
| ------------------- | -------- | ------- |
| `disease` | `str` | Canonical disease label (e.g. `T2D`, `CAD`, `Alzheimer`, `CKD`, `IBD`, `Cancer_broad`, `BRCA_hereditary`). Match these labels to the `group_assignments` you feed into `OlinkSimConfig`. |
| `protein_uniprot` | `str` | UniProt accession or gene symbol. Match these to the `proteins` field of your `OlinkPanelConfig`. |
| `delta_npx` | `f64` | **log2 NPX-unit mean shift** of cases vs a demographically-matched baseline. Negative values are allowed (e.g. adiponectin goes down in T2D). |
| `se_delta` | `f64` | Standard error of the delta, reflecting between-study / between-cohort variability. Used by `effects_for(noise_sd=...)` to draw Monte-Carlo perturbations. |
| `source` | `str` | Human-readable study tag, e.g. `"Sun et al. 2023 UKB-PPP (N=54,219) Nature"`. |
| `doi` | `str` | Real DOI (starts with `10.`). **Fabricated DOIs are not allowed** — if you cannot find one for a given protein/disease association, omit the row. |
| `evidence_strength` | `str` | One of `"strong"` / `"moderate"` / `"weak"`. See the rubric below. |
| `meta` (optional) | `str` | Free-form note — e.g. "largest-N meta-estimate", "MR-supported causal locus", or which of several papers the estimate was chosen from. |

Every row **must** provide the first seven columns. `meta` is optional but
strongly encouraged.

## Effect-size units

Deltas are in **log2 NPX units** — the native scale used across all Olink
[Explore](https://olink.com/products/olink-explore) panels. A `delta_npx`
of `1.0` means cases sit at roughly `2x` the median baseline (log2(2) =
1). A `delta_npx` of `-0.5` means cases sit at roughly `0.7x` the baseline.

### Converting from other scales

- **Fold change** (e.g. microarray log2 fold change is already compatible):
`delta_npx = log2(fold_change)`. So a reported 2-fold change maps to
`delta_npx = 1.0`; a 4-fold change maps to `delta_npx = 2.0`.
- **Odds ratio** (from logistic GWAS / PRS work): approximately,
`delta_npx = log2(OR) * (protein_sd)` — but this is often misleading
because it doesn't translate a case/control association into a plasma
NPX shift. Prefer studies that directly report case-baseline NPX
differences.
- **Z-scored deltas** (reported as "effect in SD units"): multiply by
the protein's baseline NPX SD (typically ~0.5-0.8 NPX units on
Olink Explore).

### Defensibility sanity check

Keep magnitudes in a range the real assay has been observed to span:

| Scenario | Plausible `delta_npx` |
| ------------------------------------------ | --------------------- |
| CRP in sepsis / active severe infection | +3.0 to +4.0 |
| CRP in subclinical CAD / metabolic disease | +0.3 to +0.5 |
| GDF15 in advanced CKD or heart failure | +0.7 to +1.2 |
| NEFL in mild cognitive impairment | +0.4 to +0.8 |
| NEFL in clinically diagnosed AD | +0.8 to +1.2 |
| Null hypothesis (no real effect) | 0.0 +/- 0.3 |

If you're recording a delta > 3.0, double-check the source.

### Standard-error guidance

- If the source reports a 95% CI for the delta, set
`se_delta = (upper - lower) / (2 * 1.96)`.
- If the source gives only a point estimate and the study is large
(N >= 10,000), set `se_delta = 0.15` (2x the typical UKB-PPP per-protein
SE).
- If the study is small (N < 3,000) or the effect is drawn from a single
paper, set `se_delta = 0.3` (generic conservative default).

## Evidence-strength rubric

| Label | Criteria |
| ----------- | -------- |
| `strong` | Replicated in >= 2 large cohorts (N >= 5,000 each), OR supported by Mendelian randomization / pQTL causal inference in a large-N study, OR used clinically today (e.g. CRP for IBD activity). |
| `moderate` | Single large cohort (N >= 5,000) with clear effect, OR multiple smaller cohorts (N < 5,000) in agreement. |
| `weak` | Single small study, or mechanistic inference only, or null-hypothesis placeholder (e.g. pre-symptomatic hereditary cancer carriers where no consistent plasma signature exists). |

Weak rows are fine to include, but their downstream use should assume the
noise term dominates — e.g. set `noise_sd=1.0` in
`DiseaseEffectCatalog.effects_for` so the delta is meaningfully perturbed
during Monte-Carlo sampling.

## How to add a new disease / row

Checklist for a PR touching
[`olink_disease_effects.csv`](../synthlab/data/olink_disease_effects.csv):

- [ ] **Source**: I have a published paper (journal or preprint) with a
real DOI. I've verified the DOI resolves at `https://doi.org/<mydoi>`.
No synthesized DOIs.
- [ ] **Unit conversion**: I've converted the source's reported effect
into log2 NPX units per the table above, and the resulting magnitude
is within the defensibility range.
- [ ] **SE choice**: `se_delta` follows the standard-error guidance
(explicit 95% CI -> computed; large-N point estimate -> 0.15; otherwise
0.3).
- [ ] **Evidence tag**: `evidence_strength` is set per the rubric.
- [ ] **No duplicates**: `(disease, protein_uniprot)` is unique within the
CSV. If multiple papers report an estimate for the same protein +
disease, take the largest-N meta-estimate and record which paper in the
`meta` column. Use multiple rows only when you truly want independent
draws from N(delta, se_delta^2) for a single protein + disease cell.
- [ ] **Disease label**: either matches an existing label in `diseases()`,
OR is a new canonical short label you've introduced consistently across
all rows for that disease.
- [ ] **Tests**: I've added a test that loads the new rows via
`load_disease_effect_catalog()` and asserts the expected disease +
protein coverage.
- [ ] **Docs**: I've updated the per-disease row range in
[`README.md`](../README.md) if a new disease was added.

## Example: adding a hypothetical "post-COVID" disease

```csv
post_covid,CRP,0.55,0.20,"Made-up et al. 2025 Made-up Journal",10.0/example.post-covid,moderate,placeholder only
post_covid,IL6,0.70,0.25,"Made-up et al. 2025 Made-up Journal",10.0/example.post-covid,moderate,placeholder only
post_covid,GDF15,0.45,0.25,"Made-up et al. 2025 Made-up Journal",10.0/example.post-covid,moderate,placeholder only
```

*(In a real PR, the DOI would be real — this is for illustration only.)*

## Open questions / deferred work

- **Covariate-adjusted effects**: Currently deltas are marginal (cases vs
baseline, aggregated across age / sex / ancestry). A future rev could
ship age- or BMI-conditional deltas, which would plug into an extended
simulator that accepts per-sample covariate effects.
- **Longitudinal effects**: For incidence-cohort simulations, the delta
grows over time pre-diagnosis. A future rev could add
`time_to_dx_years` -> `delta_npx` curves per (disease, protein).
- **Interaction terms**: Some effects depend on comorbidities (e.g. GDF15
in T2D with CKD is larger than T2D alone). Future rev: an interactions
CSV that layers on top of the marginal catalog.
41 changes: 41 additions & 0 deletions notebooks/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# SynthLab notebooks

Demos and walkthroughs for the [SynthLab](https://github.com/bschilder/synthlab)
synthetic-healthcare-data toolkit.

## Index

| Notebook | Module | Description |
| -------- | ------ | ----------- |
| [`olink_demo.ipynb`](olink_demo.ipynb) | [`synthlab.olink`](../synthlab/olink.py) | Olink NPX proteomics simulator — generate, analyse, visualise case vs control with LOD missingness + plate batch effects. |
| [`Synthea.ipynb`](Synthea.ipynb) | [`synthlab.synthea`](../synthlab/synthea.py) | Run Synthea and convert CSV to OMOP. |
| [`Coherent_MultimodalDataset.ipynb`](Coherent_MultimodalDataset.ipynb) | [`synthlab.coherent`](../synthlab/coherent.py) | Load and explore the Synthea Coherent multimodal dataset. |
| [`Generate_MultimodalDataset.ipynb`](Generate_MultimodalDataset.ipynb) | [`synthlab.coherent`](../synthlab/coherent.py) | Generate multimodal synthetic cohorts. |
| [`MedGemma_SOAP_Notes.ipynb`](MedGemma_SOAP_Notes.ipynb) | [`synthlab.soap`](../synthlab/soap.py) | MedGemma-based SOAP note generation with causal graph analysis. |
| [`SNOMED_Entity_Linking.ipynb`](SNOMED_Entity_Linking.ipynb) | [`synthlab.snomed`](../synthlab/snomed.py) | SNOMED entity linking with SapBERT and FAISS. |
| [`UKBiobank_Synthetic.ipynb`](UKBiobank_Synthetic.ipynb) | [`synthlab.download_ukbiobank_synthetic`](../synthlab/download_ukbiobank_synthetic.py) | Download and explore the UK Biobank Synthetic Dataset. |

## Running the Olink demo

The Olink NPX demo notebook needs the `viz` optional dependency group
(`matplotlib`, `seaborn`, `scikit-learn`, `umap-learn`). Install alongside the
core SynthLab package:

```bash
pip install 'synthlab[viz]'
```

Then open [`olink_demo.ipynb`](olink_demo.ipynb) in Jupyter — it runs
end-to-end in under a minute on CPU with no external data dependencies.

## Reproducing `olink_demo.ipynb`

The notebook is assembled from
[`_build_olink_demo.py`](_build_olink_demo.py) so the cell layout stays
reviewable under version control. After editing the builder:

```bash
python notebooks/_build_olink_demo.py # rebuild cells
jupyter nbconvert --to notebook --execute --inplace \\
notebooks/olink_demo.ipynb # embed outputs
```
Loading