Predicts later food allergy in infants from parent-captured eczema photographs plus questionnaire data, in the CANDO birth cohort.
The design is a concept bottleneck, not an end-to-end CNN. A frozen pretrained
encoder scores each photo on the four EASI signs (erythema, papulation, excoriation,
lichenification); those scores are aggregated per infant by body site and fed, with
tabular predictors, into a penalized logistic regression. The reason is sample size:
at ~2000 infants and ~10% outcome prevalence the Riley et al. criteria support roughly
11-17 predictor parameters, so the model capacity has to sit in a frozen encoder
rather than in fitted weights. See docs/ for the full architecture and the data
request that unblocks it.
data/: Raw, interim, and processed datasets. (Not tracked in Git)src/: Source code for the project.
src/data/: REDCap export parsing, image normalization, QC. Implemented.src/models/: Aggregation and the outcome model. Not yet built.src/cli/: Command-line interface.docs/: Architecture notes and the data request spec.outputs/: Model artifacts, logs, and figures. (Not tracked in Git)config.yaml: Configuration settings for the pipeline.docs/HANDOFF.md is the complete context document: goal, verified literature, data state,
architecture and its justification, every module built, measured results, the remaining phases,
and the traps already hit. Start there.
git config core.hooksPath .githooks
This repo is public and this project handles identifiable patient photos.
.gitignore keeps data//outputs/ out of accidental commits, but the hook
is the real backstop -- it hard-blocks any commit containing a path under
data//outputs/, an image/media file anywhere in the repo, or an unusually
large file, even if force-added. See .githooks/pre-commit.uv): uv venvuv sync (dependencies live in pyproject.toml; uv.lock
pins the exact graph). The frozen-encoder model stack is a separate extra, so the
parsing pipeline and test suite install without a multi-GB torch download --
add it later with uv sync --extra models.data.source in config.yaml at your REDCap export folder (root directory
plus the export's base filename, shared by its .csv/.r/report-folder trio).python -m src.cli.main --step parsepython -m src.cli.main --step embed (needs
uv sync --extra models; downloads the encoder named in config.yaml on first run)python -m src.cli.main --step validate-encoderpython -m src.cli.main --step conceptspython -m src.cli.main --step robustnessThis reads the REDCap CSV export and its .r data dictionary and file-upload
manifest (never modifying the source), and writes a cleaned, model-agnostic dataset
under data/interim/ and data/processed/ (both git-ignored):
data/processed/canonical/: EXIF-oriented, colour-normalized (sRGB) imagesdata/processed/images.parquet: one row per photo -- body site, laterality,
age-at-photo, visit index, QC metricsdata/processed/clinical.parquet: decoded, tidied clinical datadata/processed/dictionary.json, site_ontology.json: machine-readable
data dictionary and body-site adjacency graphdata/processed/qc_report.html: flagged images/records plus a contact sheetdata/interim/exif/: the complete original EXIF for every photo, verbatimThe embed step adds data/processed/embeddings.parquet: one row per photo holding a
frozen-encoder vector (MONET by default, 768-d), keyed by image_id and stamped with
the encoder name and the canonical file's content hash. Re-runs only re-embed images
whose pixels actually changed. These vectors are the input to the concept layer.
--step validate-encoder is an opt-in sanity check that the embeddings carry real
signal rather than merely the right shape: it compares within- vs between-group cosine
similarity for patients and body regions, and tests whether text prompts can recover
the body-site labels the parser already derived. Its output is aggregate-only by
construction -- no image_id, record_id, or filename ever reaches the report, which
tests/features/test_validate.py enforces. The JSON lands under outputs/, which is
git-ignored and blocked by the pre-commit hook.
--step concepts writes data/processed/concepts.parquet: one row per photo with a
raw and a within-cohort-percentile score for each of the four EASI signs (erythema,
papulation, excoriation, lichenification) plus scale and crust. These are not EASI
grades. Photographs cannot supply EASI's area score, and a CLIP similarity difference
has no natural zero, so mapping these onto 0-3 requires clinician annotation. Until
then they are an ordering, not a measurement.
--step robustness is the gate that decides whether those scores are usable at all. It
re-embeds photometrically perturbed copies of the real images and reports the Spearman
correlation between each perturbed ordering and the unperturbed one. Rank stability is
the metric because the ordering is the entire predictive content passed downstream. The
report also compares each concept's stable band against the cohort's observed
brightness spread, so a failure at sweep extremes can be judged against the lighting
that actually occurs.
Re-running the parse step is incremental: unchanged photos are skipped, and any
REDCap schema drift (added/removed CSV columns) between runs is reported, not
silently absorbed. Run pytest to run the test suite (all tests use synthetic
fixtures -- no patient data required).
Python
89.9%
Shell
10.1%
Predicts later food allergy in infants from parent-captured eczema photographs plus questionnaire data, in the CANDO birth cohort.
The design is a concept bottleneck, not an end-to-end CNN. A frozen pretrained
encoder scores each photo on the four EASI signs (erythema, papulation, excoriation,
lichenification); those scores are aggregated per infant by body site and fed, with
tabular predictors, into a penalized logistic regression. The reason is sample size:
at ~2000 infants and ~10% outcome prevalence the Riley et al. criteria support roughly
11-17 predictor parameters, so the model capacity has to sit in a frozen encoder
rather than in fitted weights. See docs/ for the full architecture and the data
request that unblocks it.
data/: Raw, interim, and processed datasets. (Not tracked in Git)src/: Source code for the project.
src/data/: REDCap export parsing, image normalization, QC. Implemented.src/models/: Aggregation and the outcome model. Not yet built.src/cli/: Command-line interface.docs/: Architecture notes and the data request spec.outputs/: Model artifacts, logs, and figures. (Not tracked in Git)config.yaml: Configuration settings for the pipeline.docs/HANDOFF.md is the complete context document: goal, verified literature, data state,
architecture and its justification, every module built, measured results, the remaining phases,
and the traps already hit. Start there.
git config core.hooksPath .githooks
This repo is public and this project handles identifiable patient photos.
.gitignore keeps data//outputs/ out of accidental commits, but the hook
is the real backstop -- it hard-blocks any commit containing a path under
data//outputs/, an image/media file anywhere in the repo, or an unusually
large file, even if force-added. See .githooks/pre-commit.uv): uv venvuv sync (dependencies live in pyproject.toml; uv.lock
pins the exact graph). The frozen-encoder model stack is a separate extra, so the
parsing pipeline and test suite install without a multi-GB torch download --
add it later with uv sync --extra models.data.source in config.yaml at your REDCap export folder (root directory
plus the export's base filename, shared by its .csv/.r/report-folder trio).python -m src.cli.main --step parsepython -m src.cli.main --step embed (needs
uv sync --extra models; downloads the encoder named in config.yaml on first run)python -m src.cli.main --step validate-encoderpython -m src.cli.main --step conceptspython -m src.cli.main --step robustnessThis reads the REDCap CSV export and its .r data dictionary and file-upload
manifest (never modifying the source), and writes a cleaned, model-agnostic dataset
under data/interim/ and data/processed/ (both git-ignored):
data/processed/canonical/: EXIF-oriented, colour-normalized (sRGB) imagesdata/processed/images.parquet: one row per photo -- body site, laterality,
age-at-photo, visit index, QC metricsdata/processed/clinical.parquet: decoded, tidied clinical datadata/processed/dictionary.json, site_ontology.json: machine-readable
data dictionary and body-site adjacency graphdata/processed/qc_report.html: flagged images/records plus a contact sheetdata/interim/exif/: the complete original EXIF for every photo, verbatimThe embed step adds data/processed/embeddings.parquet: one row per photo holding a
frozen-encoder vector (MONET by default, 768-d), keyed by image_id and stamped with
the encoder name and the canonical file's content hash. Re-runs only re-embed images
whose pixels actually changed. These vectors are the input to the concept layer.
--step validate-encoder is an opt-in sanity check that the embeddings carry real
signal rather than merely the right shape: it compares within- vs between-group cosine
similarity for patients and body regions, and tests whether text prompts can recover
the body-site labels the parser already derived. Its output is aggregate-only by
construction -- no image_id, record_id, or filename ever reaches the report, which
tests/features/test_validate.py enforces. The JSON lands under outputs/, which is
git-ignored and blocked by the pre-commit hook.
--step concepts writes data/processed/concepts.parquet: one row per photo with a
raw and a within-cohort-percentile score for each of the four EASI signs (erythema,
papulation, excoriation, lichenification) plus scale and crust. These are not EASI
grades. Photographs cannot supply EASI's area score, and a CLIP similarity difference
has no natural zero, so mapping these onto 0-3 requires clinician annotation. Until
then they are an ordering, not a measurement.
--step robustness is the gate that decides whether those scores are usable at all. It
re-embeds photometrically perturbed copies of the real images and reports the Spearman
correlation between each perturbed ordering and the unperturbed one. Rank stability is
the metric because the ordering is the entire predictive content passed downstream. The
report also compares each concept's stable band against the cohort's observed
brightness spread, so a failure at sweep extremes can be judged against the lighting
that actually occurs.
Re-running the parse step is incremental: unchanged photos are skipped, and any
REDCap schema drift (added/removed CSV columns) between runs is reported, not
silently absorbed. Run pytest to run the test suite (all tests use synthetic
fixtures -- no patient data required).
Python
89.9%
Shell
10.1%