Source code for neurokit2.eda.eda_findpeaks

# -*- coding: utf-8 -*-

import numpy as np
import pandas as pd

from ..signal import signal_filter, signal_findpeaks, signal_smooth, signal_zerocrossings


[docs] def eda_findpeaks(eda_phasic, sampling_rate=1000, method="neurokit", amplitude_min=0.1): """**Find Skin Conductance Responses (SCR) in Electrodermal Activity (EDA)** Low-level function used by `eda_peaks()` to identify Skin Conductance Responses (SCR) peaks in the phasic component of Electrodermal Activity (EDA) with different possible methods. See :func:`eda_peaks` for details. Parameters ---------- eda_phasic : Union[list, np.array, pd.Series] The phasic component of the EDA signal (from :func:`eda_phasic`). sampling_rate : int The sampling frequency of the EDA signal (in Hz, i.e., samples/second). method : str The processing pipeline to apply. Can be one of ``"neurokit"`` (default), ``"gamboa2008"``, ``"kim2004"`` (the default in BioSPPy), ``"vanhalem2020"`` or ``"nabian2018"``. amplitude_min : float Only used if "method" is ``"neurokit"`` or ``"kim2004"``. Minimum threshold by which to exclude SCRs (peaks) as relative to the largest amplitude in the signal. Returns ------- info : dict A dictionary containing additional information, in this case the aplitude of the SCR, the samples at which the SCR onset and the SCR peaks occur. Accessible with the keys ``"SCR_Amplitude"``, ``"SCR_Onsets"``, and ``"SCR_Peaks"`` respectively. See Also -------- eda_simulate, eda_clean, eda_phasic, eda_fixpeaks, eda_peaks, eda_process, eda_plot Examples --------- .. ipython:: python import neurokit2 as nk # Get phasic component eda_signal = nk.eda_simulate(duration=30, scr_number=5, drift=0.1, noise=0) eda_cleaned = nk.eda_clean(eda_signal) eda = nk.eda_phasic(eda_cleaned) eda_phasic = eda["EDA_Phasic"].values # Find peaks gamboa2008 = nk.eda_findpeaks(eda_phasic, method="gamboa2008") kim2004 = nk.eda_findpeaks(eda_phasic, method="kim2004") neurokit = nk.eda_findpeaks(eda_phasic, method="neurokit") vanhalem2020 = nk.eda_findpeaks(eda_phasic, method="vanhalem2020") nabian2018 = nk.eda_findpeaks(eda_phasic, method="nabian2018") @savefig p_eda_findpeaks.png scale=100% nk.events_plot([gamboa2008["SCR_Peaks"], kim2004["SCR_Peaks"], vanhalem2020["SCR_Peaks"], neurokit["SCR_Peaks"], nabian2018["SCR_Peaks"]], eda_phasic) @suppress plt.close() References ---------- * Gamboa, H. (2008). Multi-modal behavioral biometrics based on hci and electrophysiology. PhD Thesis Universidade. * Kim, K. H., Bang, S. W., & Kim, S. R. (2004). Emotion recognition system using short-term monitoring of physiological signals. Medical and biological engineering and computing, 42(3), 419-427. * van Halem, S., Van Roekel, E., Kroencke, L., Kuper, N., & Denissen, J. (2020). Moments That Matter? On the Complexity of Using Triggers Based on Skin Conductance to Sample Arousing Events Within an Experience Sampling Framework. European Journal of Personality. * 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, 2800711. """ # Try to retrieve the right column if a dataframe is passed if isinstance(eda_phasic, pd.DataFrame): try: eda_phasic = eda_phasic["EDA_Phasic"] except KeyError: raise KeyError( "NeuroKit error: eda_findpeaks(): Please provide an array as the input signal." ) method = method.lower() # remove capitalised letters if method in ["gamboa2008", "gamboa"]: info = _eda_findpeaks_gamboa2008(eda_phasic) elif method in ["kim", "kbk", "kim2004", "biosppy"]: info = _eda_findpeaks_kim2004( eda_phasic, sampling_rate=sampling_rate, amplitude_min=amplitude_min ) elif method in ["nk", "nk2", "neurokit", "neurokit2"]: info = _eda_findpeaks_neurokit(eda_phasic, amplitude_min=amplitude_min) elif method in ["vanhalem2020", "vanhalem", "halem2020"]: info = _eda_findpeaks_vanhalem2020(eda_phasic, sampling_rate=sampling_rate) elif method in ["nabian2018", "nabian"]: info = _eda_findpeaks_nabian2018(eda_phasic) else: raise ValueError( "NeuroKit error: eda_findpeaks(): 'method' should be one of 'neurokit', 'gamboa2008', 'kim2004'" " 'vanhalem2020' or 'nabian2018'." ) return info
# ============================================================================= # Methods # ============================================================================= def _eda_findpeaks_neurokit(eda_phasic, amplitude_min=0.1): peaks = signal_findpeaks(eda_phasic, relative_height_min=amplitude_min, relative_max=True) info = { "SCR_Onsets": peaks["Onsets"], "SCR_Peaks": peaks["Peaks"], "SCR_Height": eda_phasic[peaks["Peaks"]], } return info def _eda_findpeaks_vanhalem2020(eda_phasic, sampling_rate=1000): """Follows approach of van Halem et al. (2020). A peak is considered when there is a consistent increase of 0.5 seconds following a consistent decrease of 0.5 seconds. * van Halem, S., Van Roekel, E., Kroencke, L., Kuper, N., & Denissen, J. (2020). Moments That Matter? On the Complexity of Using Triggers Based on Skin Conductance to Sample Arousing Events Within an Experience Sampling Framework. European Journal of Personality. """ # smooth eda_phasic = signal_filter( eda_phasic, sampling_rate=sampling_rate, lowcut=None, highcut=None, method="savgol", window_size=501, ) info = signal_findpeaks(eda_phasic) peaks = info["Peaks"] threshold = 0.5 * sampling_rate # Define each peak as a consistent increase of 0.5s increase = info["Peaks"] - info["Onsets"] peaks = peaks[increase > threshold] idx = np.where(peaks[:, None] == info["Peaks"][None, :])[1] # Check if each peak is followed by consistent decrease of 0.5s decrease = info["Offsets"][idx] - peaks if any(np.isnan(decrease)): decrease[np.isnan(decrease)] = False if any(decrease < threshold): keep = np.where(decrease > threshold)[0] idx = idx[keep] # Update index info = { "SCR_Onsets": info["Onsets"][idx], "SCR_Peaks": info["Peaks"][idx], "SCR_Height": eda_phasic[info["Peaks"][idx]], } return info def _eda_findpeaks_gamboa2008(eda_phasic): """Basic method to extract Skin Conductivity Responses (SCR) from an EDA signal following the approach in the thesis by Gamboa (2008). * Gamboa, H. (2008). Multi-modal behavioral biometrics based on hci and electrophysiology. PhD Thesis Universidade. """ derivative = np.diff(np.sign(np.diff(eda_phasic))) # find extrema pi = np.nonzero(derivative < 0)[0] + 1 ni = np.nonzero(derivative > 0)[0] + 1 # sanity check if len(pi) == 0 or len(ni) == 0: raise ValueError( "NeuroKit error: eda_findpeaks(): Could not find enough SCR peaks. Try another method." ) # pair vectors if ni[0] < pi[0]: ni = ni[1:] if pi[-1] > ni[-1]: pi = pi[:-1] if len(pi) > len(ni): pi = pi[:-1] li = min(len(pi), len(ni)) peaks = pi[:li] onsets = ni[:li] # indices i0 = peaks - (onsets - peaks) / 2.0 if i0[0] < 0: i0[0] = 0 # amplitude amplitudes = np.array([np.max(eda_phasic[peaks[i] : onsets[i]]) for i in range(li)]) # output info = {"SCR_Onsets": onsets, "SCR_Peaks": peaks, "SCR_Height": amplitudes} return info def _eda_findpeaks_kim2004(eda_phasic, sampling_rate=1000, amplitude_min=0.1): """KBK method to extract Skin Conductivity Responses (SCR) from an EDA signal following the approach by Kim et al.(2004). * Kim, K. H., Bang, S. W., & Kim, S. R. (2004). Emotion recognition system using short-term monitoring of physiological signals. Medical and biological engineering and computing, 42(3), 419-427. """ # differentiation df = np.diff(eda_phasic) # smooth df = signal_smooth(signal=df, kernel="bartlett", size=int(sampling_rate)) # zero crosses zeros = signal_zerocrossings(df) if np.all(df[: zeros[0]] > 0): zeros = zeros[1:] if np.all(df[zeros[-1] :] > 0): zeros = zeros[:-1] scrs, amps, ZC, pks = [], [], [], [] for i in range(0, len(zeros) - 1, 2): scrs += [eda_phasic[zeros[i] : zeros[i + 1]]] aux = scrs[-1].max() if aux > 0: amps += [aux] ZC += [zeros[i]] ZC += [zeros[i + 1]] pks += [zeros[i] + np.argmax(eda_phasic[zeros[i] : zeros[i + 1]])] amps = np.array(amps) ZC = np.array(ZC) pks = np.array(pks) onsets = ZC[::2] # exclude SCRs with small amplitude masked = amps > (amplitude_min * np.nanmax(amps)) # threshold amps = amps[masked] pks = pks[masked] onsets = onsets[masked] # output info = {"SCR_Onsets": onsets, "SCR_Peaks": pks, "SCR_Height": amps} return info def _eda_findpeaks_nabian2018(eda_phasic): """Basic method to extract Skin Conductivity Responses (SCR) from an EDA signal following the approach by Nabian et al. (2018). The amplitude of the SCR is obtained by finding the maximum value between these two zero-crossings, and calculating the difference between the initial zero crossing and the maximum value. Detected SCRs with amplitudes smaller than 10 percent of the maximum SCR amplitudes that are already detected on the differentiated signal will be eliminated. It is crucial that artifacts are removed before finding peaks. * 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, 2800711. https://doi.org/10.1109/JTEHM.2018.2878000 """ # differentiation eda_phasic_diff = np.diff(eda_phasic) # smooth eda_phasic_smoothed = signal_smooth(eda_phasic_diff, kernel="bartlett", size=20) # zero crossings pos_crossings = signal_zerocrossings(eda_phasic_smoothed, direction="positive") neg_crossings = signal_zerocrossings(eda_phasic_smoothed, direction="negative") # if negative crossing happens before the positive crossing # delete first negative crossing because we want to identify peaks if neg_crossings[0] < pos_crossings[0]: neg_crossings = neg_crossings[1:] # Sanitize consecutive crossings if len(pos_crossings) > len(neg_crossings): pos_crossings = pos_crossings[0 : len(neg_crossings)] elif len(pos_crossings) < len(neg_crossings): neg_crossings = neg_crossings[0 : len(pos_crossings)] peaks_list = [] onsets_list = [] amps_list = [] for i, j in zip(pos_crossings, neg_crossings): window = eda_phasic[i:j] # The amplitude of the SCR is obtained by finding the maximum value # between these two zero-crossings and calculating the difference # between the initial zero crossing and the maximum value. # amplitude defined in neurokit2 amp = np.nanmax(window) # Detected SCRs with amplitudes less than 10% of max SCR amplitude will be eliminated # we append the first SCR if len(amps_list) == 0: # be careful, if two peaks have the same amplitude, np.where will return a list peaks = np.where(eda_phasic == amp)[0] # make sure that the peak is within the window peaks = [peak for peak in [peaks] if peak > i and peak < j] peaks_list.append(peaks[0]) onsets_list.append(i) amps_list.append(amp) else: # we have a list of peaks # amplitude defined in the paper diff = amp - eda_phasic[i] if not diff < (0.1 * max(amps_list)): peaks = np.where(eda_phasic == amp)[0] # make sure that the peak is within the window peaks = [peak for peak in [peaks] if peak > i and peak < j] peaks_list.append(peaks[0]) onsets_list.append(i) amps_list.append(amp) # output info = { "SCR_Onsets": np.array(onsets_list), "SCR_Peaks": np.hstack(np.array(peaks_list)), "SCR_Height": np.array(amps_list), } return info