Use Case 2: Data from multiple sources in an animal experiment

This notebook illustrates the usage of the vitabel module to visualize, annotate and process time-series data from the medical field. Please find the detailed, searchable documentation here: Documentation Status

In this case we analyze data collected in an animal laboratory experiment of cardiopulmonary resuscitation. This notebook in particular demonstrates the capabilities of vitabel to align time-series data recorded by multiple devices with unsynchronized clocks.

If you have never worked with Jupyter Notebooks before, you may find this guide helpful: Beginners Guide to Jupyter Notebooks

from vitabel import Vitals, IntervalLabel

from pathlib import Path
import pandas as pd

1. Loading Data

We begin by specifying the file paths for the multiple data sources. In this use case, three different types of files are read: data from the mechanical CPR device is stored in an XML export from a proprietary format. Invasive blood pressure was recorded from a patient monitor via VitalRecorder. Airflow, airway pressure were recorded each by a separate single board computer and stored in CSV files.

lucas_file = Path("data/Lucas_file_Lucas.xml")  # exported via CodeStat
vital_recorder_file = Path("data/vital_file.vit")  # recorded via VitalRecorder (https://vitaldb.net/vital-recorder/)

flow_file = Path("data/flow.csv.bz2")
airway_pressure_file = Path("data/p1.csv.bz2")
capnography_file = Path("data/capno.csv.bz2")

A new instance of the Vitals class is initialized and all data is loaded from the files. As ventilatory parameters were recorded in UNIX time, time_start and time_unit must be set accordingly.

case = Vitals()
case.add_defibrillator_recording(lucas_file)
case.add_vital_db_recording(
    vital_recorder_file, 
    metadata={"source": "GE Healthcare monitor"},  # providing metadata to the channels
) 

case.add_data_from_csv(
    flow_file,
    time_start=pd.Timestamp(1970, 1, 1, 0, 0, 0),
    time_unit="ms",
    metadata={"source": "volucapno"},
    index_col="timestamp",
)
case.add_data_from_csv(
    capnography_file,
    time_start=pd.Timestamp(1970, 1, 1, 0, 0, 0),
    time_unit="ms",
    metadata={"source": "volucapno"},
    index_col="Timestamp",
)
case.add_data_from_csv(
    airway_pressure_file,
    time_start=pd.Timestamp(1970, 1, 1, 0, 0, 0),
    time_unit="ms",
    metadata={"source": "volucapno"},
    index_col=0,
    names=["airway_pressure", "temperature_1"],
)

2. Processing Data

Several channels are assigned to variables and renamed to our discretion.

capno_channel = case.get_channel("CO2 Concentration")
capno_channel.rename("capnography")

case.get_channel("PLETH").rename("ppg")  # just another way to achieve the same

We get an overview over all channels by calling the get_channel_infos routine

case.get_channel_infos()

By calling get_channel_infos with keyword arguments (kwargs) we can filter the list of channels, for example by the source set in their metadata. The argument kwargs can be used the same way with other functions to retrieve channels and labels or details on them.

case.get_channel_infos(metadata={"source": "volucapno"})

In the following we are calling specific channels and assign them to variables, such that we can work on them later on.

ibp_channel = case.get_channel(name="IBP1")
cc_channel = case.get_channel(name="cc")
flow_channel = case.get_channel("airflow")
pressure_channel = case.get_channel("airway_pressure")

We generated a new IntervalLabel to later annotate noisy segements of the invasive blood pressure recordings, for example due to sampling of blood gas samples.

aline_noise = IntervalLabel("IBP noise", annotation_preset_type="timestamp")
aline_noise.attach_to(ibp_channel)

Calling the print_data_summary method gives us information about channels and labels and their attachment. Shifting the time index of a channel will also shift all labels attached to it accordingly.

case.print_data_summary()

3. Plotting Data interactively

The plotstyle of the channels is adapted by the set_channel_plotstyle method. All keyword arguments of matplotlib’s axes.plot can be set in the plotstyle dictionary.

case.set_channel_plotstyle(capno_channel, color="goldenrod", lw=1, alpha=0.8)
case.set_channel_plotstyle("airflow", color="blue", lw=1, alpha=0.8)  # another way to call the channel by its name
case.get_channel("airway_pressure").plotstyle = {"color": "green", "lw": 1, "alpha": 0.8}  # the plotstyle can also be set by direct assignment
case.set_channel_plotstyle(ibp_channel, color="red", lw=1, alpha=0.8)
case.set_channel_plotstyle(cc_channel, color="purple", marker="o", alpha=0.8, linestyle="", label="chest compressions")  # be aware that we define the _label_ of the plot legend here
case.set_label_plotstyle(aline_noise, color="dimgray", lw=3, alpha=0.8)

An interactive plot is initialized. The menu an explanation to align the channels is given in the tab Align Timelines. We can first align the chest compressions to the IBP and than select both and align them with the first artefact of a chest compression in the airway pressure. Afterward, we can label noisy segments in the blood-pressure signal the the menu offered in the Annotate-Tab. Be aware that we have added an IntervaLabel which has to be defined by a start and an end, thus clicking twice.

plot = case.plot_interactive(
    channels=[
        [flow_channel, "cc"],  # flow_channel is added directly, while the cc channel is added by its name
        [4],  # the capnography channel is added via its channel index
        [pressure_channel],
        [ibp_channel],
    ],
    labels=[[], [], [], ["IBP noise"]],
    channel_overviews=[[ibp_channel]],
    time_unit="s",
    subplots_kwargs={"figsize": (16.5, 8)},
)

plot

4. Storing Data

As we have seen already, the Vitals.info method prints a compact overview of all channels and labels contained in a given collection.

case.info()

As soon as we are done with our adjustments and annotations, the Vitals object can be serialized and saved to a JSON file.

case.save_data("case_2.json")

Our Vitals objects that have been serialized and stored can be loaded again using the Vitals.load_data method.

new_case = Vitals()
new_case.load_data("case_2.json")