Skip to main content

MESSY_synth

Python 3.12+ Tests Code Quality Contributors Pull Requests License

Generate synthetic raw data that matches the shape a MEDS-Extract v0.7 MESSY config expects — then run the whole ETL over it, with no access to the real dataset.

A MESSY file is a complete, config-only ETL. It declares where the raw data comes from and how each raw table becomes MEDS events, written in the dftly expression DSL. What it never declares is what the raw tables look like — those live behind a credentialed download.

MESSY_synth runs that reasoning backwards. It reads the config's expressions, deduces what the source files must have contained for those expressions to make sense, and writes a directory of synthetic files with the right filenames, columns, dtypes, string formats, and keys.

That gets you two things:

  • A runnable demo of any ETL, with no credentials and no data-use agreement.
  • A smoke test, so a change to a MESSY config can be checked in CI: regenerate, run, and fail the build if any event stopped producing rows.

Install

Not yet published to PyPI — install from source:

uv add git+https://github.com/mmcdermott/MESSY_synth

Or for development:

git clone https://github.com/mmcdermott/MESSY_synth
cd MESSY_synth
uv sync --group dev
uv run pre-commit install

Quick start

# Generate synthetic sources for an ETL, then look at what it inferred.
MESSY-synth path/to/messy.yaml -o raw_synthetic/ --explain

# Run the real ETL over them.
meds-extract-run spec=path/to/messy.yaml output_dir=meds_out/ \
	do_download=false input_dir=raw_synthetic/

From Python, synthesize does the inference, generation, writing, and validation in one call:

>>> from MEDS_extract.config import MessyConfig
>>> from MESSY_synth import synthesize
>>> cfg = MessyConfig.parse({
...     "etl": {"dataset_name": "Demo", "raw_dataset_version": "1"},
...     "patients": {
...         "_defaults": {"subject_id": "$pid"},
...         "dob": {"code": "MEDS_BIRTH", "time": '$dob::"%Y-%m-%d"'},
...         "sex": {"code": 'f"SEX//{$sex}"', "time": None},
...     },
...     "labs": {
...         "_defaults": {"subject_id": "$pid"},
...         "lab": {
...             "code": 'f"LAB//{$itemid}"',
...             "time": '$ts::"%Y-%m-%d %H:%M:%S"',
...             "numeric_value": "$value",
...             "_metadata": {"d_labitems": {"itemid": "$itemid", "description": "$label"}},
...         },
...     },
... })
>>> with tempfile.TemporaryDirectory() as d:
...     result = synthesize(cfg, d, seed=0, n_subjects=10, rows_per_subject=2)
...     print_directory(Path(d))
├── _MESSY_synth_manifest.json
├── d_labitems.csv
├── labs.csv
└── patients.csv

Three files, because the config mentions three: two event tables and the _metadata dictionary that labs looks its codes up in. The generated labs table carries exactly the columns the config reads, with ts rendered in the format the config parses it with:

>>> with tempfile.TemporaryDirectory() as d:
...     result = synthesize(cfg, d, seed=0, n_subjects=10, rows_per_subject=2)
...     result.dataset.frames["labs"].head(3)
shape: (3, 4)
┌─────┬──────────────────┬─────────────────────┬────────┐
│ pid ┆ itemid           ┆ ts                  ┆ value  │
│ --- ┆ ---              ┆ ---                 ┆ ---    │
│ i64 ┆ str              ┆ str                 ┆ f64    │
╞═════╪══════════════════╪═════════════════════╪════════╡
│ 1   ┆ SYNTH_ITEMID_000 ┆ 2011-01-28 14:10:11 ┆ 73.623 │
│ 2   ┆ SYNTH_ITEMID_005 ┆ 2013-02-17 19:59:30 ┆ 54.308 │
│ 3   ┆ SYNTH_ITEMID_001 ┆ 2012-06-21 15:58:27 ┆ 73.642 │
└─────┴──────────────────┴─────────────────────┴────────┘

What it guarantees

Structural validity is not a per-column property. A MEDS-Extract run only yields a real dataset if values line up across files, and every one of those relationships is silent when it breaks — a dangling join key nulls subject_id and the row is dropped with no error at all.

So the generator maintains four invariants:

Invariant Why it matters
Every table's subject column draws from one shared subject universe Otherwise events never merge onto shared subjects
Every join key exists on the far side MEDS-Extract joins are left joins; a dangling key silently deletes the row
Every _metadata dictionary covers its event's code vocabulary Otherwise the metadata join matches nothing and descriptions come back null
Each subject gets a coherent timeline — birth, then events, then death MEDS treats birth/death as record bounds, and downstream tools assume the ordering

The first three are one mechanism: a union-find over (table, column) pairs groups every column that must agree, and each group gets a single shared value pool.

How it infers types

Expectations flow down the dftly AST from each MEDS output slot — time must yield a timestamp, numeric_value a number, code a string — and each node says how its own expectation constrains its children. Column references are not always leaves: a _table.cols name is expanded in place, and a name pulled in by _table.join is attributed to the table it actually lives in.

>>> from MESSY_synth import infer_constraints
>>> cs = infer_constraints(cfg)
>>> for col in ("pid", "ts", "itemid", "value"):
...     c = cs.get("labs", col)
...     print(f"{col:<8} {c.kind.name:<13} {c.datetime_formats}")
pid      SUBJECT_ID    ()
ts       DATETIME_STR  ('%Y-%m-%d %H:%M:%S',)
itemid   CATEGORICAL   ()
value    NUMERIC       ()

Some of what it deduces is less obvious than a dtype:

  • strptime formats become the literal characters written to disk. A column parsed with coalesce($x::?"%Y-%m-%d %H:%M:%S", $x::?"%Y-%m-%d") gets rows in both formats, so both branches of the coalesce are exercised.
  • Compared-against literals are seeded into the value pool and fix the column's type. $admissioncount == 1 means the column is an integer that must sometimes equal 1 — otherwise the branch it gates never fires, and generating a string there makes polars raise outright.
  • Regexes are solved, not ignored. AmsterdamUMCdb derives every timestamp in its admissions table from extract /2003|2010/ from $admissionyeargroup. Opaque tokens there yield null and delete every event in the table, so MESSY_synth generates conforming values — and, for regexes that merely test a column, a mix of matching and non-matching ones.
  • Magnitudes implied by arithmetic. ($anchor_year - $anchor_age)::str parsed as a year only works if the first term looks like a calendar year. The default 1–100 range would make every birth event vanish.
  • Birth and death roles. A column feeding a MEDS_BIRTH or MEDS_DEATH timestamp is placed on the subject's timeline rather than sampled.

--explain prints all of it, per column, with the reasoning attached.

Smoke-testing an ETL

smoke_test generates, runs meds-extract-run, and then checks the artifacts — not just the exit status. That distinction is the point: MEDS-Extract exits 0 when a lenient time cast nulls every row of a table, and exits 0 when re-run into a populated output directory without executing a single stage.

from MESSY_synth import smoke_test


def test_etl_still_works(tmp_path):
    result = smoke_test("src/MY_MEDS/configs/messy.yaml", tmp_path)
    assert result.ok, result.summary()

The CLI exits non-zero on any error finding, so it works directly in CI:

MESSY-synth src/MY_MEDS/configs/messy.yaml -o "$RUNNER_TEMP/raw" || exit 1

Validated against real ETLs

demo/smoke_matrix.py runs the whole pipeline across a set of configs. Against the v0.7 ETL configs available at the time of writing:

ETL Result Output from synthetic input
MEDS-Extract example pass 185 events, 16 subjects, 64 codes (40 described)
MIMIC-IV pass 2,827 events, 40 subjects, 635 codes (312 described)
NWICU pass 1,447 events, 40 subjects, 275 codes (130 described)
SICdb pass 851 events, 40 subjects, 250 codes
AmsterdamUMCdb pass 1,294 events, 40 subjects, 711 codes
HiRID fails — config bug raw_stage/observation_tables.datetime is read both via strptime and directly as a timestamp
INSPIRE fails — config bug operations declares a self-join, which MEDS-Extract 0.7 rejects

The last two are the tool working as intended. Neither config can run on any input, real or synthetic, and MESSY_synth reports each with the specific column or table at fault rather than a stack trace. eICU has no v0.7 MESSY config yet, so it is not represented.

Limitations

  • Values are structurally valid, not clinically plausible. A numeric column defaults to the range 1–100 unless the config implies otherwise; use -R col=lo:hi where that matters.
  • Only regex syntax that can be run backwards is supported. Lookarounds and backreferences fall back to opaque tokens, which may make a regex-gated branch produce nothing. The dry run reports it when that happens.
  • A table is treated as subject-level if it declares any static event, and gets one row per subject. That is right for a patients table and approximate for anything else.
  • Nothing here validates clinical semantics. An ETL that runs green on synthetic data can still be wrong about the real dataset's contents.

Contributing

See CONTRIBUTORS.md. Doctests are first-class here — most of the examples in this README and in the package docstrings run as part of the test suite.

License

MIT. See LICENSE.

Metadata

Release files for MESSY-synth 0.0.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for MESSY-synth 0.0.1
File Size Uploaded Provenance
messy_synth-0.0.1.tar.gz 136.3 kB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for MESSY-synth 0.0.1
File Interpreter ABI Platform
messy_synth-0.0.1-py3-none-any.whl Python 3 none any Details

Total release size: 208.3 kB

Release files / messy_synth-0.0.1.tar.gz

Download URL messy_synth-0.0.1.tar.gz
Size 136.3 kB
Tags Source
SHA-256 checksum
How to use checksums
5337dae9b30cec64eb2b6e99da31f04ae7534473e2bd459603e47e8ebe06effe
BLAKE2b-256 checksum
How to use checksums
1421f8b73ae57c490f6acb7bd1d81a20a19ae9074a05edd37f3cd93771828ee3
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.13

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 21, 2026.

Transparency log

Release files / messy_synth-0.0.1-py3-none-any.whl

Download URL messy_synth-0.0.1-py3-none-any.whl
Size 72.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
794715d9def951cb5f2190ac7b43503cf3b92ad54365e89dd326792e2635e580
BLAKE2b-256 checksum
How to use checksums
31b7f2c13e1ee98e2972d46654d97442b9467df2aaf322eb284fd83069b22d8e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.13

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 21, 2026.

Transparency log

Security for MESSY-synth 0.0.1

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for messy_synth-0.0.1.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 21, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.0.1 This release

2 release files Full provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page