def correct_peaks(peaks, extra_correction=True, missed_correction=True, short_correction=True, long_correction=True, ectopic_correction=True): """Correct long, short, extra, missed and ectopic beats in peaks vector. Parameters ---------- peaks : 1d array-like Boolean vector of peaks. Returns ------- correction : dictionnary The corrected RR time series and the number of artefacts corrected: * clean_peaks: 1d array-like The corrected boolean time-serie. * ectopic: int The number of ectopic beats corrected. * short: int The number of short beats corrected. * long: int The number of long beats corrcted. * extra: int The number of extra beats corrected. * missed: int The number of missed beats corrected. """ if isinstance(peaks, list): peaks = np.asarray(peaks, dtype=bool) clean_peaks = peaks.copy() nEctopic, nShort, nLong, nExtra, nMissed = 0, 0, 0, 0, 0 artefacts = rr_artefacts(np.diff(np.where(clean_peaks)[0])) # Correct missed beats if missed_correction: if np.any(artefacts['missed']): for this_id in np.where(artefacts['missed'])[0]: this_id += nMissed clean_peaks = correct_missed_peaks(clean_peaks, this_id) nMissed += 1 artefacts = rr_artefacts(np.diff(np.where(clean_peaks)[0])) # Correct extra beats if extra_correction: if np.any(artefacts['extra']): for this_id in np.where(artefacts['extra'])[0]: this_id -= nExtra clean_peaks = correct_extra_peaks(clean_peaks, this_id) nExtra += 1 artefacts = rr_artefacts(np.diff(np.where(clean_peaks)[0])) return {'clean_peaks': clean_peaks, 'ectopic': nEctopic, 'short': nShort, 'long': nLong, 'extra': nExtra, 'missed': nMissed}
plot1 = plot_timedomain(rr) plotly.io.show(plot1) #%% # Frequency domain # ---------------- plot2 = plot_frequency(rr) plotly.io.show(plot2) #%% # Nonlinear domain # ---------------- plot3 = plot_nonlinear(rr) plotly.io.show(plot3) #%% # Artefact detection # ------------------ artefacts = rr_artefacts(rr) #%% # Subspaces visualization # ----------------------- # You can visualize the two main subspaces and spot outliers. The left pamel # plot subspaces that are more sensitive to ectopic beats detection. The right # panel plot subspaces that will be more sensitive to long or short beats, # comprizing the extra and missed beats. plot_subspaces(rr)
def test_rr_artefacts(self): rr = simulate_rr() # Import PPG recording artefacts = rr_artefacts(rr) artefacts = rr_artefacts(list(rr)) assert all(350 == x for x in [len(artefacts[k]) for k in artefacts.keys()])
def plot_subspaces(x, c1=.17, c2=.13, xlim=10, ylim=5, ax=None): """Plot hrv subspace as described by Lipponen & Tarvainen (2019). Parameters ---------- x : 1d array-like Array of RR intervals or subspace1. If subspace1 is provided, subspace2 and 3 must also be provided. c1 : float Fixed variable controling the slope of the threshold lines. Default is 0.13. c2 : float Fixed variable controling the intersect of the threshold lines. Default is 0.17. xlim : int Absolute range of the x axis. Default is 10. ylim : int Absolute range of the y axis. Default is 5. ax : `Matplotlib.Axes` or None Where to draw the plot. Default is *None* (create a new figure). Returns ------- ax : `Matplotlib.Axes` The figure. References ---------- [1] Lipponen, J. A., & Tarvainen, M. P. (2019). A robust algorithm for heart rate variability time series artefact correction using novel beat classification. Journal of Medical Engineering & Technology, 43(3), 173–181. https://doi.org/10.1080/03091902.2019.1640306 """ if not isinstance(x, (np.ndarray, np.generic)): x = np.asarray(x) artefacts = rr_artefacts(x) # Rescale to show outlier in scatterplot if xlim is not None: artefacts['subspace1'][artefacts['subspace1'] < -xlim] = -xlim artefacts['subspace1'][artefacts['subspace1'] > xlim] = xlim if ylim is not None: artefacts['subspace2'][artefacts['subspace2'] < -ylim] = -ylim artefacts['subspace2'][artefacts['subspace2'] > ylim] = ylim artefacts['subspace3'][artefacts['subspace3'] < -ylim*2] = -ylim*2 artefacts['subspace3'][artefacts['subspace3'] > ylim*2] = ylim*2 # Filter for normal beats normalBeats = ((~artefacts['ectopic']) & (~artefacts['short']) & (~artefacts['long']) & (~artefacts['missed']) & (~artefacts['extra'])) ############# # First panel ############# if ax is None: fig, ax = plt.subplots(1, 2, figsize=(10, 5)) # Plot normal beats ax[0].scatter(artefacts['subspace1'][normalBeats], artefacts['subspace2'][normalBeats], color='gray', edgecolors='k', s=15, alpha=0.2, zorder=10, label='Normal') # Plot outliers ax[0].scatter(artefacts['subspace1'][artefacts['ectopic']], artefacts['subspace2'][artefacts['ectopic']], color='r', edgecolors='k', zorder=10, label='Ectopic') ax[0].scatter(artefacts['subspace1'][artefacts['short']], artefacts['subspace2'][artefacts['short']], color='b', edgecolors='k', zorder=10, marker='s', label='Short') ax[0].scatter(artefacts['subspace1'][artefacts['long']], artefacts['subspace2'][artefacts['long']], color='g', edgecolors='k', zorder=10, marker='s', label='Long') ax[0].scatter(artefacts['subspace1'][artefacts['missed']], artefacts['subspace2'][artefacts['missed']], color='g', edgecolors='k', zorder=10, label='Missed') ax[0].scatter(artefacts['subspace1'][artefacts['extra']], artefacts['subspace2'][artefacts['extra']], color='b', edgecolors='k', zorder=10, label='Extra') # Upper area def f1(x): return -c1*x + c2 ax[0].plot([-1, -10], [f1(-1), f1(-10)], 'k', linewidth=1, linestyle='--') ax[0].plot([-1, -1], [f1(-1), 10], 'k', linewidth=1, linestyle='--') x = [-10, -10, -1, -1] y = [f1(-10), 10, 10, f1(-1)] ax[0].fill(x, y, color='gray', alpha=0.3) # Lower area def f2(x): return -c1*x - c2 ax[0].plot([1, 10], [f2(1), f2(10)], 'k', linewidth=1, linestyle='--') ax[0].plot([1, 1], [f2(1), -10], 'k', linewidth=1, linestyle='--') x = [1, 1, 10, 10] y = [f2(1), -10, -10, f2(10)] ax[0].fill(x, y, color='gray', alpha=0.3) ax[0].set_xlabel('Subspace $S_{11}$') ax[0].set_ylabel('Subspace $S_{12}$') ax[0].set_ylim(-ylim, ylim) ax[0].set_xlim(-xlim, xlim) ax[0].set_title('Subspace 1 \n (ectopic beats detection)') ax[0].legend() ############## # Second panel ############## # Plot normal beats ax[1].scatter(artefacts['subspace1'][normalBeats], artefacts['subspace3'][normalBeats], color='gray', edgecolors='k', alpha=0.2, zorder=10, s=15, label='Normal') # Plot outliers ax[1].scatter(artefacts['subspace1'][artefacts['ectopic']], artefacts['subspace3'][artefacts['ectopic']], color='r', edgecolors='k', zorder=10, label='Ectopic') ax[1].scatter(artefacts['subspace1'][artefacts['short']], artefacts['subspace3'][artefacts['short']], color='b', edgecolors='k', zorder=10, marker='s', label='Short') ax[1].scatter(artefacts['subspace1'][artefacts['long']], artefacts['subspace3'][artefacts['long']], color='g', edgecolors='k', zorder=10, marker='s', label='Long') ax[1].scatter(artefacts['subspace1'][artefacts['missed']], artefacts['subspace3'][artefacts['missed']], color='g', edgecolors='k', zorder=10, label='Missed') ax[1].scatter(artefacts['subspace1'][artefacts['extra']], artefacts['subspace3'][artefacts['extra']], color='b', edgecolors='k', zorder=10, label='Extra') # Upper area ax[1].plot([-1, -10], [1, 1], 'k', linewidth=1, linestyle='--') ax[1].plot([-1, -1], [1, 10], 'k', linewidth=1, linestyle='--') x = [-10, -10, -1, -1] y = [1, 10, 10, 1] ax[1].fill(x, y, color='gray', alpha=0.3) # Lower area ax[1].plot([1, 10], [-1, -1], 'k', linewidth=1, linestyle='--') ax[1].plot([1, 1], [-1, -10], 'k', linewidth=1, linestyle='--') x = [1, 1, 10, 10] y = [-1, -10, -10, -1] ax[1].fill(x, y, color='gray', alpha=0.3) ax[1].set_xlabel('Subspace $S_{21}$') ax[1].set_ylabel('Subspace $S_{22}$') ax[1].set_ylim(-ylim*2, ylim*2) ax[1].set_xlim(-xlim, xlim) ax[1].set_title('Subspace 2 \n (long and short beats detection)') ax[1].legend() plt.tight_layout() return ax
# * The category in which the artefact belongs will have an influence on the # correction procedure (see Artefact correction). #%% # Simulate RR time series # ----------------------- # This function will simulate RR time series containing ectopic, extra, missed, # long and short artefacts. rr = simulate_rr() #%% # Artefact detection # ------------------ outliers = rr_artefacts(rr) #%% # Subspaces visualization # ----------------------- # You can visualize the two main subspaces and spot outliers. The left pamel # plot subspaces that are more sensitive to ectopic beats detection. The right # panel plot subspaces that will be more sensitive to long or short beats, # comprizing the extra and missed beats. plot_subspaces(rr) #%% # References # ---------- # .. [#] Lipponen, J. A., & Tarvainen, M. P. (2019). A robust algorithm for
def correct_rr(rr, extra_correction=True, missed_correction=True, short_correction=True, long_correction=True, ectopic_correction=True): """Correct long and short beats using interpolation. Parameters ---------- rr : 1d array-like RR intervals (ms). correct_extra : boolean If True, correct extra beats in the RR time series. correct_missed : boolean If True, correct missed beats in the RR time series. correct_short : boolean If True, correct short beats in the RR time series. correct_long : boolean If True, correct long beats in the RR time series. correct_ectopic : boolean If True, correct ectopic beats in the RR time series. Returns ------- correction : dictionnary The corrected RR time series and the number of artefacts corrected: * clean_rr: 1d array-like The corrected RR time-serie. * ectopic: int The number of ectopic beats corrected. * short: int The number of short beats corrected. * long: int The number of long beats corrcted. * extra: int The number of extra beats corrected. * missed: int The number of missed beats corrected. """ if isinstance(rr, list): rr = np.asarray(rr) clean_rr = rr.copy() nEctopic, nShort, nLong, nExtra, nMissed = 0, 0, 0, 0, 0 artefacts = rr_artefacts(clean_rr) # Correct missed beats if missed_correction: if np.any(artefacts['missed']): for this_id in np.where(artefacts['missed'])[0]: this_id += nMissed clean_rr = correct_missed(clean_rr, this_id) nMissed += 1 artefacts = rr_artefacts(clean_rr) # Correct extra beats if extra_correction: if np.any(artefacts['extra']): for this_id in np.where(artefacts['extra'])[0]: this_id -= nExtra clean_rr = correct_missed(clean_rr, this_id) nExtra += 1 artefacts = rr_artefacts(clean_rr) # Correct ectopic beats if ectopic_correction: if np.any(artefacts['ectopic']): # Also correct the beat before for i in np.where(artefacts['ectopic'])[0]: if (i > 0) & (i < len(artefacts['ectopic'])): artefacts['ectopic'][i-1] = True this_id = np.where(artefacts['ectopic'])[0] clean_rr = interpolate_bads(clean_rr, [this_id]) nEctopic = np.sum(artefacts['ectopic']) # Correct short beats if short_correction: if np.any(artefacts['short']): this_id = np.where(artefacts['short'])[0] clean_rr = interpolate_bads(clean_rr, this_id) nShort = len(this_id) # Correct long beats if long_correction: if np.any(artefacts['long']): this_id = np.where(artefacts['long'])[0] clean_rr = interpolate_bads(clean_rr, this_id) nLong = len(this_id) return {'clean_rr': clean_rr, 'ectopic': nEctopic, 'short': nShort, 'long': nLong, 'extra': nExtra, 'missed': nMissed}
def test_rr_artefacts(self): rr = simulate_rr() # Simulate RR time series artefacts = rr_artefacts(rr) artefacts = rr_artefacts(list(rr)) assert all(350 == x for x in [len(artefacts[k]) for k in artefacts.keys()])