# -*- coding: utf-8 -*-
from warnings import warn
import numpy as np
from ..misc import NeuroKitWarning, as_vector
from ..signal import signal_fillmissing, signal_filter
[docs]
def ppg_clean(ppg_signal, sampling_rate=1000, heart_rate=None, method="elgendi"):
"""**Clean a photoplethysmogram (PPG) signal**
Clean a raw PPG signal for analysis by filtering to remove noise. This improves the accuracy of subsequent analyses,
such as systolic peak detection.
* ``'elgendi'`` (default): Bandpass filter the signal between 0.5 and 8 Hz using a Butterworth filter.
* ``'nabian2018'``: Lowpass filter the signal below 40 Hz. If `heart_rate` is provided then the function
checks whether 40 Hz is at least 10 times the cardiac frequency and less than half of the sampling frequency, and
raises an error if not.
* ``'langevin2021'``: Bandpass filter between 0.7 and 3.5 Hz using a second-order Butterworth filter,
applied by Langevin et al. (2021) and Vorreuther et al. (2025) in an EmotiBit wearable PPG validation
study. Note that these filter parameters were not systematically optimized — they reflect the pipeline
used in a specific device-validation context. The upper cutoff (3.5 Hz) is narrower than ``'elgendi'``
(8 Hz), which may attenuate higher-frequency components. This method should be used primarily for research
purposes until further validation is conducted.
* ``'goda2024'``: Bandpass filter the signal between 0.5 and 12 Hz using a fourth-order Chebyshev Type II filter.
Parameters
----------
ppg_signal : Union[list, np.array, pd.Series]
The raw PPG channel.
heart_rate : Union[int, float]
The heart rate of the PPG signal. Applicable only if method is ``"nabian2018"`` to check
that filter frequency is appropriate.
sampling_rate : int
The sampling frequency of ``ppg_signal`` (in Hz, i.e., samples/second). The default is 1000.
method : str
The processing pipeline to apply. Can be one of ``"elgendi"`` (default), ``"nabian2018"``,
``"langevin2021"``, ``"goda2024"``, or ``"none"``. If ``"none"`` is passed, the raw signal
will be returned without any cleaning.
Returns
-------
clean : array
A vector containing the cleaned PPG.
See Also
--------
ppg_simulate, ppg_findpeaks
Examples
--------
.. ipython:: python
import neurokit2 as nk
import pandas as pd
import matplotlib.pyplot as plt
# Simulate and clean signal
ppg = nk.ppg_simulate(heart_rate=75, duration=30)
ppg_elgendi = nk.ppg_clean(ppg, method='elgendi')
ppg_nabian = nk.ppg_clean(ppg, method='nabian2018', heart_rate=75)
ppg_langevin = nk.ppg_clean(ppg, method='langevin2021')
# Plot and compare methods
signals = pd.DataFrame({'PPG_Raw' : ppg,
'PPG_Elgendi' : ppg_elgendi,
'PPG_Nabian' : ppg_nabian,
'PPG_Langevin' : ppg_langevin})
@savefig p_ppg_clean1.png scale=100%
signals.plot()
@suppress
plt.close()
References
----------
* Nabian, M., Yin, Y., Wormwood, J., Quigley, K. S., Barrett, L. F., & Ostadabbas, S. (2018).
An open-source feature extraction tool for the analysis of peripheral physiological data.
IEEE Journal of Translational Engineering in Health and Medicine, 6, 1-11.
* Langevin, A., Bégin, W., Lavallière, M., Beaulieu, L.-D., Menelas, B.-D. J., Gaboury, S.,
et al. (2021). “Criterion validation of an open-source wearable physiological sensors device,”
in Proceedings of the 9th International Conference on Sport Sciences Research and Technology
Support – icSPORTS (SciTePress), 95–105.
* Vorreuther, A., Tagalidou, N., & Vukelić, M. (2025). Validation of the EmotiBit wearable
sensor for heart-based measures under varying workload conditions. Front Neuroergonomics,
6, 1585469.
* M. Elgendi, I. Norton, M. Brearley, D. Abbott, and D. Schuurmans (2013).
Systolic peak detection in acceleration photoplethysmograms measured from emergency responders
in tropical conditions. PLoS ONE, 8(10), 1–11.
* M.A. Goda, P.H. Charlton, and J. Behar (2024).
pyPPG: a Python toolbox for comprehensive photoplethysmography signal analysis. Physiological Measurement,
45 (4), 045001. doi: https://doi.org/10.1088/1361-6579/ad33a2
"""
ppg_signal = as_vector(ppg_signal)
# Missing data
n_missing = np.sum(np.isnan(ppg_signal))
if n_missing > 0:
warn(
"There are " + str(n_missing) + " missing data points in your signal."
" Filling missing values using `signal_fillmissing`.",
category=NeuroKitWarning,
)
ppg_signal = signal_fillmissing(ppg_signal, method="both")
method = str(method).lower()
if method in ["elgendi", "elgendi2013"]:
clean = _ppg_clean_elgendi(ppg_signal, sampling_rate)
elif method in ["nabian2018"]:
clean = _ppg_clean_nabian2018(ppg_signal, sampling_rate, heart_rate=heart_rate)
elif method in ["langevin2021", "langevin"]:
clean = _ppg_clean_langevin2021(ppg_signal, sampling_rate)
elif method in ["goda", "goda2024"]:
clean = _ppg_clean_goda(ppg_signal, sampling_rate)
elif method in ["none"]:
clean = ppg_signal
else:
raise ValueError(
"`method` not found. Must be one of 'elgendi', 'nabian2018', 'langevin2021', 'goda2024', or 'none'."
)
return clean
# =============================================================================
# Methods
# =============================================================================
def _ppg_clean_elgendi(ppg_signal, sampling_rate):
"""Low-pass filter for continuous PPG signal preprocessing, adapted from Elgendi et al. (2013)."""
filtered = signal_filter(
ppg_signal,
sampling_rate=sampling_rate,
lowcut=0.5,
highcut=8,
order=2,
method="butterworth",
)
return filtered
def _ppg_clean_nabian2018(ppg_signal, sampling_rate, heart_rate=None):
"""Low-pass filter for continuous BP signal preprocessing, adapted from Nabian et al.
(2018).
"""
# Determine low-pass filter value
highcut = 40
# Convert heart rate to seconds, check if low-pass filter within appropriate range
if heart_rate is not None:
heart_rate = heart_rate / 60
if not (highcut >= 10 * heart_rate and highcut < 0.5 * sampling_rate):
raise ValueError(
"Highcut value should be at least 10 times heart rate and"
" less than 0.5 times sampling rate."
)
filtered = signal_filter(
ppg_signal,
sampling_rate=sampling_rate,
lowcut=None,
highcut=highcut,
order=2,
method="butterworth",
)
return filtered
def _ppg_clean_langevin2021(ppg_signal, sampling_rate):
"""Band-pass filter (0.7–3.5 Hz, Butterworth order 2) from Langevin et al. (2021).
Note: these parameters reflect the pipeline used in a specific EmotiBit device-validation study.
Use primarily for reproducibility of those results.
"""
filtered = signal_filter(
ppg_signal,
sampling_rate=sampling_rate,
lowcut=0.7,
highcut=3.5,
method="butterworth",
order=2,
)
return filtered
def _ppg_clean_goda(ppg_signal, sampling_rate):
"""Band-pass filter for continuous PPG signal preprocessing, adapted from Goda et al. (2024)."""
filtered = signal_filter(
ppg_signal,
sampling_rate=sampling_rate,
lowcut=0.5,
highcut=12,
order=4,
method="chebyshevII",
)
return filtered