import matplotlib.pyplot as plt
import numpy as np
from ..ecg.ecg_peaks import _ecg_peaks_plot_artefacts
from ..signal import signal_fixpeaks, signal_formatpeaks
from ..stats import rescale
from .ppg_findpeaks import ppg_findpeaks
[docs]
def ppg_peaks(
ppg_cleaned,
sampling_rate=1000,
method="elgendi",
correct_artifacts=False,
show=False,
**kwargs
):
"""**Find systolic peaks in a photoplethysmogram (PPG) signal**
Find the peaks in an PPG signal using the specified method. You can pass an unfiltered PPG
signals as input, but typically a filtered PPG (cleaned using ``ppg_clean()``) will result in
better results.
.. note::
Please help us improve the methods' documentation and features.
Parameters
----------
ppg_cleaned : Union[list, np.array, pd.Series]
The cleaned PPG channel as returned by ``ppg_clean()``.
sampling_rate : int
The sampling frequency of ``ppg_cleaned`` (in Hz, i.e., samples/second). Defaults to 1000.
method : str
The processing pipeline to apply. Can be one of ``"elgendi"``, ``"bishop"``. The default is
``"elgendi"``.
correct_artifacts : bool
Whether or not to identify and fix artifacts, using the method by
Lipponen & Tarvainen (2019).
show : bool
If ``True``, will show a plot of the signal with peaks. Defaults to ``False``.
**kwargs
Additional keyword arguments, usually specific for each method.
Returns
-------
signals : DataFrame
A DataFrame of same length as the input signal in which occurrences of R-peaks marked as
``1`` in a list of zeros with the same length as ``ppg_cleaned``. Accessible with the keys
``"PPG_Peaks"``.
info : dict
A dictionary containing additional information, in this case the samples at which R-peaks
occur, accessible with the key ``"PPG_Peaks"``, as well as the signals' sampling rate,
accessible with the key ``"sampling_rate"``.
See Also
--------
ppg_clean, ppg_fixpeaks, .signal_fixpeaks
Examples
--------
.. ipython:: python
import neurokit2 as nk
import numpy as np
ppg = nk.ppg_simulate(heart_rate=75, duration=20, sampling_rate=50)
ppg[400:600] = ppg[400:600] + np.random.normal(0, 1.25, 200)
# Default method (Elgendi et al., 2013)
@savefig p_ppg_peaks1.png scale=100%
peaks, info = nk.ppg_peaks(ppg, sampling_rate=100, method="elgendi", show=True)
@suppress
plt.close()
info["PPG_Peaks"]
# Method by Bishop et al., (2018)
@savefig p_ppg_peaks2.png scale=100%
peaks, info = nk.ppg_peaks(ppg, sampling_rate=100, method="bishop", show=True)
@suppress
plt.close()
# Correct artifacts
@savefig p_ppg_peaks3.png scale=100%
peaks, info = nk.ppg_peaks(ppg, sampling_rate=100, correct_artifacts=True, show=True)
@suppress
plt.close()
References
----------
* Elgendi, M., Norton, I., Brearley, M., Abbott, D., & Schuurmans, D. (2013). Systolic peak
detection in acceleration photoplethysmograms measured from emergency responders in tropical
conditions. PloS one, 8(10), e76585.
* Bishop, S. M., & Ercole, A. (2018). Multi-scale peak and trough detection optimised for
periodic and quasi-periodic neuroscience data. In Intracranial Pressure & Neuromonitoring XVI
(pp. 189-195). Springer International Publishing.
"""
# Store info
info = {"method_peaks": method.lower(), "method_fixpeaks": "None"}
info.update(
ppg_findpeaks(
ppg_cleaned,
sampling_rate=sampling_rate,
method=method,
show=False,
**kwargs
)
)
# Peak correction
if correct_artifacts:
info["PPG_Peaks_Uncorrected"] = info["PPG_Peaks"].copy()
fixpeaks, info["PPG_Peaks"] = signal_fixpeaks(
info["PPG_Peaks"], sampling_rate=sampling_rate, method="Kubios"
)
# Add prefix and merge
fixpeaks = {"PPG_fixpeaks_" + str(key): val for key, val in fixpeaks.items()}
info.update(fixpeaks)
# Format output
signals = signal_formatpeaks(
dict(PPG_Peaks=info["PPG_Peaks"]),
desired_length=len(ppg_cleaned),
peak_indices=info["PPG_Peaks"],
)
info["sampling_rate"] = sampling_rate # Add sampling rate in dict info
if show is True:
_ppg_peaks_plot(ppg_cleaned, info, sampling_rate)
return signals, info
# =============================================================================
# Internals
# =============================================================================
def _ppg_peaks_plot(
ppg_cleaned,
info=None,
sampling_rate=1000,
raw=None,
quality=None,
ax=None,
):
x_axis = np.linspace(0, len(ppg_cleaned) / sampling_rate, len(ppg_cleaned))
# Prepare plot
if ax is None:
_, ax = plt.subplots()
ax.set_xlabel("Time (seconds)")
ax.set_title("PPG signal and peaks")
# Quality Area -------------------------------------------------------------
if quality is not None:
quality = rescale(
quality,
to=[
np.min([np.min(raw), np.min(ppg_cleaned)]),
np.max([np.max(raw), np.max(ppg_cleaned)]),
],
)
minimum_line = np.full(len(x_axis), quality.min())
# Plot quality area first
ax.fill_between(
x_axis,
minimum_line,
quality,
alpha=0.12,
zorder=0,
interpolate=True,
facecolor="#4CAF50",
label="Signal quality",
)
# Raw Signal ---------------------------------------------------------------
if raw is not None:
ax.plot(x_axis, raw, color="#B0BEC5", label="Raw signal", zorder=1)
label_clean = "Cleaned signal"
else:
label_clean = "Signal"
# Peaks -------------------------------------------------------------------
ax.scatter(
x_axis[info["PPG_Peaks"]],
ppg_cleaned[info["PPG_Peaks"]],
color="#FFC107",
label="Systolic peaks",
zorder=2,
)
# Artifacts ---------------------------------------------------------------
_ecg_peaks_plot_artefacts(
x_axis,
ppg_cleaned,
info,
info["PPG_Peaks"],
ax,
)
# Clean Signal ------------------------------------------------------------
ax.plot(
x_axis,
ppg_cleaned,
color="#E91E63",
label=label_clean,
zorder=3,
linewidth=1,
)
# Optimize legend
ax.legend(loc="upper right")
return ax