Respiration tutorial

import numpy as np
import matplotlib.pyplot as plt
from pathlib import Path
from pprint import pprint

import physio

Respiration cycle detection. The faster way: using parameter_preset.

The fastest way to process respiration with physio is to use compute_respiration() which is a high-level wrapper function that simplifies respiratory signal analysis. To use this function, you must provide:

  • raw_resp : the raw respiratory signal as a NumPy array.

  • srate : the sampling rate of the respiratory signal

  • parameter_preset : a string specifying the type of respiratory data, which determines the set of parameters used for processing. Can be one of: human_airflow, human_co2, human_belt, rat_plethysmo, or rat_etisens_belt.

When called, compute_respiration() performs the following:

  • Preprocesses the respiratory signal (returns a NumPy array: resp)

  • Computes cycle-by-cycle features (returns a pd.DataFrame: resp_cycles)

Warning: The orientation of the raw_resp trace is important (multiply it by -1 for reversing it if necessary). Inspiration must point downward for human_airflow or human_co2, because downward deflections are interpreted by compute_respiration() as inspiration. For rat_plethysmo or rat_etisens_belt, the inspiration–expiration transition must point upward.

For this tutorial, we will use an internal file already stored in NumPy format for demonstration purposes.

raw_resp = np.load('resp1_airflow.npy') # load respi
srate = 1000. # our example signals have been recorded at 1000 Hz

times = np.arange(raw_resp.size) / srate # build time vector


# the easiest way is to use predefined parameters
resp, resp_cycles = physio.compute_respiration(raw_resp, srate, parameter_preset='human_airflow')  # set 'human_airflow' as preset because example resp is an airflow from humans

# resp_cycles is a dataframe containing all cycles and related features (duration, amplitude, volume, timing, etc...).
print(resp_cycles)

inspi_ind = resp_cycles['inspi_index'].values # get index of inspiration start points
expi_ind = resp_cycles['expi_index'].values # get index of expiration start points

fig, ax = plt.subplots()
ax.plot(times, raw_resp)
ax.plot(times, resp)
ax.scatter(times[inspi_ind], resp[inspi_ind], color='green', label = 'inspiration start')
ax.scatter(times[expi_ind], resp[expi_ind], color='red', label = 'expiration start')
ax.set_xlabel('Time (s)')
ax.set_ylabel('Amplitude (AU)')
ax.set_ylim(-1750, -1450)
ax.set_title('Respiratory cycle detection')
ax.legend(loc = 'upper right')

ax.set_xlim(110, 170)
Respiratory cycle detection
    inspi_index  expi_index  ...  expi_peak_time  total_volume
0           963        3849  ...           4.969    291.424431
1          8755       12470  ...          13.479    248.679461
2         16629       20085  ...          20.995    225.343872
3         25589       28826  ...          30.576    180.226656
4         35467       39250  ...          41.144    218.772816
5         45631       48992  ...          50.698    185.303221
6         55914       59772  ...          61.809    222.938344
7         67325       70801  ...          72.750    173.728543
8         78023       81420  ...          82.834    241.129370
9         86030       89213  ...          90.996    162.205566
10        94623       97972  ...         100.891   1083.803965
11       104423      108683  ...         110.518    237.217117
12       114252      117877  ...         119.319    216.113417
13       124700      127879  ...         129.999    227.066006
14       134461      138178  ...         140.396    196.708830
15       145780      149124  ...         151.674    145.025697
16       156359      159809  ...         161.764    173.603192
17       165930      169247  ...         171.542    179.954129
18       175415      178225  ...         180.364    129.920402
19       184867      188712  ...         190.757    172.982517
20       195179      199483  ...         201.509    187.120995
21       205152      209074  ...         210.793    218.716716
22       214162      217778  ...         220.553    206.830544
23       223497      227737  ...         229.680    222.818645
24       233353      237747  ...         239.786    254.319332
25       242949      247667  ...         249.696    318.826908
26       253240      257182  ...         259.468    258.070059
27       263116      266749  ...         269.279    204.821141
28       273102      277759  ...         279.953    244.363774
29       284946      288731  ...         291.162    189.545873

[30 rows x 21 columns]

(110.0, 170.0)

What is parameter_preset ?

Using parameter_preset tells compute_respiration() to process respiration according to a predefined set of parameters already optimized by physio.

To get an idea of the default parameters used in parameter_preset, you can call get_respiration_parameters().

Here is an example showing the set of parameters applied in the case of a human airflow signal.

parameters = physio.get_respiration_parameters('human_airflow') # parameters is a nested dictionary of parameters used at each processing step.
pprint(parameters) # pprint to "pretty print"
{'baseline': {'baseline_mode': 'median'},
 'cycle_clean': {'low_limit_log_ratio': 4.5,
                 'variable_names': ['inspi_volume', 'expi_volume']},
 'cycle_detection': {'epsilon_factor1': 10.0,
                     'epsilon_factor2': 5.0,
                     'inspiration_adjust_on_derivative': False,
                     'method': 'crossing_baseline'},
 'preprocess': {'band': 7.0, 'btype': 'lowpass', 'ftype': 'bessel', 'order': 5},
 'sensor_type': 'airflow',
 'smooth': {'sigma_ms': 60.0, 'win_shape': 'gaussian'}}

Tuning parameters if unsatisfied

Variability during data acquisition (subject, acquisition system) can affect the recorded respiratory signal. Such variability may make some predefined parameters of physio inappropriate.

In this situation, you can tune certain parameters by re-assigning values to the keys of the parameters dictionary. You may also tune multiple parameters at once if necessary.

To fine-tune parameters properly, a good understanding of each parameter’s role is required. For this reason, we have dedicated a whole section to this topic — see the “Parameters” section.

For example, here we modify the length of the smoothing parameter, which corresponds to the duration in milliseconds of the Gaussian smoothing kernel (the width of the bell-shaped curve), usually referred to as sigma.

# let's change one parameter in the nested structure ...
parameters['smooth']['sigma_ms'] = 100. # at the key "smooth" and the sub-key "sigma_ms", the default values is 60. Here we replace it by 100 milliseconds to induce more smoothing
pprint(parameters) # pprint to "pretty print"

# ... and use them by providing it to "parameters"
resp, resp_cycles = physio.compute_respiration(raw_resp, srate, parameters=parameters) # preset_parameters = None in this case, because parameters is now explicitly defined
{'baseline': {'baseline_mode': 'median'},
 'cycle_clean': {'low_limit_log_ratio': 4.5,
                 'variable_names': ['inspi_volume', 'expi_volume']},
 'cycle_detection': {'epsilon_factor1': 10.0,
                     'epsilon_factor2': 5.0,
                     'inspiration_adjust_on_derivative': False,
                     'method': 'crossing_baseline'},
 'preprocess': {'band': 7.0, 'btype': 'lowpass', 'ftype': 'bessel', 'order': 5},
 'sensor_type': 'airflow',
 'smooth': {'sigma_ms': 100.0, 'win_shape': 'gaussian'}}

Using low-level functions to control each step of the pipeline

compute_respiration() is a high-level wrapper function that simplifies respiratory signal analysis. However, you may want more control over the entire process and therefore use the low-level functions of physio. Here are the details of all low-level functions used internally by compute_respiration().

resp = physio.preprocess(raw_resp, srate, band=25., btype='lowpass', ftype='bessel', order=5, normalize=False) # filter
resp = physio.smooth_signal(resp, srate, win_shape='gaussian', sigma_ms=90.0) # smooth

baseline = physio.get_respiration_baseline(resp, srate, baseline_mode='median') # compute baseline level
print('baseline :', baseline)

# this will give a numpy.array with shape (num_cycle, 3)
cycles = physio.detect_respiration_cycles(resp, srate, baseline_mode='manual', baseline=baseline) # detect inspi-expi / expi-inspi / next inspi-expi indices
print(cycles[:10])

# this will return a dataframe with all cycles and features before cleaning
resp_cycles = physio.compute_respiration_cycle_features(resp, srate, cycles, baseline=baseline, sensor_type = 'airflow') # compute cycle-by-cycle resp features on airflow sensor type

# this will remove outliers cycles based on log ratio distribution
resp_cycles = physio.clean_respiration_cycles(resp, srate, resp_cycles, baseline, low_limit_log_ratio=4.5, sensor_type = 'airflow') # clean features
print(resp_cycles.head(10))


inspi_ind = resp_cycles['inspi_index'].values # get index of inspiration start points
expi_ind = resp_cycles['expi_index'].values # get index of expiration start points

fig, ax = plt.subplots()
ax.plot(times, resp, label = 'preprocessed resp signal')
ax.scatter(times[inspi_ind], resp[inspi_ind], marker='o', color='green', label = 'inspiration start')
ax.scatter(times[expi_ind], resp[expi_ind], marker='o', color='red', label = 'expiration start')
ax.axhline(baseline, color='Coral', label = 'baseline', ls = '--', alpha = 0.9)
ax.set_ylabel('Amplitude (AU)')
ax.set_xlabel('Time (s)')
ax.set_title('Respiratory cycle detection (using low-level functions)')
ax.set_ylim(-1750, -1450)
ax.set_xlim(110, 170)
ax.legend(loc = 'upper right')

plt.show()
Respiratory cycle detection (using low-level functions)
baseline : -1635.636336457686
[[  959  3856  8749]
 [ 8749 12461 16623]
 [16623 20084 25584]
 [25584 28837 35465]
 [35465 39256 45628]
 [45628 48997 55907]
 [55907 59779 67319]
 [67319 70804 78017]
 [78017 81433 86026]
 [86026 89212 94617]]
/home/docs/checkouts/readthedocs.org/user_builds/physio/envs/stable/lib/python3.11/site-packages/physio/respiration.py:701: UserWarning: clean_respiration_cycles() need variable_names to be set, variable_name=['inspi_volume', 'expi_volume'] is set to  for backward compatibility
  warnings.warn("clean_respiration_cycles() need variable_names to be set, variable_name=['inspi_volume', 'expi_volume'] is set to  for backward compatibility")
   inspi_index  expi_index  ...  expi_peak_time  total_volume
0          959        3856  ...           4.968    291.387502
1         8749       12461  ...          13.477    248.663272
2        16623       20084  ...          20.993    225.308920
3        25584       28837  ...          30.575    180.185173
4        35465       39256  ...          41.138    218.718159
5        45628       48997  ...          50.699    185.262616
6        55907       59779  ...          61.812    222.891807
7        67319       70804  ...          72.757    173.670636
8        78017       81433  ...          82.837    241.081997
9        86026       89212  ...          90.998    162.158526

[10 rows x 21 columns]

Respiration features / metrics

resp_cycles is a dataframe containing one row per respiratory cycle and one column per feature. Depending on the sensor type, each cycle is described by multiple features.

Some features are related to the position (index) or time of particular points within the cycle:

  • inspi_index: Index of the start of inspiration (green point in the figure below). The index refers to the sequential position of the sample in the entire respiratory time series.

  • expi_index: Index of the start of expiration (red point in the figure below).

  • next_inspi_index: Index of the start of the next inspiration (equal to the inspi_index of cycle n+1).

  • inspi_time: (units = s) Time of the start of inspiration (green point in the figure below).

  • expi_time: (units = s) Time of the start of expiration (red point in the figure below).

  • next_inspi_time: (units = s) Time of the start of the next inspiration.

  • inspi_peak_index: Index of the inspiratory peak = position of the minimum sample of the cycle in the respiratory time series (computed only if sensor_type = airflow).

  • expi_peak_index: Index of the expiratory peak = position of the maximum sample of the cycle in the respiratory time series (computed only if sensor_type = airflow).

  • inspi_peak_time: (units = s) Time of the inspiratory peak = timestamp of the minimum sample of the cycle (computed only if sensor_type = airflow).

  • expi_peak_time: (units = s) Time of the expiratory peak = timestamp of the maximum sample of the cycle (computed only if sensor_type = airflow).

From these points, many derived features of interest (e.g., for statistics) are computed:

  • cycle_duration: (units = s) Duration of the cycle in seconds = next_inspi_time - inspi_time

  • inspi_duration: (units = s) Duration of inspiration in seconds = expi_time - inspi_time

  • expi_duration: (units = s) Duration of expiration in seconds = next_inspi_time - expi_time

  • cycle_freq: (units = Hz) Breathing frequency in Hertz = 1 / cycle_duration

  • cycle_ratio: (units = AU because proportion) Ratio of inspiration duration to total cycle duration = inspi_duration / cycle_duration. Equivalent to the relative position of the transition from inspiration to expiration.

  • inspi_amplitude: (units = AU, the units of the recorded respiratory signal) Amplitude difference of the respiratory signal from baseline at inspi_peak_index (computed only if sensor_type = airflow or belt). Equivalent to peak inspiratory flow (black descending arrow in the figure below).

  • expi_amplitude: (units = AU, the units of the recorded respiratory signal) Amplitude difference of the respiratory signal from baseline at expi_peak_index (computed only if sensor_type = airflow’ or belt). Equivalent to peak expiratory flow (black ascending arrow in the figure below).

  • total_amplitude: (units = AU, the units of the recorded respiratory signal) Sum of inspi_amplitude + expi_amplitude (computed only if sensor_type = airflow or belt).

  • inspi_volume: (units = AU * s, depending on units the recorded respiratory signal) Integral of the respiratory signal below baseline during inspiration (computed only if sensor_type = airflow). Equivalent to the green area in the figure below.

  • expi_volume: (units = AU * s, depending on units the recorded respiratory signal) Integral of the respiratory signal above baseline during expiration (computed only if sensor_type = airflow). Equivalent to the red area in the figure below.

  • total_volume: (units = AU * s, depending on units the recorded respiratory signal) Sum of inspi_volume + expi_volume (computed only if sensor_type = airflow). Equivalent to the sum of the green + red areas in the figure below.

Respiration Parameters
pprint(f'{resp_cycles.shape[0]} detected cycles')
pprint(f'{resp_cycles.shape[1]} computed features, which are the following :')
pprint(resp_cycles.columns.to_list())
'30 detected cycles'
'21 computed features, which are the following :'
['inspi_index',
 'expi_index',
 'next_inspi_index',
 'inspi_time',
 'expi_time',
 'next_inspi_time',
 'cycle_duration',
 'inspi_duration',
 'expi_duration',
 'cycle_freq',
 'cycle_ratio',
 'inspi_volume',
 'expi_volume',
 'total_amplitude',
 'inspi_amplitude',
 'expi_amplitude',
 'inspi_peak_index',
 'expi_peak_index',
 'inspi_peak_time',
 'expi_peak_time',
 'total_volume']

Quick example with respiration recorded using a belt

For this tutorial, we will use an internal file already stored in NumPy format for demonstration purposes. This file corresponds to 5 minutes of signal recorded with a belt on a human subject. The belt is a sensor of trunk circumference: the signal rises when the trunk dilates and decreases when the trunk retracts.

Therefore, the detection method does not rely on a “baseline-crossing” approach, but rather on a “min-max” approach. This min_max method is activated via the parameter_preset dedicated to this situation.

Note that respiratory signals recorded with belts are often of poor quality. In addition, some participants may present paradoxical respiration, with the trunk retracting during inspiration and dilating during expiration. In such cases, the metrics returned in resp_cycles_belt will not be meaningful.

Further note that in case of a belt, volumes can’t be computed and amplitudes are no the same ones than in airflow. In this case this a max - min measure related to a circumference, while in case of an airflow this is a vertical distance to the baseline… meaning an instantaneous flow.

Let’s run a short example.

raw_resp_belt = np.load('resp3_belt.npy') # load respi belt
srate = 1000. # our example signals have been recorded at 1000 Hz

times = np.arange(raw_resp_belt.size) / srate # build time vector


# the easiest way is to use predefined parameters
resp_belt, resp_cycles_belt = physio.compute_respiration(raw_resp_belt, srate, parameter_preset='human_belt')  # set 'human_belt' as preset because example resp is an airflow from humans

# print human_belt params
pprint(physio.get_respiration_parameters('human_belt'))

# resp_cycles_belt is a dataframe containing all cycles and related features (duration, amplitude, timing, etc...). In the case of belt, volumes are not computed.
print(resp_cycles_belt)

inspi_ind = resp_cycles_belt['inspi_index'].values # get index of inspiration start points
expi_ind = resp_cycles_belt['expi_index'].values # get index of expiration start points

fig, ax = plt.subplots()
ax.plot(times, raw_resp_belt)
ax.plot(times, resp_belt)
ax.scatter(times[inspi_ind], resp_belt[inspi_ind], color='green', label = 'inspiration start')
ax.scatter(times[expi_ind], resp_belt[expi_ind], color='red', label = 'expiration start')
ax.set_xlim(195, 215)
ax.set_xlabel('Time (s)')
ax.set_ylabel('Amplitude (AU)')
ax.set_title('Respiratory cycle detection on a belt signal (min-max signal)')
ax.legend(loc = 'upper right')
Respiratory cycle detection on a belt signal (min-max signal)
{'baseline': None,
 'cycle_clean': {'low_limit_log_ratio': 8.0,
                 'variable_names': ['inspi_amplitude', 'expi_amplitude']},
 'cycle_detection': {'exclude_sweep_ms': 200.0, 'method': 'min_max'},
 'preprocess': {'band': 5.0, 'btype': 'lowpass', 'ftype': 'bessel', 'order': 5},
 'sensor_type': 'belt',
 'smooth': {'sigma_ms': 40.0, 'win_shape': 'gaussian'}}
    inspi_index  expi_index  ...  inspi_amplitude  expi_amplitude
0          3875        6168  ...       497.761465      747.099189
1          9499       11176  ...       453.815590      316.429352
2         13317       15405  ...       390.832924      432.101417
3         18137       19825  ...       389.225680      436.079255
4         22469       24252  ...       479.711632      414.107313
..          ...         ...  ...              ...             ...
82       283479      284539  ...       256.353191      236.941509
83       286221      287424  ...       250.333176      273.552655
84       289046      290081  ...       287.955141      288.775000
85       291656      292918  ...       284.711136      271.157089
86       294552      295610  ...       271.450497      285.710228

[87 rows x 13 columns]

<matplotlib.legend.Legend object at 0x739061bd40d0>

Quick example with respiration recorded using a CO2 sensor

For this tutorial, we will use an internal file already stored in NumPy format for demonstration purposes. This file corresponds to 5 minutes of signal recorded with a CO2 sensor at 60 Hz from a human subject.

In the upper airways, CO2 concentration decreases during inspiration and increases during exhalation, and so does the recorded signal.

Therefore, the detection method does not rely on a “baseline-crossing” approach, but rather on a dedicated “co2” method. This method is activated via the parameter_preset specific to this situation.

Note that for co2, amplitude and volumes are not computed.

Let’s run a short example.

raw_resp_co2 = np.load('resp4_CO2.npy') # load respi co2
srate = 60. # our example signals have been recorded at 60 Hz

times = np.arange(raw_resp_co2.size) / srate # build time vector


# the easiest way is to use predefined parameters
resp_co2, resp_cycles_co2 = physio.compute_respiration(raw_resp_co2, srate, parameter_preset='human_co2')  # set 'human_co2' as preset because example resp is an airflow from humans

# print human_co2 params
pprint(physio.get_respiration_parameters('human_co2'))

# resp_cycles_co2 is a dataframe containing all cycles and related features (duration, timing, etc...). In the case of belt, volumes and amplitudes are not computed.
print(resp_cycles_co2)

inspi_ind = resp_cycles_co2['inspi_index'].values # get index of inspiration start points
expi_ind = resp_cycles_co2['expi_index'].values # get index of expiration start points

fig, ax = plt.subplots()
ax.plot(times, raw_resp_co2)
ax.plot(times, resp_co2)
ax.scatter(times[inspi_ind], resp_co2[inspi_ind], color='green', label = 'inspiration start')
ax.scatter(times[expi_ind], resp_co2[expi_ind], color='red', label = 'expiration start')
ax.set_xlim(95, 110)
ax.set_ylim(-1, 40)
ax.set_xlabel('Time (s)')
ax.set_ylabel('Amplitude (mmHg)')
ax.set_title('Respiratory cycle detection on a CO2 signal')
ax.legend(loc = 'upper right')
Respiratory cycle detection on a CO2 signal
{'baseline': None,
 'cycle_clean': None,
 'cycle_detection': {'clean_by_mid_value': True,
                     'method': 'co2',
                     'thresh_expi_factor': 0.05,
                     'thresh_inspi_factor': 0.08},
 'preprocess': {'band': 10.0,
                'btype': 'lowpass',
                'ftype': 'bessel',
                'order': 5},
 'sensor_type': 'co2',
 'smooth': {'sigma_ms': 40.0, 'win_shape': 'gaussian'}}
    inspi_index  expi_index  ...  cycle_freq  cycle_ratio
0           131         198  ...    0.255319     0.285106
1           366         396  ...    0.256410     0.128205
2           600         667  ...    0.256410     0.286325
3           834         865  ...    0.255319     0.131915
4          1069        1136  ...    0.255319     0.285106
..          ...         ...  ...         ...          ...
76        17755       17821  ...    0.256410     0.282051
77        17989       18055  ...    0.300000     0.330000
78        18189       18258  ...    0.256410     0.294872
79        18423       18491  ...    0.254237     0.288136
80        18659       18688  ...    0.740741     0.358025

[81 rows x 11 columns]

<matplotlib.legend.Legend object at 0x739061a82990>

Total running time of the script: (0 minutes 0.677 seconds)

Gallery generated by Sphinx-Gallery