Visualise COVID-19 case data using Python, Dash, and Plotly | by Meinhard Ploner | Towards Data Science

This article is less about COVID-19, but more about how to create a useful web application using Python & Dash. It is intended as a simple guide to get up & running with the technology, and makes it easy to do it by yourself!
I wrote pretty much the same article about how to perform this task using R & Shiny, and intentionally wanted to compare these two software stacks. Here I explain the Python-Dash way, but I plan on another article doing a dedicated comparison of the solutions.
To get an idea what the final app would look like, test the app:
https://go.aws/2xsdb7q
The following Step-by-Step guide explains how to create this web application including the interactive plots. The software stack is based on Python & Dash, and it is open-source. Altogether only about 130 lines of code.
Btw, a link to the GitHub code repository for download is given at the bottom of the article.
The goal is to create a web application that updates the COVID-19 case data on app launch and shows the data on a dashboard. Features:
Download and parsing of officially available data on the Coronavirus.
Drop-down menus to choose country and state.
Two plots showing new and cumulated case statistics of the selected country. Confirmed cases, recovered cases, and deaths can be selected.
Set up Python & Visual Studio Code.Set up the Python Project and its folder structure.Implement the logic to load and preprocess the data.Create the User Interface using Dash & Plotly.Add the server logic.[optional] Upload the application to the cloud.
Set up Python & Visual Studio Code.
Set up the Python Project and its folder structure.
Implement the logic to load and preprocess the data.
Create the User Interface using Dash & Plotly.
Add the server logic.
[optional] Upload the application to the cloud.
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.
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!
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 dashimport dash_core_components as dccimport dash_html_components as htmlimport plotly.graph_objects as gofrom dash.dependencies import Input, Outputimport 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.
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 }    )
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.
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”.
@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")@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")
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.
@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, '<all>')    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 :)
All the code above can be found on GitHub: https://github.com/ploner/coronavirus-py
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
Create an account on Heroku.
Install the Heroku CLI (command line interface).
Follow the instructions on https://dash.plot.ly/deployment — section “Heroku Example”. Basically, you install gunicorn by pip, create .gitignore, ProcFile and requirements.txt, and commit altogether to Heroku using git.
This is my second post on Medium, and it is highly related to my first one:
Visualize COVID-19 case data in a blink of an eye — using R, Shiny & Plotly
If you are leading a Data Science team and your members are skilled in R and Python as well, comparing Shiny with Dash might be on your To-Do list. Let me know how you make your decision.