Heartbeat Counting task - Summary results#

Author: Nicolas Legrand nicolas.legrand@cas.au.dk

Hide code cell content
%%capture
import sys

if 'google.colab' in sys.modules:
    !pip install systole, metadpy
from pathlib import Path
import matplotlib.pyplot as plt
from matplotlib.dates import date2num
import numpy as np
import pandas as pd
import seaborn as sns
from systole.detection import ppg_peaks
from systole.plots import plot_raw, plot_subspaces

sns.set_context('paper')
%matplotlib inline

Import data

# Define the result and report folders - This should be adapted to you own settings
resultPath = Path(Path.cwd(), "data", "HBC")
reportPath = Path(Path.cwd(), "reports")
# ensure that the paths are pathlib instance in case they are passed through cardioception.reports.report
resultPath = Path(resultPath)
reportPath = Path(reportPath)
# Search files ending with "final.txt" - This is the main data frame that is saved at the end of the task
results_df = [file for file in Path(resultPath).glob('*final.txt')]
# Load dataframe
df = pd.read_csv(results_df[0])
df
nTrial Reported Condition Duration Confidence ConfidenceRT
0 0 36 Count 40 4 5.146
1 1 27 Count 30 5 9.909
2 2 29 Count 35 4 4.279
3 3 39 Count 45 5 3.278
4 4 47 Count 50 5 4.007
5 5 23 Count 25 5 2.635
# Load raw PPG signal - PPG is saved as .npy files, one for each trial
ppg = {}
for i in range(6):
    ppg[str(i)] = np.load(
        [file for file in resultPath.glob(f'*_{i}.npy')][0]
        )

Heartbeats and artefacts detection#

Note

This section reports the raw PPG signal together with the peaks detected. The instantaneous heart rate frequency (R-R intervals) is derived and represented below each PPG time series. Artefacts in the RR time series are detected using the method described in [Lipponen and Tarvainen, 2019]. The shaded areas represent the pre-recording and post-recording period. Heartbeats detected inside these intervals are automatically removed.

Loop across trials#

counts = []
for nTrial in range(6):

    print(f'Analyzing trial number {nTrial+1}')

    signal, peaks = ppg_peaks(ppg[str(nTrial)][0], clean_extra=True, sfreq=75)
    axs = plot_raw(
        signal=signal, sfreq=1000, figsize=(18, 5), clean_extra=True,
        show_heart_rate=True
        );

    # Show the windows of interest
    # We need to convert sample vector into Matplotlib internal representation
    # so we can index it easily
    x_vec = date2num(
        pd.to_datetime(
            np.arange(0, len(signal)), unit="ms", origin="unix"
            )
        )
    l = len(signal)/1000
    for i in range(2):
        # Pre-trial time
        axs[i].axvspan(
            x_vec[0], x_vec[- (3+df.Duration.iloc[nTrial]) * 1000]
            , alpha=.2
            )
        # Post trial time
        axs[i].axvspan(
            x_vec[- 3 * 1000], 
            x_vec[- 1], 
            alpha=.2
            )
    plt.show()

    # Detected heartbeat in the time window of interest
    peaks = peaks[int(l - (3+df.Duration.iloc[nTrial]))*1000:int((l-3)*1000)]

    rr = np.diff(np.where(peaks)[0])

    _, axs = plt.subplots(ncols=2, figsize=(12, 6))
    plot_subspaces(rr=rr, ax=axs);
    plt.show()

    trial_counts = np.sum(peaks)
    print(f'Reported: {df.Reported.loc[nTrial]} beats ; Detected : {trial_counts} beats')
    counts.append(trial_counts)
Analyzing trial number 1
../../_images/c7546afca0b287a57fe458549e841379bbae57bfac466fe89624fecb1b477176.png ../../_images/408c5e495dcb6283c93074f21bf903c1be50a1b76dae108ced9feafbe9c7bc4a.png
Reported: 36 beats ; Detected : 40 beats
Analyzing trial number 2
../../_images/c75282e52ff388fea547340fb26d61a8b6ec0c4ca287a120b0b459155dcb6ef8.png ../../_images/756b5b5e07f8908164a4a28161714d7dd9ba9c1b4e498ae6e5ba32e053f475a5.png
Reported: 27 beats ; Detected : 30 beats
Analyzing trial number 3
../../_images/bae0fb7b08d3d47704e07bb5034e96b8c8e18d26482d82cab80ebc7590cff109.png ../../_images/d9390860521d300ed1609f38d7afb5b561df709a13d66eb5de89bc3b4dc1af25.png
Reported: 29 beats ; Detected : 36 beats
Analyzing trial number 4
../../_images/b37383b0d985fb24ccc093632baf81263dbfb5bbfaafee5cb1835c04d68e0e9f.png ../../_images/258d8a0b8458809e43b19b35bd54d213bcb7951449ad0e5f97bf62c6237f75fc.png
Reported: 39 beats ; Detected : 46 beats
Analyzing trial number 5
../../_images/629886b618cf2ce89937d84f60c584abf74f4b54da608aa51ba0db357a5439b5.png ../../_images/f4a3c670fab70c811041804542faacd73907e639c77d8bdfe1ca46a4d90a87e5.png
Reported: 47 beats ; Detected : 51 beats
Analyzing trial number 6
../../_images/896459c5ee4ee2d88b2dbd8655e28c25b87d80855a4299ed38c53e039fef8706.png ../../_images/13741e7ae1c1750da7095bd04c7d3f710d6d000ee7149b56b68a26841e74f614.png
Reported: 23 beats ; Detected : 25 beats

Save reults#

# Add heartbeat counts and compute accuracy score
df['Counts'] = counts
df['Score'] = 1 - ((df.Counts - df.Reported).abs() / ((df.Counts + df.Reported)/2))
df
nTrial Reported Condition Duration Confidence ConfidenceRT Counts Score
0 0 36 Count 40 4 5.146 40 0.894737
1 1 27 Count 30 5 9.909 30 0.894737
2 2 29 Count 35 4 4.279 36 0.784615
3 3 39 Count 45 5 3.278 46 0.835294
4 4 47 Count 50 5 4.007 51 0.918367
5 5 23 Count 25 5 2.635 25 0.916667
# Uncomment this to save the final result
#df.to_csv(Path(resultPath, 'processed.txt'))