CLI¶
Nine subcommands, each with a matching Python entry point. ecgbench/cli/_main.py
builds the root parser and dispatches on args.func; every subcommand module
exposes a public run_X(...) taking plain keyword arguments, so anything the CLI
can do is importable — see Pipelines for their generated
signatures.
Installing ecgbench adds a single ecgbench console command with nine subcommands:
ecgbench --help # top-level help
ecgbench <command> --help # per-subcommand flags
ecgbench --version # package version
| Subcommand | Purpose |
|---|---|
splits |
Full pipeline -- validate signals, generate 10-fold splits, export CSVs, and write Croissant metadata |
croissant |
Generate Croissant 1.1 JSON-LD for an already-split dataset directory |
upload |
Upload fold CSVs and metadata to HuggingFace Hub (requires ecgbench[hf]) |
list |
List every dataset with its implementation state, merged from the catalogue and the configs |
search |
Ranked full-text search (FTS5 syntax) over every dataset, with structured filters |
info |
Show one dataset's merged metadata; accepts either slug or the display name |
fields |
List a dataset's label columns with type, unit, vocabulary and description |
related |
Show a dataset's relationships to others, with the shares_records leakage flag |
metadata |
Rebuild or verify (build --check) the derived metadata files |
Every subcommand has an equivalent Python function (run_splits, run_croissant, run_upload, run_list, run_search, run_info, run_fields, run_related, run_metadata_build, run_metadata_check) with the same arguments, so the same workflow can be driven from a notebook or downstream code.
ecgbench splits¶
Runs the full pipeline: validate -> split -> export -> Croissant. Writes output/<dataset>/{original,clean}/ by default.
ecgbench splits --dataset ptbxl --data-path /path/to/ptb-xl/1.0.3/
ecgbench splits --dataset ptbxl # auto-download
ecgbench splits --dataset chapman_shaoxing \
--data-path /data/chapman/ \
--output-dir /data/outputs/chapman/ \
--n-folds 10 --max-workers 8
# PhysioNet ecg-arrhythmia (45,152 records, Chapman-Shaoxing + Ningbo).
# Ships no metadata CSV — the splitter builds ecgbench_metadata.csv from the
# per-record WFDB headers on first run, so the data directory must be writable.
ecgbench splits --dataset ecg_arrhythmia \
--data-path /data/ecg-arrhythmia/1.0.0/ --max-workers 32
| Flag | Type | Default | Description |
|---|---|---|---|
--dataset |
str | required | Dataset slug — see list_available_configs() (e.g. ptbxl, ecg_arrhythmia, mimic_iv_ecg_demo) |
--data-path |
path | auto-download | Path to the dataset root directory |
--output-dir |
path | output/<dataset>/ |
Output directory for fold CSVs + metadata |
--sampling-rate |
int | config default | Sampling rate to validate against |
--n-folds |
int | 10 |
Number of cross-validation folds |
--max-workers |
int | 4 |
Parallel workers for signal validation |
--skip-validation |
flag | off | Skip signal validation (faster; no quality flags) |
--skip-croissant |
flag | off | Skip Croissant metadata generation |
Python equivalent:
import ecgbench
result = ecgbench.run_splits(
dataset="ptbxl",
data_path="/path/to/ptb-xl/1.0.3/",
output_dir=None, # -> output/ptbxl/
sampling_rate=None, # -> config default_sampling_rate
n_folds=10,
max_workers=4,
skip_validation=False,
skip_croissant=False,
)
# result is a dict with: dataset, dataset_name, output_dir,
# original={total,train,val,test}, clean={total,train,val,test}, excluded
ecgbench croissant¶
Standalone Croissant 1.1 JSON-LD generator for an existing splits directory. Run once per version (clean and original).
ecgbench croissant --dataset ptbxl --splits-dir output/ptbxl/clean/ --version clean
ecgbench croissant --dataset ptbxl --splits-dir output/ptbxl/original/ --version original
ecgbench croissant --dataset ptbxl --splits-dir output/ptbxl/clean/ --validate
| Flag | Type | Default | Description |
|---|---|---|---|
--dataset |
str | required | Dataset slug |
--splits-dir |
path | required | Version directory to scan (e.g. output/ptbxl/clean/) |
--output |
path | <splits-dir>/croissant.json |
Where to write the JSON-LD |
--version |
clean|original |
clean |
Version label to record in the Croissant file |
--validate |
flag | off | Validate the file after writing (non-zero exit if invalid) |
Python equivalent:
from pathlib import Path
import ecgbench
saved_path: Path = ecgbench.run_croissant(
dataset="ptbxl",
splits_dir="output/ptbxl/clean/",
output=None, # -> splits_dir/croissant.json
version="clean",
validate=True, # raises RuntimeError if the file does not validate
)
Requires the croissant extra (pip install ecgbench[croissant]).
ecgbench upload¶
Uploads each dataset's original/ and clean/ CSV folds, plus validation_report.json and croissant.json if present, to a HuggingFace Hub dataset repository. One or more dataset slugs can be uploaded in a single call.
ecgbench upload --data-dir output/ --datasets ptbxl
ecgbench upload --data-dir output/ --datasets ptbxl chapman_shaoxing
ecgbench upload --data-dir output/ --datasets ptbxl --dry-run
ecgbench upload --data-dir output/ --datasets ptbxl \
--hf-repo-id your-org/ECGBench
| Flag | Type | Default | Description |
|---|---|---|---|
--data-dir |
path | required | Root directory containing per-dataset subdirectories |
--datasets |
list | required | One or more dataset slugs to upload |
--hf-repo-id |
str | vlbthambawita/ECGBench |
Target HuggingFace dataset repo ID |
--dry-run |
flag | off | Print the files that would be uploaded, without uploading |
Authentication resolves in this order: token= argument (Python API only) -> HF_TOKEN env var -> HUGGINGFACE_HUB_TOKEN env var -> .env file in the current working directory. Run with --dry-run first to review the file list.
Python equivalent:
import ecgbench
counts: dict[str, int] = ecgbench.run_upload(
data_dir="output/",
datasets=["ptbxl", "chapman_shaoxing"],
hf_repo_id="vlbthambawita/ECGBench",
dry_run=False,
token=None, # falls back to env / .env
)
# counts: {"ptbxl": 42, "chapman_shaoxing": 42}
Requires the hf extra (pip install ecgbench[hf]).
ecgbench list¶
One row per dataset — all 64 catalogue entries, not only the implemented ones — merged from the catalogue front matter and the YAML configs. The state column is derived, not declared: catalogue_only (no config), config (config but no label loader), config_labels (labels available, fold CSVs not published) or published (labels available and fold CSVs on the Hub).
ecgbench list
ecgbench list --state published
ecgbench list --category two-lead --format csv
ecgbench list --format json | jq '.[] | select(.signal.leads == 2) | .dataset_id'
| Flag | Type | Default | Description |
|---|---|---|---|
--state |
catalogue_only|config|config_labels|published |
all | Keep only datasets in this implementation state |
--category |
str | all | Keep only one catalogue category (e.g. two-lead) |
--format |
table|json|csv |
table |
Output format; json writes one document to stdout and nothing else |
ecgbench search¶
Ranked full-text search over all 64 datasets — name, aliases, keywords, description, institution and the full text of each dataset page — with structured filters. The query is SQLite FTS5 syntax passed through verbatim: words are ANDed, fib* is a prefix, "..." is a phrase, NOT/OR/AND combine terms. Results are ranked by bm25 with the name weighted highest, then a small prior that puts an implemented dataset ahead of a catalogue-only entry at near-equal relevance. Omit the query to filter only.
ecgbench search "atrial fib*" # prefix: afdb, ltafdb, shdb_af, …
ecgbench search "holter NOT paediatric" --leads 2 # boolean + structured filter
ecgbench search '"sleep apnea"' # phrase (quote it for the shell too)
ecgbench search --fs 500 --patient-id --published # structured only
ecgbench search brazil --format json | jq '.[].dataset_id'
A query containing punctuation must be quoted as a phrase — ecgbench search '"ptb-xl"' — because ptb-xl is a column reference in FTS5 syntax; the error message says so.
| Flag | Type | Default | Description |
|---|---|---|---|
QUERY |
str | none | FTS5 query; omit to filter only |
--leads |
int | any | Exact lead count |
--fs |
int | any | A sampling rate the release ships |
--signal-format |
str | any | wfdb, csv, edf, mat, hdf5, … |
--access |
open|credentialed|restricted |
any | Access type |
--license |
str | any | Substring of the licence name or URL |
--category |
str | any | Exact catalogue category |
--state |
implementation state | any | catalogue_only, config, config_labels, published |
--min-records / --max-records |
int | any | Bounds on the parsed record count (unparsed counts are excluded) |
--labels / --no-labels |
flag | any | Whether a label loader exists |
--patient-id / --no-patient-id |
flag | any | Whether folds are patient-grouped |
--published / --no-published |
flag | any | Whether fold CSVs are on the Hub |
--limit |
int | all | Keep at most N results |
--format |
table|json|csv |
table |
Output format; the table shows rank and bm25 score |
When this Python's SQLite lacks FTS5, or the index is unavailable, the query falls back to a case-insensitive substring match over the same fields and a warning says so once.
ecgbench info¶
Everything ECGBench knows about one dataset. The argument is any alias — the dashed catalogue slug (ptb-xl), the underscored config slug (ptbxl) or the display name — and an unknown one exits non-zero naming the closest matches. A dagger (†) marks a value on which the catalogue and the config disagree; --verbose lists every fact with its source file, most trustworthy first.
ecgbench info mit-bih-arrhythmia-database # same record as: ecgbench info mitdb
ecgbench info ptbxl --verbose
ecgbench info echonext --format json
| Flag | Type | Default | Description |
|---|---|---|---|
dataset |
str | required | Dataset id or any alias |
--verbose |
flag | off | Append every fact with its source and path |
--format |
table|json |
table |
Output format |
ecgbench fields¶
The columns load_labels() returns for a dataset, as declared data: name, Frictionless Table Schema type (string, integer, number, boolean, datetime, array[string], …), unit, closed vocabulary where one exists, whether the value can be missing, and a description that spells out sentinels and encodings (PTB-XL's age of 300 meaning over 89, MIMIC's p_onset becoming NaN where the source held 29999). Declarations live in each label module's FIELDS tuple, or in the config's labels.fields: block for declarative datasets, and a test pins them to the loader's actual output. Field names and descriptions are indexed for ecgbench search, so ecgbench search recorder finds MIT-BIH.
ecgbench fields ptbxl # superclasses array[string] NORM, MI, STTC, CD, HYP ...
ecgbench fields mitdb --format json
ecgbench fields afdb --format frictionless # Frictionless Table Schema, primaryKey = record_name
| Flag | Type | Default | Description |
|---|---|---|---|
dataset |
str | required | Dataset id or any alias |
--format |
table|json|csv|frictionless |
table |
Output format |
The inventory is being declared in batches; a dataset whose labels exist but are not yet declared says so, and --format json returns [] for it.
ecgbench related¶
The relationships declared in the catalogue, both directions, with the shares_records flag that matters for leakage: yes means the two datasets contain the same recordings, so training on one and evaluating on the other contaminates the test set.
Python equivalents:
import ecgbench
meta = ecgbench.get_metadata("mit-bih-arrhythmia-database") # DatasetMeta; same as "mitdb"
meta.signal.leads, meta.access.license_text, meta.implementation_state
ecgbench.search_metadata("holter", leads=2, access="open") # list[DatasetMeta]
ecgbench.related_metadata("ptbxl") # list[RelationMeta]
# or the exact CLI equivalents
ecgbench.run_list(state="published")
ecgbench.run_search("atrial fib*", leads=2, limit=5)
ecgbench.run_info("ptb-xl")
ecgbench.run_fields("ptbxl") # tuple[FieldMeta, ...]
ecgbench.run_related("ptbxl")
ecgbench records¶
One row per record: the fold table ECGBench published for a dataset, joined with the labels load_labels() reads from the source dataset under --data-path. Fold CSVs come from the Hub by default; --splits-dir output/<slug>/ reads the tree ecgbench splits wrote instead, which is the only way for a withheld dataset. --sql runs DuckDB over the view records with file-system access disabled, so a query can read the view and nothing else; --hub queries a published folds.csv in place on the Hub through DuckDB's httpfs (identifiers only — labels never leave the source dataset).
ecgbench records ptbxl --data-path /data/ptb-xl --split val --format csv
ecgbench records ptbxl --data-path /data/ptb-xl \
--sql "select sex, count(*) as n from records where fold = 9 group by 1"
ecgbench records mimic_iv_ecg --splits-dir output/mimic_iv_ecg --no-labels --format parquet --output mimic.parquet
ecgbench records ptbxl --hub --sql "select fold, count(*) from records group by 1 order by 1"
| Flag | Type | Default | Description |
|---|---|---|---|
dataset |
str | required | Dataset id or any alias |
--data-path |
path | auto-resolve | Local copy of the source dataset, where the labels live |
--splits-dir |
path | — | Local fold tree (output/<slug>/); implies --source local |
--source |
hf|local |
hf (local with --splits-dir) |
Where the fold CSVs come from |
--version |
clean|original |
clean |
Dataset version |
--split |
train|val|test |
all | Restrict to one split |
--fold |
int… | all | Restrict to these folds; with --split they must belong to it |
--no-labels |
flag | off | Fold table only |
--sql |
str | — | DuckDB SQL over the view records (needs ecgbench[analytics]) |
--hub |
flag | off | Query the published folds.csv on the Hub in place; no labels |
--format |
table|json|csv|parquet |
table |
Output format (parquet needs --output) |
--output |
path | stdout | Write here instead of printing |
--limit |
int | 20 for table, all otherwise |
Rows to print; 0 = all |
Python equivalents:
import ecgbench
df = ecgbench.load_records("ptbxl", data_path="/data/ptb-xl", split="val") # pandas DataFrame
ecgbench.query_records(df, "select sex, count(*) as n from records group by 1")
ecgbench.run_records("ptbxl", data_path="/data/ptb-xl", sql="select count(*) from records")
ecgbench mcp¶
An MCP server over the metadata layer, for agents. Five tools, each a thin wrapper on the same store the CLI reads: search_datasets (an FTS5 query plus the filters of ecgbench search: leads, fs, signal_format, access, license, category, state, min_records, max_records, has_labels, has_patient_id, published), list_datasets, get_dataset (the full record by any id, slug or display name), list_fields (the declared label columns) and related_datasets (the leakage edges). The server reads the bundled metadata.json / metadata.sqlite only — no records, signals or network — so it can be given to any agent without a data path. A bad query or an unknown dataset comes back as a tool error carrying the same message the CLI prints, close matches included.
pip install ecgbench[mcp]
ecgbench mcp # stdio, what MCP clients spawn
claude mcp add ecgbench -- ecgbench mcp # Claude Code
Any other client registers the command ecgbench with arguments ["mcp"] over stdio. Asked for "two-lead Holter datasets", an agent calls search_datasets with query="holter" and leads=2 and gets the ten MIT-BIH-family long-term sets back, best match first.
| Flag | Type | Default | Description |
|---|---|---|---|
--transport |
stdio|sse|streamable-http |
stdio |
Transport to serve on |
Python equivalents:
import ecgbench
server = ecgbench.build_mcp_server() # an MCPServer / FastMCP with the five tools
ecgbench.run_mcp(transport="stdio") # same as `ecgbench mcp`
from ecgbench.metadata.mcp_server import ToolFunctions
ToolFunctions().search_datasets("holter", leads=2) # the tools, without the SDK
ecgbench metadata build¶
Maintenance of the derived metadata files. ecgbench/data/metadata.json is committed and metadata.sqlite (the FTS5 index) is generated; both are compiled from the catalogue front matter and the YAML configs, and the packaging hook rebuilds them into every wheel. Run build after editing any docs/_datasets/*.md or config, and build --check in CI: it exits 1 and names the added, removed and changed dataset ids when the committed export no longer matches a fresh build.
ecgbench metadata build # rewrite metadata.json (if changed) and metadata.sqlite
ecgbench metadata build --check # exit 1 if metadata.json is stale; prints the diff
ecgbench metadata build --output /tmp/meta/
ecgbench metadata snapshot¶
The recomputed numbers of a split run — record counts before and after validation, the per-check failure counts, the fold count, the seed and the fold_digest over the whole record-to-fold mapping — reach the metadata layer through a snapshot, ecgbench/data/snapshots/<slug>.json, committed alongside the configs. snapshot derives one from the manifest.json and validation_report.json that ecgbench splits leaves in output/<slug>/, keeping counts and digests and never a record identifier (the report's excluded_records block is the one thing it does not copy, so a snapshot can ship for a dataset whose fold CSVs may not). The build turns each snapshot into facts with manifest and validation_report provenance, which outrank the catalogue: ecgbench info <id> shows the recomputed record count and info --verbose lists the catalogue's figure beside it with the run's timestamp.
ecgbench splits --dataset mitdb --data-path /path/to/mitdb/
ecgbench metadata snapshot --dataset mitdb # output/mitdb/ -> ecgbench/data/snapshots/mitdb.json
ecgbench metadata snapshot --all # every output/*/ tree
ecgbench metadata snapshot --output-dir /elsewhere/mitdb/ --dest /tmp/mitdb.json
ecgbench metadata build # then rebuild the export
ecgbench metadata export¶
Three views over the same model. --croissant PATH writes one JSON-LD DataCatalog holding an MLCommons Croissant 1.1 Dataset per configured dataset: distribution names the published fold CSVs on the Hub with the SHA-256 the snapshot recorded, a RecordSet per version types the fold table's guaranteed columns, the declared label fields appear as variableMeasured, the run's input files as PROV-O wasDerivedFrom, and a dataset whose fold CSVs are withheld carries conditionsOfAccess and an ODRL prohibition on distribution instead of a distribution. --validate runs mlcroissant over every member and exits 1 on an error. --schema-org DIR writes one schema.org/Dataset block per dataset, and --website - writes docs/_data/metadata.json, the committed copy the dataset pages embed as JSON-LD and the catalogue search box reads field names from; ecgbench metadata build refreshes that copy too in a source checkout, and export --check (like build --check) exits 1 when it is stale.
ecgbench metadata export --croissant ecgbench-croissant.jsonld --validate
ecgbench metadata export --schema-org /tmp/schema-org/
ecgbench metadata export --website - # docs/_data/metadata.json
ecgbench metadata export --check
| Flag | Type | Default | Description |
|---|---|---|---|
--check |
flag | off | Verify instead of writing; non-zero exit on drift |
--output |
path | ecgbench/data/ |
Directory to write into (or, with --check, to verify) |
In a source checkout the index is also refreshed automatically: open_store() fingerprints the source files (mtime and size) and rebuilds when they changed, so ecgbench search never runs against a stale index there.