Motivation¶
Several Python libraries exist for reading EDF files, but none adequately address the requirements of modern biosignal workflows — particularly long-duration clinical recordings, rigorous timestamp alignment for machine learning, and memory constraints at scale. edfplus was built to fill these gaps.
Discontinuous recordings (EDF+D)¶
The EDF+ specification defines two recording modes: continuous (EDF+C) and discontinuous (EDF+D). Discontinuous mode is essential for polysomnography (PSG) and other long-duration clinical studies — recordings that routinely exceed 10 hours — where pauses occur due to bathroom breaks, patient restlessness, impedance checks, or equipment adjustments.
Some existing libraries (e.g., pyedflib) do not support EDF+D at all. Others silently concatenate data records across temporal gaps, producing a continuous array that misrepresents the actual timeline and corrupts any downstream analysis that depends on accurate sample timing.
edfplus correctly parses EDF+D time-stamped annotation lists (TALs) to identify discontinuities and exposes them through a straightforward API, preserving the true temporal structure of the recording.
Reading a discontinuous file
Timezone-aware timestamps¶
The EDF+ specification is ambiguous regarding timezones: the startdate field is described as local time at the recording site, but no formal mechanism exists to encode the timezone itself. When patient data is anonymized and recording location is redacted — standard practice for research datasets — the timezone becomes unrecoverable from the file alone.
Existing libraries typically handle this by either assuming UTC or returning naive datetime objects, leaving the burden of timezone resolution entirely on the caller. For machine learning and predictive modeling on biosignals, this is insufficient. Ground-truth labels, environmental covariates, and other time-aligned data sources must share a consistent temporal reference. Misaligned timeseries make supervised learning, forecasting, and cross-modal fusion impossible.
edfplus accepts an explicit tzinfo parameter at read time, attaching timezone information to all parsed datetimes and enabling correct absolute timestamp recovery for every sample in the recording.
Attaching timezone information
from datetime import timezone, timedelta
from edfplus import read_edf
# Recording was made in US Eastern Time
eastern = timezone(timedelta(hours=-5))
with read_edf("recording.edf", tzinfo=eastern) as edf:
# All datetimes are now timezone-aware
print(edf.header.start_datetime)
# datetime.datetime(2024, 3, 15, 22, 30, 0, tzinfo=...)
signal = edf["EEG Fp1"]
_, timestamps = signal.load_with_timestamps(
onset=0.0, duration=10.0, time_format="datetime"
)
# timestamps are absolute, timezone-aware datetime64 values
This approach makes it straightforward to align EDF signal data with external event logs, actigraphy, environmental sensors, or any other timestamped data source — a prerequisite for multi-modal ML pipelines.
Memory efficiency at scale¶
High-density PSG recordings generate large files. Consider a typical clinical configuration: 30 signals sampled at 512 Hz for 10 hours. Read into NumPy as the default float64 dtype:
Once per-sample timestamps are computed alongside the signal data, the baseline memory footprint doubles to approximately 8.8 GB — just for the raw arrays.
In practice, the situation is worse. Any ETL or ML pipeline holds the raw signals in memory simultaneously with normalized copies, windowed transforms, spectral features, and other derived representations. A single file can easily consume dozens of gigabytes across the stages of a feature engineering pipeline. This is why controlling what gets loaded — and how — is critical for production biosignal workflows.
edfplus provides fine-grained control over memory consumption:
Configurable dtype¶
Pass a smaller floating-point type to halve (or further reduce) memory usage while retaining physical unit scaling:
import numpy as np
from edfplus import read_edf
with read_edf("large_study.edf", dtype=np.float32) as edf:
# Samples are scaled to physical units but stored as float32
# Memory usage: ~2.2 GB instead of ~4.4 GB
samples = edf["EEG Fp1"].samples
Raw digital access¶
Bypass physical scaling entirely and work with the native int16 samples stored in the file:
from edfplus import read_edf
with read_edf("large_study.edf", physical=False) as edf:
# Raw int16 values — 2 bytes per sample
# Memory usage: ~1.1 GB for 30 channels × 512 Hz × 10 hours
raw = edf["EEG Fp1"].samples
This is particularly useful for preprocessing pipelines that apply their own normalization, or for feeding data into models that expect integer inputs.
Selective signal loading¶
Rather than materializing all 30 channels at once, load only the signals your pipeline actually needs:
from edfplus import read_edf
with read_edf("large_study.edf", dtype=np.float32) as edf:
# Load only the channels relevant to your model
eeg = edf["EEG Fp1"]
ecg = edf["ECG"]
# The other 28 channels are never read into memory
Lazy and streaming reads¶
By default, edfplus does not load samples into memory until they are accessed. Combined with slice-based loading, this enables streaming workflows that process recordings in chunks without ever materializing the full dataset:
import numpy as np
from edfplus import read_edf
with read_edf("large_study.edf", dtype=np.float32) as edf:
signal = edf["EEG Fp1"]
# Process in 30-second epochs — only one epoch in memory at a time
for epoch_start in range(0, 36000, 30):
chunk = signal.load(start=float(epoch_start), stop=float(epoch_start + 30))
# Process chunk...
This prevents OOM errors in memory-constrained environments such as containerized ML pipelines, CI runners, and edge devices. The combination of selective signal access, configurable dtypes, and streaming reads means edfplus can handle arbitrarily large recordings without requiring the entire file to fit in RAM.