# -*- coding: utf-8 -*-
"""
biosppy.plotting
----------------
This module provides utilities to plot data.
:copyright: (c) 2015-2023 by Instituto de Telecomunicacoes
:license: BSD 3-clause, see LICENSE for more details.
"""
# Imports
# compat
from __future__ import absolute_import, division, print_function
from six.moves import range, zip
import six
# built-in
import os
# 3rd party
import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec
import matplotlib.patches as patches
import matplotlib.lines as lines
import numpy as np
from mock import MagicMock
# local
from . import utils
from .signals import tools as st
# Globals
MAJOR_LW = 1.5
MED_LW = 1.25
MINOR_LW = 1.0
MAX_ROWS = 10
LOGOS_FOLDER = '.logos'
BIOSPPY_LOGO = 'biosppy.png'
SCIENTISST_LOGO = 'scientisst.png'
def _get_params():
"""Get matplotlib parameters.
Returns
-------
params : dict
Dictionary of matplotlib parameters.
"""
params = {
'text.color': '#495057',
'axes.labelcolor': '#495057',
'axes.labelsize': 10,
'axes.edgecolor': '#495057',
'axes.facecolor': '#f8f9fa',
'axes.titlesize': 11,
'xtick.color': '#495057',
'ytick.color': '#495057',
'grid.color': '#ebeef0',
'axes.spines.top': False,
'axes.spines.right': False,
'axes.grid': True,
'grid.linestyle': '-',
'grid.color': '#ebeef0',
'legend.facecolor': 'white',
'legend.framealpha': 0.75,
'legend.loc': 'upper right',
'legend.fontsize': 8
}
return params
[docs]def color_palette(idx):
"""Color palette to use throughout the biosppy package
Parameters
----------
idx: str or int
identifier of color to use
Returns
-------
color_id: str
hexadecimal color code chosen
"""
color_dict = {
'blue': '#71a7cc',
'dark-blue': '#184169',
'light-blue': '#a5c8e0',
'green': '#329352',
'dark-green': '#154d28',
'light-green': '#7ac77d',
'red': '#D62839',
'dark-red': '#B14343',
'light-red': '#FF5A5F',
'yellow': '#D19C2F',
'dark-yellow': '#FFC300',
'light-yellow': '#FFDD55',
'violet': '#9D4EDD',
'dark-violet': '#3C096C',
'light-violet': '#E0AAFF',
'orange': '#f3883e',
'dark-orange': '#b2400a',
'light-orange': '#f7d1ab',
'grey': '#ADB5BD',
'dark-grey': '#495057',
'light-grey': '#E6E6E6'
}
if type(idx) == int:
color_id = list(color_dict.values())[idx]
else:
if idx in color_dict.keys():
color_id = color_dict[idx]
else:
raise ValueError(f'Please choose one color from {color_dict.keys()}'
f' or give an index')
return color_id
[docs]def add_logo(fig):
logos_folder = os.path.join(os.path.dirname(__file__), LOGOS_FOLDER)
try:
# add biosppy logo
biosppy_logo = plt.imread(os.path.join(logos_folder, BIOSPPY_LOGO))
logo = fig.add_axes([0.80, 0.02, 0.08, 0.08], anchor='SE')
logo.imshow(biosppy_logo, alpha=0.5)
logo.axis('off')
# add scientisst logo
scientisst_logo = plt.imread(os.path.join(logos_folder, SCIENTISST_LOGO))
logo = fig.add_axes([0.90, 0.02, 0.08, 0.08], anchor='SE')
logo.imshow(scientisst_logo, alpha=0.5)
logo.axis('off')
except:
pass
def _plot_filter(b, a, sampling_rate=1000., nfreqs=4096, log_xscale=True,
ax=None):
"""Compute and plot the frequency response of a digital filter.
Parameters
----------
b : array
Numerator coefficients.
a : array
Denominator coefficients.
sampling_rate : int, float, optional
Sampling frequency (Hz).
nfreqs : int, optional
Number of frequency points to compute.
log_xscale : bool, optional
Whether to use log scale for x-axis.
ax : axis, optional
Plot Axis to use.
Returns
-------
fig : Figure
Figure object.
"""
# compute frequency response
freqs, resp = st._filter_resp(b, a,
sampling_rate=sampling_rate,
nfreqs=nfreqs)
# get matplotlib parameters
plt.rcParams.update(_get_params())
# plot
if ax is None:
fig = plt.figure(figsize=(8, 4))
ax = fig.add_subplot(211)
ax2 = fig.add_subplot(212)
else:
ax2 = ax.twinx()
# set layout
fig.subplots_adjust(top=0.88, bottom=0.17, hspace=0.2, left=0.16,
right=0.94)
# title
fig.suptitle('Filter Frequency Response', fontsize=12, fontweight='bold')
# amplitude
pwr = 20. * np.log10(np.abs(resp))
if log_xscale:
ax.semilogx(freqs, pwr, color=color_palette('blue'),
linewidth=MAJOR_LW)
else:
ax.plot(freqs, pwr, color=color_palette('blue'), linewidth=MAJOR_LW)
ax.set_ylabel('Amplitude (dB)')
ax.grid(True)
ax.tick_params(axis='x', which='both', bottom=False, top=False,
labelbottom=False)
ax.spines['bottom'].set_visible(False)
# phase
angles = np.unwrap(np.angle(resp)) * 180. / np.pi # radians to degrees
if log_xscale:
ax2.semilogx(freqs, angles, color=color_palette('dark-blue'),
linewidth=MAJOR_LW)
else:
ax2.plot(freqs, angles, color=color_palette('dark-blue'),
linewidth=MAJOR_LW)
ax2.set_ylabel('Angle (degrees)')
ax2.set_xlabel('Frequency (Hz)')
ax2.grid(True)
# align y-axis labels
fig.align_ylabels()
# add logo
add_logo(fig)
return fig
[docs]def plot_filter(ftype='FIR',
band='lowpass',
order=None,
frequency=None,
sampling_rate=1000.,
log_xscale=True,
path=None,
show=True, **kwargs):
"""Plot the frequency response of the filter specified with the given
parameters.
Parameters
----------
ftype : str
Filter type:
* Finite Impulse Response filter ('FIR');
* Butterworth filter ('butter');
* Chebyshev filters ('cheby1', 'cheby2');
* Elliptic filter ('ellip');
* Bessel filter ('bessel').
band : str
Band type:
* Low-pass filter ('lowpass');
* High-pass filter ('highpass');
* Band-pass filter ('bandpass');
* Band-stop filter ('bandstop').
order : int
Order of the filter.
frequency : int, float, list, array
Cutoff frequencies; format depends on type of band:
* 'lowpass' or 'bandpass': single frequency;
* 'bandpass' or 'bandstop': pair of frequencies.
sampling_rate : int, float, optional
Sampling frequency (Hz).
log_xscale : bool, optional
Whether to use log scale for x-axis.
path : str, optional
If provided, the plot will be saved to the specified file.
show : bool, optional
If True, show the plot immediately.
``**kwargs`` : dict, optional
Additional keyword arguments are passed to the underlying
scipy.signal function.
"""
# get filter
b, a = st.get_filter(ftype=ftype,
band=band,
order=order,
frequency=frequency,
sampling_rate=sampling_rate, **kwargs)
# plot
fig = _plot_filter(b, a, sampling_rate, log_xscale=log_xscale)
# save to file
if path is not None:
path = utils.normpath(path)
root, ext = os.path.splitext(path)
ext = ext.lower()
if ext not in ['png', 'jpg']:
path = root + '.png'
fig.savefig(path, dpi=200, bbox_inches='tight')
# show
if show:
plt.show()
else:
# close
plt.close(fig)
[docs]def plot_spectrum(signal=None, sampling_rate=1000., path=None, show=True):
"""Plot the power spectrum of a signal (one-sided).
Parameters
----------
signal : array
Input signal.
sampling_rate : int, float, optional
Sampling frequency (Hz).
path : str, optional
If provided, the plot will be saved to the specified file.
show : bool, optional
If True, show the plot immediately.
"""
freqs, power = st.power_spectrum(signal, sampling_rate,
pad=0,
pow2=False,
decibel=True)
# get matplotlib parameters
plt.rcParams.update(_get_params())
fig = plt.figure()
ax = fig.add_subplot(111)
ax.plot(freqs, power, linewidth=MAJOR_LW)
ax.set_xlabel('Frequency (Hz)')
ax.set_ylabel('Power (dB)')
ax.grid()
# make layout tight
fig.tight_layout()
# save to file
if path is not None:
path = utils.normpath(path)
root, ext = os.path.splitext(path)
ext = ext.lower()
if ext not in ['png', 'jpg']:
path = root + '.png'
fig.savefig(path, dpi=200, bbox_inches='tight')
# show
if show:
plt.show()
else:
# close
plt.close(fig)
[docs]def plot_acc(ts=None,
raw=None,
vm=None,
sm=None,
units=None,
path=None,
show=False):
"""Create a summary plot from the output of signals.acc.acc.
Parameters
----------
ts : array
Signal time axis reference (seconds).
raw : array
Raw ACC signal.
vm : array
Vector Magnitude feature of the signal.
sm : array
Signal Magnitude feature of the signal
units : str, optional
Units of the vertical axis. If provided, the plot title will include
the units information. Default is None.
path : str, optional
If provided, the plot will be saved to the specified file.
show : bool, optional
If True, show the plot immediately.
"""
raw_t = np.transpose(raw)
acc_x, acc_y, acc_z = raw_t[0], raw_t[1], raw_t[2]
# get matplotlib parameters
plt.rcParams.update(_get_params())
fig = plt.figure(figsize=(10, 5))
fig.suptitle('ACC Summary', fontsize=12, fontweight='bold')
gs = gridspec.GridSpec(6, 2)
fig.subplots_adjust(top=0.85, hspace=0.7, wspace=0.34, left=0.13,
right=0.96, bottom=0.18)
# raw signal (acc_x)
ax1 = fig.add_subplot(gs[:2, 0])
ax1.set_title("Signal")
ax1.plot(ts, acc_x, linewidth=MINOR_LW, label='X',
color=color_palette('dark-blue'))
ax1.legend(loc='upper right')
ax1.tick_params(axis='x', which='both', bottom=False, top=False,
labelbottom=False)
ax1.spines['bottom'].set_visible(False)
# raw signal (acc_y)
ax2 = fig.add_subplot(gs[2:4, 0], sharex=ax1)
ax2.plot(ts, acc_y, linewidth=MINOR_LW, label='Y',
color=color_palette('blue'))
ax2.set_ylabel('Amplitude' if units is None else 'Amplitude ($%s$)' % units)
ax2.legend(loc='upper right')
ax2.tick_params(axis='x', which='both', bottom=False, top=False,
labelbottom=False)
ax2.spines['bottom'].set_visible(False)
# raw signal (acc_z)
ax3 = fig.add_subplot(gs[4:, 0], sharex=ax1)
ax3.plot(ts, acc_z, linewidth=MINOR_LW, label='Z',
color=color_palette('light-blue'))
ax3.set_xlabel('Time (s)')
ax3.legend(loc='upper right')
# vector magnitude
ax4 = fig.add_subplot(gs[:3, 1], sharex=ax1)
ax4.set_title('Features')
ax4.plot(ts, vm, linewidth=MINOR_LW, label='Vector Magnitude',
color=color_palette('orange'))
ax4.set_ylabel('Amplitude' if units is None else 'Amplitude ($%s$)' % units)
ax4.legend(loc='upper right')
ax4.tick_params(axis='x', which='both', bottom=False, top=False,
labelbottom=False)
ax4.spines['bottom'].set_visible(False)
# signal magnitude
ax5 = fig.add_subplot(gs[3:, 1], sharex=ax1)
ax5.plot(ts, sm, linewidth=MINOR_LW, label='Signal Magnitude',
color=color_palette('light-orange'))
ax5.set_ylabel('Amplitude' if units is None else 'Amplitude (%s)' % units)
ax5.set_xlabel('Time (s)')
ax5.legend(loc='upper right')
# align y-axis labels
fig.align_ylabels([ax1, ax2, ax3])
fig.align_ylabels([ax4, ax5])
# add logo
add_logo(fig)
# save to file
if path is not None:
path = utils.normpath(path)
root, ext = os.path.splitext(path)
ext = ext.lower()
if ext not in ['png', 'jpg']:
path = root + '.png'
fig.savefig(path, dpi=200, bbox_inches='tight')
# show
if show:
plt.show()
else:
# close
plt.close(fig)
[docs]def plot_ppg(ts=None,
raw=None,
filtered=None,
peaks=None,
templates_ts=None,
templates=None,
heart_rate_ts=None,
heart_rate=None,
units=None,
path=None,
show=False):
"""Create a summary plot from the output of signals.ppg.ppg.
Parameters
----------
ts : array
Signal time axis reference (seconds).
raw : array
Raw PPG signal.
filtered : array
Filtered PPG signal.
peaks : array
Indices of PPG pulse peaks.
templates_ts : array
Templates time axis reference (seconds).
templates : array
Extracted PPG templates.
heart_rate_ts : array
Heart rate time axis reference (seconds).
heart_rate : array
Instantaneous heart rate (bpm).
units : str, optional
Units of the vertical axis. If provided, the plot title will include
the units information. Default is None.
path : str, optional
If provided, the plot will be saved to the specified file.
show : bool, optional
If True, show the plot immediately.
"""
# get matplotlib parameters
plt.rcParams.update(_get_params())
fig = plt.figure(figsize=(10, 5))
fig.suptitle('PPG Summary', fontsize=12, fontweight='bold')
gs = gridspec.GridSpec(6, 2)
fig.subplots_adjust(top=0.88, bottom=0.12, hspace=0.9, wspace=0.3,
left=0.1, right=0.96)
# raw signal
ax1 = fig.add_subplot(gs[:2, 0])
ax1.set_title('Signal')
ax1.plot(ts, raw, linewidth=MED_LW, label='Raw',
color=color_palette('light-blue'), alpha=0.6)
ax1.plot(ts, filtered+np.mean(raw), linewidth=MINOR_LW, label='Filtered',
color=color_palette('blue'))
ax1.set_ylabel('Amplitude' if units is None else 'Amplitude (%s)' % units)
ax1.legend()
ax1.tick_params(axis='x', which='both', bottom=False, top=False,
labelbottom=False)
ax1.spines['bottom'].set_visible(False)
# signal with onsets
ax2 = fig.add_subplot(gs[2:4, 0], sharex=ax1)
ax2.set_title('Systolic Peak Detection')
ymin = np.min(filtered)
ymax = np.max(filtered)
alpha = 0.1 * (ymax - ymin)
ymax += alpha
ymin -= alpha
ax2.plot(ts, filtered, linewidth=MINOR_LW, color=color_palette('blue'))
ax2.plot(ts[peaks], filtered[peaks], ls='None', marker='x',
color=color_palette('dark-red'), label='Peaks')
ax2.set_ylabel('Amplitude' if units is None else 'Amplitude (%s)' % units)
ax2.legend()
ax2.tick_params(axis='x', which='both', bottom=False, top=False,
labelbottom=False)
ax2.spines['bottom'].set_visible(False)
# heart rate
ax3 = fig.add_subplot(gs[4:, 0], sharex=ax1)
ax3.set_title('Heart Rate')
ax3.plot(heart_rate_ts, heart_rate, linewidth=MAJOR_LW, label='Heart Rate',
color=color_palette('blue'))
ax3.set_xlabel('Time (s)')
ax3.set_ylabel('Heart Rate (bpm)')
# align y-axis labels
fig.align_ylabels([ax1, ax2, ax3])
# templates
ax4 = fig.add_subplot(gs[1:5, 1])
ax4.plot(templates_ts, templates, linewidth=MINOR_LW, alpha=0.5,
color=color_palette('blue'))
ax4.set_xlabel('Time (s)')
ax4.set_ylabel('Amplitude' if units is None else 'Amplitude (%s)' % units)
ax4.set_title('Templates')
add_logo(fig)
# save to file
if path is not None:
path = utils.normpath(path)
root, ext = os.path.splitext(path)
ext = ext.lower()
if ext not in ['png', 'jpg']:
path = root + '.png'
fig.savefig(path, dpi=200, bbox_inches='tight')
# show
if show:
plt.show()
else:
# close
plt.close(fig)
[docs]def plot_bvp(ts=None,
raw=None,
filtered=None,
onsets=None,
heart_rate_ts=None,
heart_rate=None,
path=None,
show=False):
"""Create a summary plot from the output of signals.bvp.bvp.
Parameters
----------
ts : array
Signal time axis reference (seconds).
raw : array
Raw BVP signal.
filtered : array
Filtered BVP signal.
onsets : array
Indices of BVP pulse onsets.
heart_rate_ts : array
Heart rate time axis reference (seconds).
heart_rate : array
Instantaneous heart rate (bpm).
path : str, optional
If provided, the plot will be saved to the specified file.
show : bool, optional
If True, show the plot immediately.
"""
# get matplotlib parameters
plt.rcParams.update(_get_params())
fig = plt.figure()
fig.suptitle('BVP Summary')
# raw signal
ax1 = fig.add_subplot(311)
ax1.plot(ts, raw, linewidth=MAJOR_LW, label='Raw')
ax1.set_ylabel('Amplitude')
ax1.legend()
ax1.grid()
# filtered signal with onsets
ax2 = fig.add_subplot(312, sharex=ax1)
ymin = np.min(filtered)
ymax = np.max(filtered)
alpha = 0.1 * (ymax - ymin)
ymax += alpha
ymin -= alpha
ax2.plot(ts, filtered, linewidth=MAJOR_LW, label='Filtered')
ax2.vlines(ts[onsets], ymin, ymax,
color='m',
linewidth=MINOR_LW,
label='Onsets')
ax2.set_ylabel('Amplitude')
ax2.legend()
ax2.grid()
# heart rate
ax3 = fig.add_subplot(313, sharex=ax1)
ax3.plot(heart_rate_ts, heart_rate, linewidth=MAJOR_LW, label='Heart Rate')
ax3.set_xlabel('Time (s)')
ax3.set_ylabel('Heart Rate (bpm)')
ax3.legend()
ax3.grid()
# make layout tight
fig.tight_layout()
# save to file
if path is not None:
path = utils.normpath(path)
root, ext = os.path.splitext(path)
ext = ext.lower()
if ext not in ['png', 'jpg']:
path = root + '.png'
fig.savefig(path, dpi=200, bbox_inches='tight')
# show
if show:
plt.show()
else:
# close
plt.close(fig)
[docs]def plot_abp(ts=None,
raw=None,
filtered=None,
onsets=None,
heart_rate_ts=None,
heart_rate=None,
path=None,
show=False):
"""Create a summary plot from the output of signals.abp.abp.
Parameters
----------
ts : array
Signal time axis reference (seconds).
raw : array
Raw ABP signal.
filtered : array
Filtered ABP signal.
onsets : array
Indices of ABP pulse onsets.
heart_rate_ts : array
Heart rate time axis reference (seconds).
heart_rate : array
Instantaneous heart rate (bpm).
path : str, optional
If provided, the plot will be saved to the specified file.
show : bool, optional
If True, show the plot immediately.
"""
# get matplotlib parameters
plt.rcParams.update(_get_params())
fig = plt.figure()
fig.suptitle('ABP Summary')
# raw signal
ax1 = fig.add_subplot(311)
ax1.plot(ts, raw, linewidth=MAJOR_LW, label='Raw')
ax1.set_ylabel('Amplitude')
ax1.legend()
ax1.grid()
# filtered signal with onsets
ax2 = fig.add_subplot(312, sharex=ax1)
ymin = np.min(filtered)
ymax = np.max(filtered)
alpha = 0.1 * (ymax - ymin)
ymax += alpha
ymin -= alpha
ax2.plot(ts, filtered, linewidth=MAJOR_LW, label='Filtered')
ax2.vlines(ts[onsets], ymin, ymax,
color='m',
linewidth=MINOR_LW,
label='Onsets')
ax2.set_ylabel('Amplitude')
ax2.legend()
ax2.grid()
# heart rate
ax3 = fig.add_subplot(313, sharex=ax1)
ax3.plot(heart_rate_ts, heart_rate, linewidth=MAJOR_LW, label='Heart Rate')
ax3.set_xlabel('Time (s)')
ax3.set_ylabel('Heart Rate (bpm)')
ax3.legend()
ax3.grid()
# make layout tight
fig.tight_layout()
# save to file
if path is not None:
path = utils.normpath(path)
root, ext = os.path.splitext(path)
ext = ext.lower()
if ext not in ['png', 'jpg']:
path = root + '.png'
fig.savefig(path, dpi=200, bbox_inches='tight')
# show
if show:
plt.show()
else:
# close
plt.close(fig)
[docs]def plot_eda(ts=None,
raw=None,
filtered=None,
edr=None,
edl=None,
onsets=None,
peaks=None,
amplitudes=None,
units=None,
path=None,
show=False):
"""Create a summary plot from the output of signals.eda.eda.
Parameters
----------
ts : array
Signal time axis reference (seconds).
raw : array
Raw EDA signal.
filtered : array
Filtered EDA signal.
edr : array
Electrodermal response signal.
edl : array
Electrodermal level signal.
onsets : array
Events onsets indices.
peaks : array
Events peaks indices.
amplitudes : array
Amplitudes location indices.
units : str, optional
Units of the vertical axis. If provided, the plot title will include
the units information. Default is None.
path : str, optional
If provided, the plot will be saved to the specified file.
show : bool, optional
If True, show the plot immediately.
"""
# get matplotlib parameters
plt.rcParams.update(_get_params())
fig = plt.figure(figsize=(10, 5))
fig.suptitle('EDA Summary', fontsize=14, fontweight='bold')
gs = gridspec.GridSpec(6, 2)
fig.subplots_adjust(top=0.85, hspace=0.7, wspace=0.34, left=0.13,
right=0.96, bottom=0.18)
# signal
ax1 = fig.add_subplot(gs[:2, 0])
ax1.plot(ts, raw, linewidth=MED_LW, label='Raw',
color=color_palette('light-blue'), alpha=0.6)
ax1.plot(ts, filtered, linewidth=MINOR_LW, label='Filtered',
color=color_palette('blue'))
ax1.set_ylabel('Amplitude' if units is None else 'Amplitude \n (%s)' % units)
ax1.set_title('Signal')
ax1.legend()
ax1.tick_params(axis='x', which='both', bottom=False, top=False,
labelbottom=False)
ax1.spines['bottom'].set_visible(False)
# event detection
ax2 = fig.add_subplot(gs[2:4, 0], sharex=ax1, sharey=ax1)
ymin = np.min(filtered)
ymax = np.max(filtered)
alpha = 0.1 * (ymax - ymin)
ymax += alpha
ymin -= alpha
ax2.plot(ts, filtered, linewidth=MINOR_LW, color=color_palette('blue'))
ax2.scatter(ts[onsets], filtered[onsets], marker='|', lw=1, s=100,
color=color_palette('dark-red'), label='Onsets', zorder=3)
ax2.scatter(ts[peaks], filtered[peaks], marker='x',
color=color_palette('dark-red'), label='Peaks', zorder=3)
ax2.set_ylabel('Amplitude' if units is None else 'Amplitude \n (%s)' % units)
ax2.set_title('Event Detection')
ax2.legend()
ax2.tick_params(axis='x', which='both', bottom=False, top=False,
labelbottom=False)
ax2.spines['bottom'].set_visible(False)
# amplitudes
ax3 = fig.add_subplot(gs[4:, 0], sharex=ax1)
ax3.plot(ts[peaks], amplitudes, linewidth=MAJOR_LW, color=color_palette('blue'), label='Amplitude')
ax3.set_xlabel('Time (s)')
ax3.set_ylabel('Amplitude' if units is None else 'Amplitude \n (%s)' % units)
ax3.set_title('Event Amplitudes')
# align y axis labels
fig.align_ylabels()
# decomposition EDL
ax4 = fig.add_subplot(gs[0:3, 1], sharex=ax1, sharey=ax1)
ax4.plot(ts, filtered, linewidth=MAJOR_LW, color=color_palette('light-blue'))
ax4.plot(ts, edl, linewidth=MINOR_LW, color=color_palette('dark-orange'),
label="EDL")
ax4.set_ylabel('Amplitude' if units is None else 'Amplitude \n (%s)' % units)
ax4.set_title('EDA Decomposition')
ax4.legend()
ax4.tick_params(axis='x', which='both', bottom=False, top=False,
labelbottom=False)
ax4.spines['bottom'].set_visible(False)
# decomposition EDR
ax5 = fig.add_subplot(gs[3:, 1], sharex=ax1)
ax5.plot(ts[1:], edr, linewidth=MINOR_LW,
color=color_palette('dark-orange'), label="EDR")
ax5.set_ylabel('Amplitude' if units is None else 'Amplitude \n (%s)' % units)
ax5.set_xlabel('Time (s)')
ax5.legend()
# logo
add_logo(fig)
# save to file
if path is not None:
path = utils.normpath(path)
root, ext = os.path.splitext(path)
ext = ext.lower()
if ext not in ['png', 'jpg']:
path = root + '.png'
fig.savefig(path, dpi=200, bbox_inches='tight')
# show
if show:
plt.show()
else:
# close
plt.close(fig)
[docs]def plot_emg(ts=None,
sampling_rate=None,
raw=None,
filtered=None,
onsets=None,
processed=None,
units=None,
path=None,
show=False):
"""Create a summary plot from the output of signals.emg.emg.
Parameters
----------
ts : array
Signal time axis reference (seconds).
sampling_rate : int, float
Sampling frequency (Hz).
raw : array
Raw EMG signal.
filtered : array
Filtered EMG signal.
onsets : array
Indices of EMG pulse onsets.
processed : array, optional
Processed EMG signal according to the chosen onset detector.
units : str, optional
Units of the vertical axis. If provided, the plot title will include
the units information. Default is None.
path : str, optional
If provided, the plot will be saved to the specified file.
show : bool, optional
If True, show the plot immediately.
"""
# get matplotlib parameters
plt.rcParams.update(_get_params())
fig = plt.figure(figsize=(10, 5))
fig.suptitle('EMG Summary', fontsize=12, fontweight='bold')
fig.subplots_adjust(top=0.85, hspace=0.25, wspace=0.34, left=0.1,
right=0.96, bottom=0.18)
if processed is not None:
ax1 = fig.add_subplot(311)
ax2 = fig.add_subplot(312, sharex=ax1)
ax3 = fig.add_subplot(313)
# processed signal
L = len(processed)
T = (L - 1) / sampling_rate
ts_processed = np.linspace(0, T, L, endpoint=True)
ax3.plot(ts_processed, processed,
linewidth=MINOR_LW,
label='Processed',
color=color_palette('blue'))
ax3.set_xlabel('Time (s)')
ax3.set_ylabel('Amplitude')
ax3.legend(loc='upper right')
else:
ax1 = fig.add_subplot(211)
ax2 = fig.add_subplot(212, sharex=ax1)
# signal
ax1.set_title('Signal')
ax1.plot(ts, raw, linewidth=MED_LW, label='Raw', alpha=0.6,
color=color_palette('light-blue'))
ax1.plot(ts, filtered+np.mean(raw), linewidth=MINOR_LW, label='Filtered',
color=color_palette('blue'))
ax1.set_ylabel('Amplitude' if units is None else 'Amplitude (%s)' % units)
ax1.legend(loc='upper right')
ax1.tick_params(axis='x', which='both', bottom=False, top=False,
labelbottom=False)
ax1.spines['bottom'].set_visible(False)
# filtered signal with onsets
ymin = np.min(filtered)
ymax = np.max(filtered)
alpha = 0.1 * (ymax - ymin)
ymax += alpha
ymin -= alpha
ax2.set_title('Event Detection')
ax2.plot(ts, filtered, linewidth=MINOR_LW, color=color_palette('blue'))
ax2.vlines(ts[onsets], ymin, ymax,
color=color_palette('orange'),
linewidth=MINOR_LW,
label='Onsets')
ax2.set_xlabel('Time (s)')
ax2.set_ylabel('Amplitude' if units is None else 'Amplitude (%s)' % units)
ax2.legend(loc='upper right')
# align y axis labels
fig.align_ylabels()
# add logo
add_logo(fig)
# save to file
if path is not None:
path = utils.normpath(path)
root, ext = os.path.splitext(path)
ext = ext.lower()
if ext not in ['png', 'jpg']:
path = root + '.png'
fig.savefig(path, dpi=200, bbox_inches='tight')
# show
if show:
plt.show()
else:
# close
plt.close(fig)
[docs]def plot_resp(ts=None,
raw=None,
filtered=None,
zeros=None,
resp_rate_ts=None,
resp_rate=None,
units=None,
path=None,
show=False):
"""Create a summary plot from the output of signals.ppg.ppg.
Parameters
----------
ts : array
Signal time axis reference (seconds).
raw : array
Raw Resp signal.
filtered : array
Filtered Resp signal.
zeros : array
Indices of Respiration zero crossings.
resp_rate_ts : array
Respiration rate time axis reference (seconds).
resp_rate : array
Instantaneous respiration rate (Hz).
units : str, optional
Units of the vertical axis. If provided, the plot title will include
the units information. Default is None.
path : str, optional
If provided, the plot will be saved to the specified file.
show : bool, optional
If True, show the plot immediately.
"""
# get matplotlib parameters
plt.rcParams.update(_get_params())
fig = plt.figure(figsize=(10, 5))
fig.suptitle('RESP Summary', fontsize=12, fontweight='bold')
fig.subplots_adjust(top=0.85, hspace=0.35, wspace=0.34, left=0.13,
right=0.96, bottom=0.18)
# raw signal
ax1 = fig.add_subplot(311)
ax1.set_title('Signal')
ax1.plot(ts, raw, linewidth=MED_LW, label='Raw', alpha=0.6,
color=color_palette('light-blue'))
ax1.plot(ts, filtered+np.mean(raw), linewidth=MINOR_LW, label='Filtered',
color=color_palette('blue'))
ax1.set_ylabel('Amplitude' if units is None else 'Amplitude \n (%s)' % units)
ax1.legend(loc='upper right')
ax1.tick_params(axis='x', which='both', bottom=False, top=False,
labelbottom=False)
ax1.spines['bottom'].set_visible(False)
# filtered signal with zeros
ax2 = fig.add_subplot(312, sharex=ax1)
ymin = np.min(filtered)
ymax = np.max(filtered)
alpha = 0.1 * (ymax - ymin)
ymax += alpha
ymin -= alpha
ax2.plot(ts, filtered, linewidth=MINOR_LW, color=color_palette('blue'))
ax2.vlines(ts[zeros], ymin, ymax,
color=color_palette('dark-red'),
linewidth=MINOR_LW,
label='Zero crossings')
ax2.set_ylabel('Amplitude' if units is None else 'Amplitude \n (%s)' % units)
ax2.legend(loc='upper right')
ax2.tick_params(axis='x', which='both', bottom=False, top=False,
labelbottom=False)
ax2.spines['bottom'].set_visible(False)
# respiration rate
ax3 = fig.add_subplot(313, sharex=ax1)
ax3.set_title('Respiration Rate')
ax3.plot(resp_rate_ts, resp_rate, linewidth=MAJOR_LW,
color=color_palette('blue'))
ax3.set_xlabel('Time (s)')
ax3.set_ylabel('Respiration Rate \n(Hz)')
# align y axes labels
fig.align_ylabels()
# add logo
add_logo(fig)
# save to file
if path is not None:
path = utils.normpath(path)
root, ext = os.path.splitext(path)
ext = ext.lower()
if ext not in ['png', 'jpg']:
path = root + '.png'
fig.savefig(path, dpi=200, bbox_inches='tight')
# show
if show:
plt.show()
else:
# close
plt.close(fig)
[docs]def plot_eeg(ts=None,
raw=None,
filtered=None,
labels=None,
features_ts=None,
theta=None,
alpha_low=None,
alpha_high=None,
beta=None,
gamma=None,
plf_pairs=None,
plf=None,
path=None,
show=False):
"""Create a summary plot from the output of signals.eeg.eeg.
Parameters
----------
ts : array
Signal time axis reference (seconds).
raw : array
Raw EEG signal.
filtered : array
Filtered EEG signal.
labels : list
Channel labels.
features_ts : array
Features time axis reference (seconds).
theta : array
Average power in the 4 to 8 Hz frequency band; each column is one
EEG channel.
alpha_low : array
Average power in the 8 to 10 Hz frequency band; each column is one
EEG channel.
alpha_high : array
Average power in the 10 to 13 Hz frequency band; each column is one
EEG channel.
beta : array
Average power in the 13 to 25 Hz frequency band; each column is one
EEG channel.
gamma : array
Average power in the 25 to 40 Hz frequency band; each column is one
EEG channel.
plf_pairs : list
PLF pair indices.
plf : array
PLF matrix; each column is a channel pair.
path : str, optional
If provided, the plot will be saved to the specified file.
show : bool, optional
If True, show the plot immediately.
"""
nrows = MAX_ROWS
alpha = 2.
# Get number of channels
nch = raw.shape[1]
figs = []
# raw
fig = _plot_multichannel(ts=ts,
signal=raw,
labels=labels,
nrows=nrows,
alpha=alpha,
title='EEG Summary - Raw',
xlabel='Time (s)',
ylabel='Amplitude')
figs.append(('_Raw', fig))
# filtered
fig = _plot_multichannel(ts=ts,
signal=filtered,
labels=labels,
nrows=nrows,
alpha=alpha,
title='EEG Summary - Filtered',
xlabel='Time (s)',
ylabel='Amplitude')
figs.append(('_Filtered', fig))
# band-power
names = ('Theta Band', 'Lower Alpha Band', 'Higher Alpha Band',
'Beta Band', 'Gamma Band')
args = (theta, alpha_low, alpha_high, beta, gamma)
for n, a in zip(names, args):
fig = _plot_multichannel(ts=features_ts,
signal=a,
labels=labels,
nrows=nrows,
alpha=alpha,
title='EEG Summary - %s' % n,
xlabel='Time (s)',
ylabel='Power')
figs.append(('_' + n.replace(' ', '_'), fig))
# Only plot/compute plf if there is more than one channel
if nch > 1:
# PLF
plf_labels = ['%s vs %s' % (labels[p[0]], labels[p[1]]) for p in plf_pairs]
fig = _plot_multichannel(ts=features_ts,
signal=plf,
labels=plf_labels,
nrows=nrows,
alpha=alpha,
title='EEG Summary - Phase-Locking Factor',
xlabel='Time (s)',
ylabel='PLF')
figs.append(('_PLF', fig))
# save to file
if path is not None:
path = utils.normpath(path)
root, ext = os.path.splitext(path)
ext = ext.lower()
if ext not in ['png', 'jpg']:
ext = '.png'
for n, fig in figs:
path = root + n + ext
fig.savefig(path, dpi=200, bbox_inches='tight')
# show
if show:
plt.show()
else:
# close
for _, fig in figs:
plt.close(fig)
def _yscaling(signal=None, alpha=1.5):
"""Get y axis limits for a signal with scaling.
Parameters
----------
signal : array
Input signal.
alpha : float, optional
Scaling factor.
Returns
-------
ymin : float
Minimum y value.
ymax : float
Maximum y value.
"""
mi = np.min(signal)
m = np.mean(signal)
mx = np.max(signal)
if mi == mx:
ymin = m - 1
ymax = m + 1
else:
ymin = m - alpha * (m - mi)
ymax = m + alpha * (mx - m)
return ymin, ymax
def _plot_multichannel(ts=None,
signal=None,
labels=None,
nrows=10,
alpha=2.,
title=None,
xlabel=None,
ylabel=None):
"""Plot a multi-channel signal.
Parameters
----------
ts : array
Signal time axis reference (seconds).
signal : array
Multi-channel signal; each column is one channel.
labels : list, optional
Channel labels.
nrows : int, optional
Maximum number of rows to use.
alpha : float, optional
Scaling factor for y axis.
title : str, optional
Plot title.
xlabel : str, optional
Label for x axis.
ylabel : str, optional
Label for y axis.
Returns
-------
fig : Figure
Figure object.
"""
# ensure numpy
signal = np.array(signal)
nch = signal.shape[1]
# check labels
if labels is None:
labels = ['Ch. %d' % i for i in range(nch)]
if nch < nrows:
nrows = nch
ncols = int(np.ceil(nch / float(nrows)))
# get matplotlib parameters
plt.rcParams.update(_get_params())
fig = plt.figure()
# title
if title is not None:
fig.suptitle(title)
gs = gridspec.GridSpec(nrows, ncols, hspace=0, wspace=0.2)
# reference axes
ax0 = fig.add_subplot(gs[0, 0])
ax0.plot(ts, signal[:, 0], linewidth=MAJOR_LW, label=labels[0])
ymin, ymax = _yscaling(signal[:, 0], alpha=alpha)
ax0.set_ylim(ymin, ymax)
ax0.legend()
ax0.grid()
axs = {(0, 0): ax0}
for i in range(1, nch - 1):
a = i % nrows
b = int(np.floor(i / float(nrows)))
ax = fig.add_subplot(gs[a, b], sharex=ax0)
axs[(a, b)] = ax
ax.plot(ts, signal[:, i], linewidth=MAJOR_LW, label=labels[i])
ymin, ymax = _yscaling(signal[:, i], alpha=alpha)
ax.set_ylim(ymin, ymax)
ax.legend()
ax.grid()
# last plot
i = nch - 1
a = i % nrows
b = int(np.floor(i / float(nrows)))
ax = fig.add_subplot(gs[a, b], sharex=ax0)
axs[(a, b)] = ax
ax.plot(ts, signal[:, -1], linewidth=MAJOR_LW, label=labels[-1])
ymin, ymax = _yscaling(signal[:, -1], alpha=alpha)
ax.set_ylim(ymin, ymax)
ax.legend()
ax.grid()
if xlabel is not None:
ax.set_xlabel(xlabel)
for b in range(0, ncols - 1):
a = nrows - 1
ax = axs[(a, b)]
ax.set_xlabel(xlabel)
if ylabel is not None:
# middle left
a = nrows // 2
ax = axs[(a, 0)]
ax.set_ylabel(ylabel)
# make layout tight
gs.tight_layout(fig)
return fig
[docs]def plot_ecg(ts=None,
raw=None,
filtered=None,
rpeaks=None,
templates_ts=None,
templates=None,
heart_rate_ts=None,
heart_rate=None,
units=None,
path=None,
show=False):
"""Create a summary plot from the output of signals.ecg.ecg.
Parameters
----------
ts : array
Signal time axis reference (seconds).
raw : array
Raw ECG signal.
filtered : array
Filtered ECG signal.
rpeaks : array
R-peak location indices.
templates_ts : array
Templates time axis reference (seconds).
templates : array
Extracted heartbeat templates.
heart_rate_ts : array
Heart rate time axis reference (seconds).
heart_rate : array
Instantaneous heart rate (bpm).
units : str, optional
Units of the vertical axis. If provided, the plot title will include
the units information. Default is None.
path : str, optional
If provided, the plot will be saved to the specified file.
show : bool, optional
If True, show the plot immediately.
"""
# get matplotlib parameters
plt.rcParams.update(_get_params())
fig = plt.figure(figsize=(10, 5))
fig.suptitle('ECG Summary', fontsize=12, fontweight='bold')
gs = gridspec.GridSpec(6, 2)
fig.subplots_adjust(top=0.88, bottom=0.12, hspace=0.9, wspace=0.3,
left=0.1, right=0.96)
# signal
ax1 = fig.add_subplot(gs[:2, 0])
ax1.set_title('Signal')
ax1.plot(ts, raw, linewidth=MED_LW, label='Raw', alpha=0.6,
color=color_palette('light-blue'))
ax1.plot(ts, filtered+np.mean(raw), linewidth=MINOR_LW, label='Filtered',
color=color_palette('blue'))
ax1.set_ylabel('Amplitude' if units is None else 'Amplitude (%s)' % units)
ax1.legend(loc='upper right')
ax1.tick_params(axis='x', which='both', bottom=False, top=False,
labelbottom=False)
ax1.spines['bottom'].set_visible(False)
# filtered signal with rpeaks
ax2 = fig.add_subplot(gs[2:4, 0], sharex=ax1)
ax2.set_title('R-Peak Detection')
ymin = np.min(filtered)
ymax = np.max(filtered)
alpha = 0.1 * (ymax - ymin)
ymax += alpha
ymin -= alpha
ax2.plot(ts, filtered, linewidth=MINOR_LW, color=color_palette('blue'))
ax2.plot(ts[rpeaks], filtered[rpeaks], ls='None', marker='x',
color=color_palette('dark-red'), label='Peaks')
ax2.set_ylabel('Amplitude' if units is None else 'Amplitude (%s)' % units)
ax2.legend(loc='upper right')
ax2.tick_params(axis='x', which='both', bottom=False, top=False,
labelbottom=False)
ax2.spines['bottom'].set_visible(False)
# heart rate
ax3 = fig.add_subplot(gs[4:, 0], sharex=ax1)
ax3.set_title('Heart Rate')
ax3.plot(heart_rate_ts, heart_rate, linewidth=MAJOR_LW,
color=color_palette('blue'))
ax3.set_xlabel('Time (s)')
ax3.set_ylabel('Heart Rate (bpm)')
# align y axis labels
fig.align_ylabels([ax1, ax2, ax3])
# templates
ax4 = fig.add_subplot(gs[1:5, 1])
ax4.set_title('Templates')
ax4.plot(templates_ts, templates.T, linewidth=MINOR_LW, alpha=0.5,
color=color_palette('blue'))
ax4.set_xlabel('Time (s)')
ax4.set_ylabel('Amplitude' if units is None else 'Amplitude (%s)' % units)
# add logo
add_logo(fig)
# save to file
if path is not None:
path = utils.normpath(path)
root, ext = os.path.splitext(path)
ext = ext.lower()
if ext not in ['png', 'jpg']:
path = root + '.png'
fig.savefig(path, dpi=200, bbox_inches='tight')
# show
if show:
plt.show()
else:
# close
plt.close(fig)
[docs]def plot_bcg(ts=None,
raw=None,
filtered=None,
jpeaks=None,
templates_ts=None,
templates=None,
heart_rate_ts=None,
heart_rate=None,
path=None,
show=False):
"""Create a summary plot from the output of signals.bcg.bcg.
Parameters
----------
ts : array
Signal time axis reference (seconds).
raw : array
Raw ECG signal.
filtered : array
Filtered ECG signal.
ipeaks : array
I-peak location indices.
templates_ts : array
Templates time axis reference (seconds).
templates : array
Extracted heartbeat templates.
heart_rate_ts : array
Heart rate time axis reference (seconds).
heart_rate : array
Instantaneous heart rate (bpm).
path : str, optional
If provided, the plot will be saved to the specified file.
show : bool, optional
If True, show the plot immediately.
"""
# get matplotlib parameters
plt.rcParams.update(_get_params())
fig = plt.figure()
fig.suptitle('BCG Summary')
gs = gridspec.GridSpec(6, 2)
# raw signal
ax1 = fig.add_subplot(gs[:2, 0])
ax1.plot(ts, raw, linewidth=MAJOR_LW, label='Raw')
ax1.set_ylabel('Amplitude')
ax1.legend()
ax1.grid()
# filtered signal with rpeaks
ax2 = fig.add_subplot(gs[2:4, 0], sharex=ax1)
ymin = np.min(filtered)
ymax = np.max(filtered)
alpha = 0.1 * (ymax - ymin)
ymax += alpha
ymin -= alpha
ax2.plot(ts, filtered, linewidth=MAJOR_LW, label='Filtered')
ax2.vlines(ts[jpeaks], ymin, ymax,
color='m',
linewidth=MINOR_LW,
label='J-peaks')
ax2.set_ylabel('Amplitude')
ax2.legend()
ax2.grid()
# heart rate
ax3 = fig.add_subplot(gs[4:, 0], sharex=ax1)
ax3.plot(heart_rate_ts, heart_rate, linewidth=MAJOR_LW, label='Heart Rate')
ax3.set_xlabel('Time (s)')
ax3.set_ylabel('Heart Rate (bpm)')
ax3.legend()
ax3.grid()
# templates
ax4 = fig.add_subplot(gs[1:5, 1])
ax4.plot(templates_ts, templates.T, 'm', linewidth=MINOR_LW, alpha=0.7)
ax4.set_xlabel('Time (s)')
ax4.set_ylabel('Amplitude')
ax4.set_title('Templates')
ax4.grid()
# make layout tight
gs.tight_layout(fig)
# save to file
if path is not None:
path = utils.normpath(path)
root, ext = os.path.splitext(path)
ext = ext.lower()
if ext not in ['png', 'jpg']:
path = root + '.png'
fig.savefig(path, dpi=200, bbox_inches='tight')
# show
if show:
plt.show()
else:
# close
plt.close(fig)
[docs]def plot_pcg(ts=None,
raw=None,
filtered=None,
peaks=None,
heart_sounds=None,
heart_rate_ts=None,
inst_heart_rate=None,
units=None,
path=None,
show=False):
"""Create a summary plot from the output of signals.pcg.pcg.
Parameters
----------
ts : array
Signal time axis reference (seconds).
raw : array
Raw PCG signal.
filtered : array
Filtered PCG signal.
peaks : array
Peak location indices.
heart_sounds : array
Classification of peaks as S1 or S2
heart_rate_ts : array
Heart rate time axis reference (seconds).
inst_heart_rate : array
Instantaneous heart rate (bpm).
units : str, optional
Units of the vertical axis. If provided, the plot title will include
the units information. Default is None.
path : str, optional
If provided, the plot will be saved to the specified file.
show : bool, optional
If True, show the plot immediately.
"""
# get matplotlib parameters
plt.rcParams.update(_get_params())
fig = plt.figure(figsize=(12, 5))
fig.suptitle('PCG Summary', fontsize=12, fontweight='bold')
gs = gridspec.GridSpec(6, 2)
fig.subplots_adjust(top=0.88, bottom=0.12, hspace=0.9, wspace=0.3,
left=0.1, right=0.96)
# signal
ax1 = fig.add_subplot(gs[:2, 0])
ax1.set_title('Signal')
ax1.plot(ts, raw, linewidth=MED_LW, label='Raw',
color=color_palette('light-blue'))
ax1.plot(ts, filtered+np.mean(raw), linewidth=MINOR_LW, label='Filtered',
color=color_palette('blue'))
ax1.set_ylabel('Amplitude' if units is None else 'Amplitude (%s)' % units)
ax1.legend(loc='upper right')
ax1.tick_params(axis='x', which='both', bottom=False, top=False,
labelbottom=False)
ax1.spines['bottom'].set_visible(False)
# filtered signal with sounds
ax2 = fig.add_subplot(gs[2:4, 0], sharex=ax1)
ax2.set_title('Heart Sound Detection')
ymin = np.min(filtered)
ymax = np.max(filtered)
alpha = 0.1 * (ymax - ymin)
ymax += alpha
ymin -= alpha
ax2.plot(ts, filtered, linewidth=MINOR_LW, color=color_palette('blue'))
ax2.vlines(ts[peaks], ymin, ymax,
color=color_palette('dark-red'),
linewidth=MED_LW,
label='Heart Sounds')
ax2.set_ylabel('Amplitude' if units is None else 'Amplitude (%s)' % units)
ax2.legend(loc='upper right')
ax2.tick_params(axis='x', which='both', bottom=False, top=False,
labelbottom=False)
ax2.spines['bottom'].set_visible(False)
# heart rate
ax3 = fig.add_subplot(gs[4:, 0], sharex=ax1)
ax3.set_title('Heart Rate')
ax3.plot(heart_rate_ts, inst_heart_rate, linewidth=MAJOR_LW,
color=color_palette('blue'))
ax3.set_xlabel('Time (s)')
ax3.set_ylabel('Heart Rate (bpm)')
# align y axis labels
fig.align_ylabels([ax1, ax2, ax3])
# heart sounds
ax4 = fig.add_subplot(gs[1:5, 1], sharex=ax1)
ax4.plot(ts, filtered, linewidth=MINOR_LW, color=color_palette('blue'))
for i in range(0, len(peaks)):
text = "S" + str(int(heart_sounds[i]))
plt.annotate(text, (ts[peaks[i]], ymax - alpha), ha='center', va='center', size=9,
color=color_palette('dark-grey'))
ax4.set_xlabel('Time (s)')
ax4.set_ylabel('Amplitude' if units is None else 'Amplitude (%s)' % units)
ax4.set_title('Heart Sounds Classification')
# add logo
add_logo(fig)
# save to file
if path is not None:
path = utils.normpath(path)
root, ext = os.path.splitext(path)
ext = ext.lower()
if ext not in ['png', 'jpg']:
path = root + '.png'
fig.savefig(path, dpi=200, bbox_inches='tight')
# show
if show:
plt.show()
else:
# close
plt.close(fig)
def _plot_rates(thresholds, rates, variables,
lw=1,
colors=None,
alpha=1,
eer_idx=None,
labels=False,
ax=None):
"""Plot biometric rates.
Parameters
----------
thresholds : array
Classifier thresholds.
rates : dict
Dictionary of rates.
variables : list
Keys from 'rates' to plot.
lw : int, float, optional
Plot linewidth.
colors : list, optional
Plot line color for each variable.
alpha : float, optional
Plot line alpha value.
eer_idx : int, optional
Classifier reference index for the Equal Error Rate.
labels : bool, optional
If True, will show plot labels.
ax : axis, optional
Plot Axis to use.
Returns
-------
fig : Figure
Figure object.
"""
# get matplotlib parameters
plt.rcParams.update(_get_params())
if ax is None:
fig = plt.figure()
ax = fig.add_subplot(111)
else:
fig = ax.figure
if colors is None:
x = np.linspace(0., 1., len(variables))
colors = plt.get_cmap('rainbow')(x)
if labels:
for i, v in enumerate(variables):
ax.plot(thresholds, rates[v], colors[i],
lw=lw,
alpha=alpha,
label=v)
else:
for i, v in enumerate(variables):
ax.plot(thresholds, rates[v], colors[i], lw=lw, alpha=alpha)
if eer_idx is not None:
x, y = rates['EER'][eer_idx]
ax.vlines(x, 0, 1, 'r', lw=lw)
ax.set_title('EER = %0.2f %%' % (100. * y))
return fig
[docs]def plot_biometrics(assessment=None, eer_idx=None, path=None, show=False):
"""Create a summary plot of a biometrics test run.
Parameters
----------
assessment : dict
Classification assessment results.
eer_idx : int, optional
Classifier reference index for the Equal Error Rate.
path : str, optional
If provided, the plot will be saved to the specified file.
show : bool, optional
If True, show the plot immediately.
"""
# get matplotlib parameters
plt.rcParams.update(_get_params())
fig = plt.figure()
fig.suptitle('Biometrics Summary')
c_sub = ['#008bff', '#8dd000']
c_global = ['#0037ff', 'g']
ths = assessment['thresholds']
auth_ax = fig.add_subplot(121)
id_ax = fig.add_subplot(122)
# subject results
for sub in six.iterkeys(assessment['subject']):
auth_rates = assessment['subject'][sub]['authentication']['rates']
_ = _plot_rates(ths, auth_rates, ['FAR', 'FRR'],
lw=MINOR_LW,
colors=c_sub,
alpha=0.4,
eer_idx=None,
labels=False,
ax=auth_ax)
id_rates = assessment['subject'][sub]['identification']['rates']
_ = _plot_rates(ths, id_rates, ['MR', 'RR'],
lw=MINOR_LW,
colors=c_sub,
alpha=0.4,
eer_idx=None,
labels=False,
ax=id_ax)
# global results
auth_rates = assessment['global']['authentication']['rates']
_ = _plot_rates(ths, auth_rates, ['FAR', 'FRR'],
lw=MAJOR_LW,
colors=c_global,
alpha=1,
eer_idx=eer_idx,
labels=True,
ax=auth_ax)
id_rates = assessment['global']['identification']['rates']
_ = _plot_rates(ths, id_rates, ['MR', 'RR'],
lw=MAJOR_LW,
colors=c_global,
alpha=1,
eer_idx=eer_idx,
labels=True,
ax=id_ax)
# set labels and grids
auth_ax.set_xlabel('Threshold')
auth_ax.set_ylabel('Authentication')
auth_ax.grid()
auth_ax.legend()
id_ax.set_xlabel('Threshold')
id_ax.set_ylabel('Identification')
id_ax.grid()
id_ax.legend()
# make layout tight
fig.tight_layout()
# save to file
if path is not None:
path = utils.normpath(path)
root, ext = os.path.splitext(path)
ext = ext.lower()
if ext not in ['png', 'jpg']:
path = root + '.png'
fig.savefig(path, dpi=200, bbox_inches='tight')
# show
if show:
plt.show()
else:
# close
plt.close(fig)
[docs]def plot_clustering(data=None, clusters=None, path=None, show=False):
"""Create a summary plot of a data clustering.
Parameters
----------
data : array
An m by n array of m data samples in an n-dimensional space.
clusters : dict
Dictionary with the sample indices (rows from `data`) for each cluster.
path : str, optional
If provided, the plot will be saved to the specified file.
show : bool, optional
If True, show the plot immediately.
"""
# get matplotlib parameters
plt.rcParams.update(_get_params())
fig = plt.figure()
fig.suptitle('Clustering Summary')
ymin, ymax = _yscaling(data, alpha=1.2)
# determine number of clusters
keys = list(clusters)
nc = len(keys)
if nc <= 4:
nrows = 2
ncols = 4
else:
area = nc + 4
# try to fit to a square
nrows = int(np.ceil(np.sqrt(area)))
if nrows > MAX_ROWS:
# prefer to increase number of columns
nrows = MAX_ROWS
ncols = int(np.ceil(area / float(nrows)))
# plot grid
gs = gridspec.GridSpec(nrows, ncols, hspace=0.2, wspace=0.2)
# global axes
ax_global = fig.add_subplot(gs[:2, :2])
# cluster axes
c_grid = np.ones((nrows, ncols), dtype='bool')
c_grid[:2, :2] = False
c_rows, c_cols = np.nonzero(c_grid)
# generate color map
x = np.linspace(0., 1., nc)
cmap = plt.get_cmap('rainbow')
for i, k in enumerate(keys):
aux = data[clusters[k]]
color = cmap(x[i])
label = 'Cluster %s' % k
ax = fig.add_subplot(gs[c_rows[i], c_cols[i]], sharex=ax_global)
ax.set_ylim([ymin, ymax])
ax.set_title(label)
ax.grid()
if len(aux) > 0:
ax_global.plot(aux.T, color=color, lw=MINOR_LW, alpha=0.7)
ax.plot(aux.T, color=color, lw=MAJOR_LW)
ax_global.set_title('All Clusters')
ax_global.set_ylim([ymin, ymax])
ax_global.grid()
# make layout tight
gs.tight_layout(fig)
# save to file
if path is not None:
path = utils.normpath(path)
root, ext = os.path.splitext(path)
ext = ext.lower()
if ext not in ['png', 'jpg']:
path = root + '.png'
fig.savefig(path, dpi=200, bbox_inches='tight')
# show
if show:
plt.show()
else:
# close
plt.close(fig)
[docs]def plot_rri(rri, rri_trend=None, legends=None, ax=None, show=False):
"""Plot a series of RR intervals.
Parameters
----------
rri : array
RR-intervals (ms).
rri_trend : array, optional
RR-intervals trend (ms).
legends : dict, optional
Dictionary of features to add to the plot legend.
ax : axis, optional
Plot Axis to use.
show : bool, optional
If True, show the plot immediately.
"""
# time axis
t = np.cumsum(rri) / 1000.
# plot
if ax is None:
fig, ax = plt.subplots(1, 1, figsize=(8, 4))
fig.suptitle('HRV - RR Intervals', fontsize=12, fontweight='bold')
fig.subplots_adjust(bottom=0.17)
add_logo(fig)
# plot signal
ax.plot(t, rri, color=color_palette('blue'), linewidth=MAJOR_LW,
label='RR Intervals')
ax.set_ylabel('RRI (ms)')
ax.set_xlabel('Time (s)')
if rri_trend is not None:
ax.plot(t, rri_trend, color=color_palette('dark-red'), alpha=0.5,
linewidth=MED_LW, label='Trend')
# plot legend
if legends is not None:
handles, labels = ax.get_legend_handles_labels()
for key, value in legends.items():
new_patch = patches.Patch(color='white', alpha=0)
handles.extend([new_patch])
labels.extend(['%s = %.2f %s' % (key, value[0], value[1])])
try:
pos = ax.get_position()
ax.set_position([pos.x0, pos.y0, pos.width * 0.9, pos.height])
ax.legend(handles=handles, labels=labels, loc='upper left',
bbox_to_anchor=(1.01, 1.02), frameon=False)
except ValueError:
pass
else:
ax.legend(loc='upper right')
# show
if show:
plt.show()
[docs]def plot_poincare(rri=None,
s=None,
sd1=None,
sd2=None,
legends=None,
ax=None,
show=False):
"""Plot a Poincaré plot of a series of RR intervals (RRI[i+1] vs. RRI[i])
from the output of signals.hrv.compute_poincare.
Parameters
----------
rri : array
RR-intervals (ms).
s : float
S - Area of the ellipse of the Poincaré plot (ms^2).
sd1 : float
SD1 - Poincaré plot standard deviation perpendicular to the identity
line (ms).
sd2 : float
SD2 - Poincaré plot standard deviation along the identity line (ms).
legends : dict, optional
Dictionary of features to add to the plot legend.
ax : axis, optional
Plot Axis to use.
show : bool, optional
If True, show the plot immediately.
"""
x, y = rri[:-1], rri[1:]
rr_mean = rri.mean()
if ax is None:
fig, ax = plt.subplots(figsize=(7, 5))
fig.suptitle('HRV - Poincaré Plot', fontsize=12, fontweight='bold')
# add logo
add_logo(fig)
ax.set_xlabel('$RR_i$ (ms)')
ax.set_ylabel('$RR_{i+1}$ (ms)')
# plot Poincaré data points
ax.scatter(x, y, marker='.', color=color_palette('blue'), alpha=0.5, s=100,
zorder=1)
ax.set_xlim([np.min(rri) - 50, np.max(rri) + 50])
ax.set_ylim([np.min(rri) - 50, np.max(rri) + 50])
ax.set_aspect(1. / ax.get_data_ratio())
# draw identity line (RRi+1=RRi)
lims = [np.min([ax.get_xlim(), ax.get_ylim()]), # min of both axes
np.max([ax.get_xlim(), ax.get_ylim()])] # max of both axes
ax.plot(lims, lims, linewidth=0.7, color=color_palette('grey'),
linestyle='--', zorder=2, label='Identity line')
# draw ellipse
ellipse = patches.Ellipse((rr_mean, rr_mean), sd1 * 2, sd2 * 2, angle=-45,
linewidth=1, edgecolor=color_palette('dark-grey'),
facecolor='None', label='S = %.1f ms$^2$' % s, zorder=3)
ax.add_artist(ellipse)
# draw SD1 and SD2
ax.arrow(rr_mean, rr_mean, sd1 * np.cos(3 * np.pi / 4), sd1 * np.sin(3 * np.pi / 4),
facecolor=color_palette('dark-red'), edgecolor=color_palette('dark-red'),
linewidth=2, length_includes_head=True, head_width=4, head_length=4,
label='SD1 = %.1f ms' % sd1, zorder=3)
ax.arrow(rr_mean, rr_mean, sd2 * np.cos(np.pi / 4), sd2 * np.sin(np.pi / 4),
facecolor=color_palette('yellow'), edgecolor=color_palette('yellow'),
linewidth=2, length_includes_head=True, head_width=4, head_length=4,
label='SD2 = %.1f ms' % sd2, zorder=3)
# draw SD1 and SD2 axes
f = 1.25 # scaling factor
ax.add_artist(
lines.Line2D([rr_mean - f * sd1 * np.cos(3 * np.pi / 4), rr_mean + f * sd1 * np.cos(3 * np.pi / 4)],
[rr_mean + f * sd1 * np.cos(3 * np.pi / 4), rr_mean - f * sd1 * np.cos(3 * np.pi / 4)],
lw=1, color='0.2'))
ax.add_artist(lines.Line2D([rr_mean - f * sd2 * np.cos(np.pi / 4), rr_mean + f * sd2 * np.cos(np.pi / 4)],
[rr_mean - f * sd2 * np.cos(np.pi / 4), rr_mean + f * sd2 * np.cos(np.pi / 4)],
lw=1, color='0.2'))
# adjust grid
ax.set_axisbelow(True)
# add extra labels
# update figure legend
handles, labels = ax.get_legend_handles_labels()
if legends is not None:
for key, value in legends.items():
if value is not None:
new_patch = patches.Patch(color='white', alpha=0)
handles.extend([new_patch])
labels.extend(['%s = %.2f' % (key, value)])
# change handle for ellipse and arrows
handles[1] = plt.Line2D([], [], color=color_palette('dark-grey'),
marker="o", markersize=10, linewidth=0,
markerfacecolor='none') # ellipse
handles[2] = plt.Line2D([], [], color=color_palette('dark-red'),
linestyle="-", linewidth=1) # SD1
handles[3] = plt.Line2D([], [], color=color_palette('yellow'),
linestyle="-", linewidth=1) # SD2
# create legend
pos = ax.get_position()
ax.set_position([pos.x0, pos.y0, pos.width * 0.9, pos.height])
ax.legend(handles=handles, labels=labels, loc='upper left',
bbox_to_anchor=(1.01, 1.02), frameon=False)
# show
if show:
plt.show()
[docs]def plot_hrv_hist(rri=None,
bins=None,
q_hist=None,
hti=None,
tinn=None,
ax=None,
show=False):
"""Plots the RRI histogram with the corresponding geometrical HRV features
from the output of signals.hrv.compute_geometrical.
Parameters
----------
rri : array
RR-intervals (ms).
bins : array
Histogram bins.
q_hist : array
Multilinear function fitted to the histogram.
hti : float
HTI - HRV triangular index - Integral of the density of the RR interval
histogram divided by its height.
tinn : float
TINN - Baseline width of RR interval histogram (ms).
ax : axis, optional
Plot Axis to use.
show : bool, optional
If True, show the plot immediately.
"""
# plot histogram and triangle
if ax is None:
fig, ax = plt.subplots()
fig.suptitle('HRV - RRI Distribution', fontsize=12, fontweight='bold')
# add logo
add_logo(fig)
ax.hist(rri, bins, facecolor=color_palette('light-blue'),
edgecolor=color_palette('dark-grey'), label='HTI: %.1f' % hti)
ax.plot(bins, q_hist, color=color_palette('dark-red'), linewidth=1.5,
label='TINN: %.1f ms' % tinn)
ax.set_xlabel('RR Interval (ms)')
ax.set_ylabel('Count')
ax.locator_params(axis='y', integer=True)
pos = ax.get_position()
ax.set_position([pos.x0, pos.y0, pos.width * 0.9, pos.height])
ax.legend(loc='upper left',
bbox_to_anchor=(1.01, 1.02), frameon=False)
# adjust grid to be in the background
ax.set_axisbelow(True)
# show
if show:
plt.show()
[docs]def plot_hrv_fbands(frequencies=None,
powers=None,
fbands=None,
method_name=None,
legends=None,
ax=None,
show=False):
"""Plots the power spectrum and highlights the defined frequency bands
from the output of signals.hrv.compute_fbands.
Parameters
----------
frequencies : array
Frequency axis.
powers : array
Power spectrum values for the frequency axis.
fbands : dict, optional
Dictionary containing the limits of the frequency bands.
method_name : str, optional
Method that was used to compute the power spectrum.
legends : dict, optional
Additional legend elements.
ax : axis, optional
Plot Axis to use.
show : bool, optional
If True, show the plot immediately.
"""
spectrum_colors = {'ulf': '#e6eff6',
'vlf': '#89b4c4',
'lf': '#548999',
'hf': '#f1d3a1',
'vhf': '#e3dbd9'
}
# convert power values
powers = powers / (10 ** 6) # to s^2/Hz
# initialize plot
if ax is None:
fig, ax = plt.subplots()
fig.suptitle('HRV - Power Spectral Density', fontsize=12, fontweight='bold')
# add logo
add_logo(fig)
# figure attributes
if method_name is not None:
ax.set_title(f'({method_name})')
ax.set_xlabel('Frequency (Hz)')
ax.set_ylabel('Power (s$^2$/Hz)')
ax.set_xlim([0, 0.5])
# plot spectrum
ax.plot(frequencies, powers, linewidth=1, color='0.2')
ax.margins(0)
# plot frequency bands
for fband in fbands.keys():
band = np.argwhere((frequencies >= fbands[fband][0]) & (frequencies <= fbands[fband][-1])).reshape(-1)
color = spectrum_colors[fband]
if len(band) > 0:
ax.fill_between(frequencies[band], powers[band], color=color,
label=f'{fband.upper()}: {legends[fband+"_rpwr"]*100:.1f}%')
# update figure legend
handles, labels = ax.get_legend_handles_labels()
if legends.__len__() != 0:
for key, value in legends.items():
if not key.endswith('_rpwr') and value is not None:
new_patch = patches.Patch(color='white', alpha=0)
handles.extend([new_patch])
labels.extend(['%s = %.2f' % (key, value)])
pos = ax.get_position()
ax.set_position([pos.x0, pos.y0, pos.width * 0.9, pos.height])
ax.legend(handles=handles, labels=labels, loc='upper left',
bbox_to_anchor=(1.01, 1.02), frameon=False)
# adjust grid
ax.set_axisbelow(True)
# show
if show:
plt.show()
[docs]def plot_hrv(rri,
rri_trend=None,
td_out=None,
nl_out=None,
fd_out=None,
show=False):
"""Create a summary plot of a HRV analysis.
Parameters
----------
rri : array
RR-intervals (ms).
rri_trend : array, optional
RR-intervals trend (ms).
td_out : dict
Output of signals.hrv.timedomain.
nl_out : dict
Output of signals.hrv.nonlinear.
fd_out : dict
Output of signals.hrv.frequencyomain.
show : bool, optional
If True, show the plot immediately.
"""
# convert ReturnTuple to dict
if td_out is not None:
td_out = td_out.as_dict()
if nl_out is not None:
nl_out = nl_out.as_dict()
if fd_out is not None:
fd_out = fd_out.as_dict()
# plot
# get matplotlib parameters
plt.rcParams.update(_get_params())
fig = plt.figure(figsize=(12, 6))
fig.suptitle('HRV Summary', fontsize=12, fontweight='bold')
gs = gridspec.GridSpec(6, 2)
# plot rri and display time domain features
ax1 = fig.add_subplot(gs[:2, 0])
time_legends = {'Mean': (td_out['rr_mean'], 'ms'),
'Median': (td_out['rr_median'], 'ms'),
'MinMax': (td_out['rr_minmax'], 'ms'),
'SDNN': (td_out['sdnn'], 'ms'),
'RMSSD': (td_out['rmssd'], 'ms'),
'pNN50': (td_out['pnn50'], '%'),
}
plot_rri(rri=rri, rri_trend=rri_trend, legends=time_legends, ax=ax1,
show=False)
ax1.set_title('Time Domain', fontweight='bold')
# plot hrv hist
ax2 = fig.add_subplot(gs[2:, 0])
bins = td_out['bins']
q_hist = td_out['q_hist']
hti = td_out['hti']
tinn = td_out['tinn']
plot_hrv_hist(rri=rri, bins=bins, q_hist=q_hist, hti=hti, tinn=tinn,
ax=ax2, show=False)
# plot pointcare and display non-linear features
ax3 = fig.add_subplot(gs[:3, 1])
ax3.set_title('Non-linear Domain', fontweight='bold')
rri_pc = rri - rri_trend if rri_trend is not None else rri
s = nl_out['s']
sd1 = nl_out['sd1']
sd2 = nl_out['sd2']
poincare_legend = {'SD1/SD2': nl_out['sd12'],
'SD2/SD1': nl_out['sd21'],
'SampEn': nl_out['sampen'] if 'sampen' in nl_out.keys() else None,
'AppEn': nl_out['appen'] if 'appen' in nl_out.keys() else None,
}
plot_poincare(rri=rri_pc, s=s, sd1=sd1, sd2=sd2, legends=poincare_legend,
ax=ax3, show=False)
# plot hrv fbands
ax4 = fig.add_subplot(gs[3:, 1])
frequencies = fd_out['frequencies']
powers = fd_out['powers']
fbands = fd_out['fbands']
method_name = fd_out['freq_method']
freq_legends = {'LF/HF': fd_out['lf_hf']}
for key in fd_out.keys():
if key.endswith('_rpwr'):
freq_legends[key] = fd_out[key]
plot_hrv_fbands(frequencies=frequencies, powers=powers, fbands=fbands,
method_name=method_name, legends=freq_legends, ax=ax4,
show=False)
ax4.set_title('Frequency Domain', fontweight='bold')
# tight layout
gs.tight_layout(fig)
# adjust margins
gs.update(right=0.88, bottom=0.14)
# add logo
add_logo(fig)
# show
if show:
plt.show()