Visualise COVID-19 case data using Python, Dash, and Plotly
By: Meinhard Ploner
Tags: Coronavirus, Covit 19, Python, Dash, Plotly

Set up Python & Visual Studio Code

First, download the latest stable release of Python from https://www.python.org/downloads/, start the installer and go through.

For the selection of the code editor, there are plenty of possibilities. Visual Studio Code is a valid option. To install it, download the installer from https://code.visualstudio.com/download, run it, and complete the installation.

Set up the Python Project and its folder structure

There exist multiple solutions for package management in Python: pip, pipenv, (ana)conda, and virtual environments. For a quick explanation see e.g. this Medium post. We use here virtual environments.

Still on the command line, move to the directory where the app should be located. Then create a new app directory and a virtual environment:

cd <folder-with-my-apps> # Put here the folder of your choice.

mkdir corona-app # Create a dedicated app directory.

cd corona-app

virtualenv venv # Create the virtual environment 'venv'.

Next, activate the virtual environment. On macOS and Linux, do:

source venv/bin/activate

On Windows, instead, type:

venv\Scripts\activate

To install the packages into the new environment, type:

pip install dash # Installs <plotly>, too.

pip install pandas

Now open Visual Studio Code. On the top right of the Welcome screen (Customize -> Tools and languages) you can select Python to install the language support for Python:

Still on the Welcome screen, click on the left side on “Open folder…” and select the folder you created previously (corona-app):

For more details on setting up and customizing VS Code have a look at this dedicated tutorial. The next step is to create the file app.py, which will contain all the app code we need. Click on the plus button on the file explorer and enter the file name.

Now we can start coding!

Implement the logic to load and preprocess the data

In Visual Studio Code, click on the file name “app.py” if the file is not already open. Type the following lines to load the libraries we need:

import dash

import dash_core_components as dcc

import dash_html_components as html

import plotly.graph_objects as go

from dash.dependencies import Input, Output

import pandas as pd

Next, define a baseURL from which we download the case data of the Coronavirus by country and region. The data is located on a server of the Johns Hopkins Center for Systems Science and Engineering (JHU/CSSE). The function loadData handles the download and some basic transformations, as explained below.

baseURL = "https://raw.githubusercontent.com/CSSEGISandData/COVID-19/master/csse_covid_19_data/csse_covid_19_time_series/" def loadData(fileName, columnName):

data = pd.read_csv(baseURL + fileName) \

.drop(['Lat', 'Long'], axis=1) \

.melt(id_vars=['Province/State', 'Country/Region'],

var_name='date', value_name=columnName) \

.astype({'date':'datetime64[ns]', columnName:'Int64'},

errors='ignore')

data['Province/State'].fillna('<all>', inplace=True)

data[columnName].fillna(0, inplace=True)

return data

read_csv downloads the file from the web and converts it into a Pandas dataframe. Geo-coordinates are omitted by drop; melt converts the dataframe from wide to long format. Then, astype ensures the types are correct. Finally, the method fillna inserts the text <all> if no region is available or 0 for the column containing the cumulated case number.

The last transformation is a conversion into date type for the corresponding column.

By calling the function loadData multiple times we load a cleaned dataframe of the Confirmed Cases, Recovered Cases, and Deaths. The function merge joins these 3 data sets to get one data set including all the data:

allData =

loadData(

"time_series_covid19_confirmed_global.csv", "CumConfirmed") \

.merge(loadData(

"time_series_covid19_deaths_global.csv", "CumDeaths")) \

.merge(loadData(

"time_series_covid19_recovered_global.csv", "CumRecovered"))

For the initialization of the user interface, we need the country names. We extract them from the big dataframe using:

countries = allData['Country/Region'].unique()

countries.sort()

Now we are done with the data load. Let’s move to the UI part!

[Update 2020-Mar-27]: the file names changed Mar-23 on the server, so I had to update them here, too.

Create the User Interface using Dash & Plotly

First, we initialize a path to an external stylesheet, and a custom font (matching the R-Shiny version of this app):

external_stylesheets=['https://codepen.io/chriddyp/pen/bWLwgP.css'] tickFont = {'size':12, 'color':"rgb(30,30,30)", \

'family':"Courier New, monospace"}

The Dash app will be initialized with the stylesheet defined above using:

app = dash.Dash(__name__, external_stylesheets=external_stylesheets) app.layout = html.Div(

style={ 'font-family':"Courier New, monospace" }, children=[

## ...

]

])

While the title and the two plots spread over the whole screen width, the three controls occupy only one third. In terms of the bootstrap grid system, this translates to three columns of a width of 4 each, which sums up to 12.

Instead of the three dots (see last code block) we enter now the following code block to define the columns containing the controls:

html.H1('Case History of the Coronavirus (COVID-19)'),

html.Div(className="row", children=[

html.Div(className="four columns", children=

html.H5('Country'),

dcc.Dropdown(

id='country',

options=[{'label':c, 'value':c} \

for c in countries],

value='Italy'

)

]),

html.Div(className="four columns", children=[

html.H5('State / Province'),

dcc.Dropdown(

id='state'

)

]),

html.Div(className="four columns", children=[

html.H5('Selected Metrics'),

dcc.Checklist(

id='metrics',

options=[{'label':m, 'value':m} for m in \

['Confirmed', 'Deaths', 'Recovered']],

value=['Confirmed', 'Deaths']

)

])

]),

The controls we defined are:

Country Dropdown, filled with unique countries.

State/Province Dropdown. Not initialized so far.

Metrics Checklist, filled with the 3 metric names.

For the two bar charts we use the interactive version of Plotly (documentation), and change the config to hide the overwhelming amount of controls. The code is again part of the children array:

dcc.Graph(

id="plot_new_metrics",

config={ 'displayModeBar': False }

),

dcc.Graph(

id="plot_cum_metrics",

config={ 'displayModeBar': False }

)

Add the server logic

In contrast to the R-Shiny software stack, Python-Dash has no simple solution to make expressions (=data!) reactive. The next function returns all the data for a specified country and state. The function will be used twice in our code (one per plot) so it would have been an advantage if the calculations would not happen twice:

def nonreactive_data(country, state):

data = allData.loc[allData['Country/Region'] == country] \

.drop('Country/Region', axis=1)

if state == '<all>':

data = data.drop('Province/State', axis=1) \

.groupby("date") \

.sum() \

.reset_index()

else:

data = data.loc[data['Province/State'] == state]

newCases = data.select_dtypes(include='Int64').diff().fillna(0)

newCases.columns = [column.replace('Cum', 'New')

for column in newCases.columns]

data = data.join(newCases)

data['dateStr'] = data['date'].dt.strftime('%b %d, %Y')

return data

Besides filtering the overall dataframe for country and state, the function also aggregates the data (using groupby and sum) if the user chooses <all> for a country having states (e.g. US). Further differences are calculated from cumulated statistics and saved into new columns, prefixed with “New” instead of “Cum”.

The next block defines a function to create the barchart, which will be used for both the plots.

colors = { 'Deaths':'rgb(200,30,30)', \

'Recovered':'rgb(30,200,30)', \

'Confirmed':'rgb(100,140,240)' } def barchart(data, metrics, prefix="", yaxisTitle=""):

figure = go.Figure(data=[

go.Bar(

name=metric, x=data.date, y=data[prefix + metric],

marker_line_color='rgb(0,0,0)', marker_line_width=1,

marker_color=colors[metric]

) for metric in metrics

])

figure.update_layout(

barmode='group', legend=dict(x=.05, y=0.95),

plot_bgcolor='#FFFFFF', font=tickFont) \

.update_xaxes(

title="", tickangle=-90, type='category',

showgrid=True, gridcolor='#DDDDDD',

tickfont=tickFont, ticktext=data.dateStr,

tickvals=data.date) \

.update_yaxes(

title=yaxisTitle, showgrid=True, gridcolor='#DDDDDD')

return figure

The top part of the function loops through the metrics to add the bars. The bottom part sets the barchart mode (“group”) and does some tweaking and styling on legend, axes, and labels. Most importantly, we wanna show the date on every single tick of the x-axis.

Reactive Plots

While reactive expressions are not supported in Dash so far, reactive UI elements are. The following callbacks are fired if one of the Input values changes (country, state, or metrics). In this case, the subsequent function is called and updates the variables defined in Output (figure).

To accomplish the task, the function loads the cleaned data and renders the chart by calling the appropriate function “barchart”.



Output('plot_new_metrics', 'figure'),

[Input('country', 'value'), Input('state', 'value'),

Input('metrics', 'value')]

)

def update_plot_new_metrics(country, state, metrics):

data = nonreactive_data(country, state)

return barchart(data, metrics, prefix="New",

yaxisTitle="New Cases per Day") @app .callback(Output('plot_new_metrics', 'figure'),[Input('country', 'value'), Input('state', 'value'),Input('metrics', 'value')]def update_plot_new_metrics(country, state, metrics):data = nonreactive_data(country, state)return barchart(data, metrics, prefix="New",yaxisTitle="New Cases per Day")

Output('plot_cum_metrics', 'figure'),

[Input('country', 'value'), Input('state', 'value'),

Input('metrics', 'value')]

)

def update_plot_cum_metrics(country, state, metrics):

data = nonreactive_data(country, state)

return barchart(data, metrics, prefix="Cum",

yaxisTitle="Cumulated Cases") @app .callback(Output('plot_cum_metrics', 'figure'),[Input('country', 'value'), Input('state', 'value'),Input('metrics', 'value')]def update_plot_cum_metrics(country, state, metrics):data = nonreactive_data(country, state)return barchart(data, metrics, prefix="Cum",yaxisTitle="Cumulated Cases")

Reactive Dropdown

Last but not least, the States/Provinces shown in the Dropdown menu have to adapt when the user changes the country of choice. update_states extracts the list of unique states, adds <all>, and returns the whole list and updates the selected value.



[Output('state', 'options'), Output('state', 'value')],

[Input('country', 'value')]

)

def update_states(country):

states = list(allData.loc[allData['Country/Region'] == country]

['Province/State'].unique()

)

states.insert(0, '<all>')

states.sort()

state_options = [{'label':s, 'value':s} for s in states]

state_value = state_options[0]['value']

return state_options, state_value @app .callback([Output('state', 'options'), Output('state', 'value')],[Input('country', 'value')]def update_states(country):states = list(allData.loc[allData['Country/Region'] == country]['Province/State'].unique()states.insert(0, ' ')states.sort()state_options = [{'label':s, 'value':s} for s in states]state_value = state_options[0]['value']return state_options, state_value

Done!

Now you can run the app by simply pushing the green “Run” button on the top-right of VS Code. Have fun :)

GitHub code

All the code above can be found on GitHub: https://github.com/ploner/coronavirus-py

[optional] Upload the application to the cloud

Publishing the app on a web server is quite easy, especially with free services like Heroku.

The steps are:

Add the following line to the app.py file:

server = app.server