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 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/
| 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.