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