Skip to content

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

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.

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.

ecgbench related ptb-xl
ecgbench related mimic_iv_ecg --format json

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.