Source code for neurokit2.ppg.ppg_clean

# -*- 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