ECG & Accelerometry (EDF) Preprocessing Pipeline
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
--helpto 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 pressCtrl + 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
.kerasfile intomodels, and setauto_download_model: falseinconfig.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
plotsdirectory with the PDF report, and aprocessed_datadirectory with detailed CSVs. - Top-level files: an aggregated
df_info_summary.csv.gzplus 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)
| File | Size | Uploaded | |
|---|---|---|---|
| cardiacprep-1.0.0.tar.gz | 83.4 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| 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 logRelease 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