Visualising vital signs in dash

I vividly remember the graphs of the vital signs of patients during my working period as an MD in the IC department. Graphical representations of vital signs can help in getting a better insight into what is happening behind the curtains. The graphs usually gave far more insight into the health state of a patient than the snapshots of vital signs in other work environments.

 

Dash is a python module that is getting an increasing amount of attention. With <100 lines of code you can make remarkable beautiful and interactive charts. Dash creates a local server on your computer and the application you build can be accessed via your browser. By doing so you have a graphical user interface (GUI) that you can interact with within your browser. This saves you time in writing a whole user interface to control the functionality of a script or software package. Dash also has a steep learning curve, understanding how to get things done doesn’t take much time. Dash seems to be the perfect python package for data visualisations.

Let’s see if we can create a graphical user interface of the vital signs of a dummy patient. The workflow to achieve this is a three step process. You get the data, clean the data, and then show the bling bling. The last one is the easiest thanks to Dash, the middle one the hardest.

We get the data from our dummy patient with a selenium module for web automation and create a dataframe with pandas:

# import scraping modules
from selenium import webdriver
import pandas as pd

# open browser
driver = webdriver.Chrome()

# scrape vital signs
driver.get("https://medicalprogress.dev/patient_file2/vit_signs.html")
html = driver.page_source
driver.close()
data = pd.read_html(html)
data = data[0]
data = pd.DataFrame(data)
print(data)

The result should look something like this:

         Date           BP  Pulse  Saturation  Temperature  Breathing frequency
0 1-2-2020 150/100 90 95 37.0 15
1 2-2-2020 145/95 92 95 372.0 16
2 3-2-2020 140/96 90 96 37.1 12
3 4-2-2020 142/94 r.a. 80 93 37.6 12
4 5-2-2020 138/90 90 20 36.2 95
5 6-2-2020 - / - 87 94 7.4 15
6 7-2-2020 143/87 94 96 37.2 16
7 8-2-2020 120/90 80 96 37.1 17
8 9-2-2020 121/80 75 96 37.0 15
9 10-2-2020 123/90 70 99 36.4 14
10 11-2-2020 130/80 73 96 36.5 15
11 12-2-2020 129/89 72 94 36.7 15
12 13-2-2020 125/80 73 96 37.0 14
13 14-2-2020 119/87 70 96 37.4 11

Process finished with exit code 0

cleaning

We see a couple of things here:

  • There are N.A. values in the blood pressure measurements that we cant use in our graph (e.g. — / -).
  • There is text within the blood pressure measurements (e.g. r.a. = right arm).
  • There are deviations from normal (saturation of 20 and breathing frequency of 95 where switched in the 5th row (index 4)). `
  • There are typos: 372.0 should have been 37.2, the problem was that there was a comma used instead of a dot. Furthermore, 7.4 in row 6 (index 5) is probably representative of 37.4.

We solve (1) and (2) with the following line of code that uses regex. For more info about regex see the tutorial of Corey Schafer:

# replace empty bloodpressure values (- / -) with nothing
data = data.replace(to_replace='- / -', value='')
# replace all comments behind numbers with nothing
data[data.columns[1]] = data[data.columns[1]].str.replace(r'[a-zA-Z].*', '', regex=True)

 

 The only thing left to do for the blood pressure column is making two separate columns, one for diastolic and one for systolic blood pressures so we can process them in our graph:

# make two new columns for sys and dias BP
data['Systolic BP'] = ''
data['Diastolic BP'] = ''
# transport BP into two new columns separated on systolic and diastolic
data['Systolic BP'] = data[data.columns[1]].str.replace(r'[/].*', '', regex=True)
data['Diastolic BP'] = data[data.columns[1]].str.replace(r'.*[/]', '', regex=True)
# drop the blood pressure column
data.drop(data.columns[1], axis=1, inplace=True)

The table will look something like this if you print it. You can see that there are two new columns at the end that show only numerical values:

         Date  Pulse  Saturation  ...  Breathing frequency  Systolic BP Diastolic BP
0 1-2-2020 90 95 ... 15 150 100
1 2-2-2020 92 95 ... 16 145 95
2 3-2-2020 90 96 ... 12 140 96
3 4-2-2020 80 93 ... 12 142 94
4 5-2-2020 90 20 ... 95 138 90
5 6-2-2020 87 94 ... 15
6 7-2-2020 94 96 ... 16 143 87
7 8-2-2020 80 96 ... 17 120 90
8 9-2-2020 75 96 ... 15 121 80
9 10-2-2020 70 99 ... 14 123 90
10 11-2-2020 73 96 ... 15 130 80
11 12-2-2020 72 94 ... 15 129 89
12 13-2-2020 73 96 ... 14 125 80
13 14-2-2020 70 96 ... 11 119 87

[14 rows x 7 columns]

Point 3 en 4, the removal of outliers, can be achieved with masks and using some reasonable values as filters:

# mask all temp > 43 and < 35
data["Temperature"] = data["Temperature"].mask(data["Temperature"] > float(40))
data["Temperature"] = data["Temperature"].mask(data["Temperature"] < float(35))

# mask all AF > 50 and < 3
data["Breathing frequency"] = data["Breathing frequency"].mask(data["Breathing frequency"] > float(50))
data["Breathing frequency"] = data["Breathing frequency"].mask(data["Breathing frequency"] < float(5))

# mask Saturatie (%) > 100 and <60
data["Saturation"] = data["Saturation"].mask(data["Saturation"] > float(100))
data["Saturation"] = data["Saturation"].mask(data["Saturation"] < float(60))

visualisation

We create a graph object with the module go of the visualisation package plotly:


import plotly.graph_objs as go

fig_graph_vit_par = go.Figure()
fig_graph_vit_par.add_trace(go.Scatter(x=data["Date"], y=data["Saturation"],
mode='lines+markers',
name='Saturation (%)',
line=dict(color='deepskyblue', width=2),
connectgaps=True, ))
fig_graph_vit_par.add_trace(go.Scatter(x=data["Date"], y=data["Pulse"],
mode='lines+markers',
name='Pulse (bpm)',
line=dict(color='red', width=2),
connectgaps=True, ))
fig_graph_vit_par.add_trace(go.Scatter(x=data["Date"], y=data["Diastolic BP"],
mode='lines+markers',
name='Diastolic BP (mmHg)',
line=dict(color='darkgreen', width=2),
connectgaps=True, ))
fig_graph_vit_par.add_trace(go.Scatter(x=data["Date"], y=data["Systolic BP"],
mode='lines+markers',
name='Systolic BP (mmHg)',
line=dict(color='forestgreen', width=2),
connectgaps=True, ))
fig_graph_vit_par.add_trace(go.Scatter(x=data["Date"], y=data["Breathing frequency"],
mode='lines+markers',
name='AF (/min)',
line=dict(color='steelblue', width=2),
connectgaps=True, ))
fig_graph_vit_par.add_trace(go.Scatter(x=data["Date"], y=data["Temperature"],
mode='lines+markers',
name='Temp. (ᵒC)',
line=dict(color='darkorange', width=2),
connectgaps=True, ))

To display our vital signs in a figure in Dash we need to install some packages:

  • pip install dash
  • pip install dash-core-components
  • pip install dash-html-components

We make a graph by using the dash core component “Graph” to plot the figure. To do this we provide the graph object from the previous section as a variable for the figure:

 

import dash
import dash_core_components as dcc
import dash_html_components as html

external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css']

app = dash.Dash(__name__, external_stylesheets=external_stylesheets)

server = app.server

app.layout = html.Div([
html.Center(
html.H1('VITAL SIGNS'),
),
html.Div([
dcc.Graph(
id="vit signs",
figure=fig_graph_vit_par)
]),
])

if __name__ == '__main__':
app.run_server(debug=True)

The result is the graph beneath. Now, let’s imagine the patient received metoprolol (bloodpressure medication) on the 7th of February, we can now see in the graph that there was a decrease in systolic blood pressure on the 8th of February and also a decrease in pulse from around 90 to 70 bpm so it then looks like the intervention worked:

Vital signs in dash

A lot of calls during on-call shifts are concerned about the question of whether to isolate or not isolate a patient based on a specific temperature. If you unselect all vital signs on the right and only leave the temperature in the list you get more insight into the temperatures variation through time. It becomes easy to identify a subfebrile temperature (=37.5–38.4) on 4 February. It seemed to likely be a consequence of the normal circadian rhythm (biological variability) or an error measurement (measurement variability) because in follow-up measurements the temperature was within the normal range. Visualisation like this can make making decisions around covid related consultations easier.

Body temperature Dash

That was a quick and easy data visualisation of vital signs. There is of course a lot more to do before it’s usable in a day-to-day work environment, but I have shown with this tutorial that it can be done with the help of the ‘snake language’ and Dash as a visualisation method. If you want access to the full code, you can find it on my Github.