Skip to main content

ECG & Accelerometry (EDF) Preprocessing Pipeline

CI Docs Python 3.9–3.12 Licence: Academic Use

An end-to-end Python pipeline for processing multi-day wearable sensor data from .EDF files. It extracts heart rate, heart rate variability (HRV) and physical activity metrics, performs quality control and imputation, and generates summary reports and visualisations.

📖 Full documentation: cardiac-prep.readthedocs.io

You do not need to be a programmer to use this. Follow the steps in order, copying each command exactly.


🚦 The three commands

Once set up, this is the whole pipeline. Run them in this order:

1. Process your recordings

cardiac-prep process

Reads every .edf file in input_data and writes results to output. About 30 seconds per file.

2. Summarise the whole dataset

cardiac-prep summarise

Creates population-level plots across all participants.

3. Inspect one participant (optional)

cardiac-prep inspect --list
cardiac-prep inspect --subject NAME_FROM_THE_LIST

Add --show to open the plots in a window as well as saving them.

First time here? Do the Setup below first. Add --help to any command to see its options.


Features ✨

  • Parallel Processing: Uses all CPU cores except one, so your machine stays usable (no GPU needed). Configurable via n_processes.
  • Preprocessing: QRS detection, artifact removal, signal quality assessment.
  • Data Imputation: Linear interpolation for short gaps, time-of-day averaging for longer ones.
  • Comprehensive Metrics:
    • Heart Rate (Resting, Min, Max, Average)
    • Heart Rate Variability (RMSSD and log-normalised RMSSD to account for HR)
    • Physical Activity (time in Sedentary, Light, Moderate and Vigorous zones)
  • Automated Reporting: A 2-page PDF summary report per participant.
  • Dataset Summarisation: Population-level trend plots.

Before you start 📝

You need Python 3.9, 3.10, 3.11 or 3.12. Python 3.13 is not yet supported.

To check what you have, open a terminal and type:

python --version

If that says "command not found", or shows a version outside the range, install Miniforge from https://conda-forge.org/download/ and reopen your terminal.

Opening a terminal: on macOS press Cmd + Space, type "Terminal", press Enter. On Windows press Start, type "Miniforge Prompt", press Enter. On Linux press Ctrl + Alt + T.


Setup 🚀

You only do this once.

Step 1 – Create a separate environment

An environment is a private space for this project's software, so it cannot clash with anything else on your computer. Pick ONE option.

Option A – conda (recommended)

conda create -n cardiacprep python=3.11 -y
conda activate cardiacprep

Option B – venv (built into Python)

python -m venv .venv

Activate it. On macOS or Linux:

source .venv/bin/activate

On Windows:

.venv\Scripts\activate

✅ How to tell it worked: your prompt now starts with (cardiacprep) or (.venv).

⚠️ Activate the environment every time you open a new terminal. If your prompt does not show the name in brackets, run the activate line again.

Step 2 – Install the pipeline

pip install cardiacprep

This takes several minutes. It is finished when your prompt reappears.

Step 3 – Set up a folder for your study

Go to wherever you want your results to live, then:

cardiac-prep init

This creates config.yaml plus the input_data, output and models folders, and prints the handful of settings worth checking before you start.

Step 4 – The heartbeat detector

The pipeline uses a machine-learning model to find heartbeats. It is too big for this repository, so it is downloaded separately.

You do not need to do anything here. The first run fetches it into the models folder, checks it against a known checksum, and carries on.

To fetch it now instead, useful before working somewhere with no wifi:

cardiac-prep download

⚠️ No internet on the machine doing the processing? Fetch the model elsewhere, copy the .keras file into models, and set auto_download_model: false in config.yaml.

Step 5 – Check it works

cardiac-prep --help

If you see a list of options, setup is complete. 🎉


Running it 🏃

Put your .edf files into the input_data folder, then use the three commands above.

To preview what will be processed without processing anything:

cardiac-prep process --dry-run

To use folders elsewhere on your computer:

cardiac-prep process --input /path/to/my/edfs --output /path/to/my/results

Changing settings ⚙️

All settings live in config.yaml. Open it in any text editor. Every setting is explained in comments and every one is optional, so deleting a line restores its default.

You should never need to edit a .py file.

Setting What it does
input_dir, output_dir Where recordings are read from and results written to
n_processes How many recordings to process at once. Set to 1 for clearer error messages
night_start_hour, night_end_hour The hours treated as overnight rest (see below)
activity_*_mg Movement cut-points separating activity intensity levels

Mistakes are reported clearly and stop the run immediately, before anything is processed.


Outputs 📂

An ./output/ directory containing:

  • Participant folders: a plots directory with the PDF report, and a processed_data directory with detailed CSVs.
  • Top-level files: an aggregated df_info_summary.csv.gz plus dataset-wide summary plots.

Every row of df_info_summary.csv.gz records model_file and model_sha256, identifying exactly which detector produced those results. Failed recordings get failed = 1 and a failure_reason explaining why.

Reading *_df_qc.csv.gz

One row = one 10-second segment of the recording. The index is seconds from the start, so row 0 is 00:00:00–00:00:10, row 10 is the next segment, and so on.

Column group Meaning
device_worn, clipped_5perc_thrs, passed_initialQC Per-segment screening: was the device worn, was the signal saturated, did it pass basic signal checks
passed_finalQC Whether the segment's heartbeats were good enough to trust
N_beats, N_RR, rr_Cover, rr_sd, rr_outliers, qrs_snr, qrs_amp, rmssd Beat-level measurements
*_raw The measured value, blank wherever QC failed
*_imputed The gap-filled value. Use these for analysis
*_isImputed True where that value was filled rather than measured

Why are the beat columns so often blank? They are only computed for segments that were actually analysed. A segment is skipped when the device was not worn, the signal was too noisy or clipped to pass initial checks, or the detector found fewer than n_beats_min beats. Separately, RRm_raw is deliberately blanked wherever passed_finalQC is False, so blank means "not measurable here", not "missing data".

Lots of blanks therefore means a lot of non-wear time or poor signal quality, and is expected rather than a bug. To judge quality fairly, look at prop_ECG_worn_passed_finalQC in the summary file: it measures quality among worn time only, so it is not diluted by periods when the device was off.


Things to be aware of ⚠️

Resting heart rate and HRV use a fixed 21:00–09:00 window. Participants who sleep outside those hours, such as shift workers or someone who regularly sleeps 02:00–11:00, will have these metrics computed from the wrong hours or not at all. You can move the window in config.yaml, but it is one fixed window applied to everybody.

Planned improvement: detect each participant's main sleep period from their own accelerometer data instead of assuming a clock window.

Activity cut-points come from older adults. The thresholds are from Etzkorn et al. (2024), derived in 381 adults with a median age of 78. Interpret cautiously in younger populations.


A note on HRV normalisation

This pipeline uses the natural logarithm of RMSSD to normalise HRV. This removes the mathematical influence of a person's average heart rate, allowing fairer comparisons. For a detailed explanation, see Should we normalize HRV by heart rate? by Marco Altini.


If something goes wrong 🔧

What you see What to do
command not found: conda Install Miniforge, then close and reopen your terminal
No module named ... Your environment is not active. Run conda activate cardiacprep
No '*.keras' file found The detector model is missing. Redo Setup Step 4
Found 2 model files Remove the spare .keras file from models
No .edf files found Your recordings are not in input_data, or do not end in .edf
Configuration problem: ... A mistake in config.yaml. The message names the setting
One file failed The others still process. Rerun with --jobs 1 to see the error clearly

For developers 🛠️

Work from a clone rather than the released package:

git clone https://github.com/OxWearables/cardiac-prep.git
cd cardiac-prep && pip install -e ".[dev]"
pytest

Tests use synthetic data only, so no recordings or model weights are needed. GitHub Actions runs the tests and ruff check . on Python 3.9–3.12 for every push.

A clone also has process.py, summarise_dataset.py and plot_subject.py at the root, which run the same three subcommands without installing anything.

See the developer documentation for the layout, how the docs are built, and how releases are made.


Licence

Academic Use Licence, see LICENSE. Free for academic, non-commercial research and teaching. Commercial use requires a separate licence - contact Oxford University Innovation at enquiries@innovation.ox.ac.uk.

Copyright © 2026, University of Oxford.

Authors

  • Stefan van Duijvenboden
  • Anna Bator

Metadata

Release files for cardiacprep 1.0.0

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

Source distribution (sdist)

Source distribution for cardiacprep 1.0.0
File Size Uploaded
cardiacprep-1.0.0.tar.gz 83.4 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for cardiacprep 1.0.0
File Interpreter ABI Platform
cardiacprep-1.0.0-py3-none-any.whl Python 3 none any Details

Total release size: 147.2 kB

Release files / cardiacprep-1.0.0.tar.gz

Download URL cardiacprep-1.0.0.tar.gz
Size 83.4 kB
Tags Source
SHA-256 checksum
How to use checksums
0506a1eecc634d3679b009b918503fe928ee4b6ae1346c42a182ae1898b0908d
BLAKE2b-256 checksum
How to use checksums
fced4ceb2497080025b6c0bb33ba6441add9f31289e7b0e25e3d68051610a09c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

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 Aug 13, 2026.

Transparency log

Release files / cardiacprep-1.0.0-py3-none-any.whl

Download URL cardiacprep-1.0.0-py3-none-any.whl
Size 63.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
f3207170a1f3383a1c8ad689af0461d5e4e9449cc3f196ac862ddaa0a191368f
BLAKE2b-256 checksum
How to use checksums
9c2925180a2ee281ff7d2488ca15872ac6a25238df4e1d63c101ac31b684e59f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

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 Aug 13, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

1.0.0 This release

2 release files

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