avatarMauro Di Pietro

Summary

The provided content is a comprehensive tutorial on building a COVID-19 infection forecaster web application using Python, Dash, and Bootstrap.

Abstract

The article offers a step-by-step guide to creating a web application that forecasts COVID-19 spread within countries using Python, Dash, and Bootstrap. It begins with an introduction to Dash and Bootstrap, emphasizing the convenience of Dash for Python developers and the efficiency of Bootstrap for web development. The tutorial covers the setup of the development environment, the back-end process of data retrieval, processing, and model forecasting, and the front-end development using Dash and Bootstrap components. It also includes instructions on deploying the application to a cloud platform like Heroku. The author demonstrates how to integrate Bootstrap into a Dash application to create a consistent and responsive layout with fewer lines of code. The article concludes with a call to action for readers to connect with the author and explore other web development projects using Python.

Opinions

  • The author expresses a preference for Dash due to its ease of use for Python developers and its potential as a future tool for web development in Python.
  • Bootstrap is highly regarded for its ability to save time and lines of code, as well as for producing aesthetically pleasing web components.
  • The author suggests that integrating Bootstrap with Dash is a significant advantage, making it easier to build complex web applications with consistent styling.
  • The tutorial reflects the author's enthusiasm for the Dash Bootstrap Components library, which simplifies the development process and enhances the visual appeal of Dash applications.
  • The author's choice to use the CSSE COVID-19 dataset from Johns Hopkins University indicates a trust in reputable data sources for model training and forecasting.
  • The article implies that Heroku is a recommended platform for deploying Python web applications, particularly for developers looking to share their projects with a wider audience.
  • The author encourages reader engagement and feedback, indicating a willingness to assist and collaborate on similar projects.

How to embed Bootstrap CSS & JS in your Python Dash app

Build a COVID-19 infection forecaster app with Dash Bootstrap Components

Summary

In this article I will show how to build a web app that forecasts the spread of covid-19 virus within any infected countries using Python, Dash and Bootstrap, that looks like this:

Let me start with this: coding a Dash app is messy… I don’t mean any harm with this, I like Dash and I think it is the future of web development for Python. Dash is a Python (and R) framework for building web applications. It’s built on top of Flask, Plotly.js and React js. It is open source, its apps run on the web browser. Dash is super convenient if you are better in Python than Javascript because allows you to build dashboards using pure Python.

I don’t know if you’ve ever seen a dash application code. It’s a mess: the code comes out really long as you need to write every html Div with contents and properties, just like an html page before that Bootstrap was invented. Bootstrap is an open source toolkit for developing with HTML, CSS, and JS. It is the most used library for web development thanks to its extensive prebuilt components and powerful plugins built on jQuery.

I love Boostrap, not only because the output is always pretty good looking, but especially because it saves you lines and lines of HTML, CSS and JS code. What if I tell you that it is possible also for Dash applications? You like the sound of that, don’t you?

Through this tutorial I will explain step by step how Bootstrap can be easily integrated in Dash and how to build and deploy a web application, using my Covid-19 infection forecaster app as an example (link below, it might take 30 seconds to load).

I will present some useful Python code that can be easily used in other similar cases (just copy, paste, run) and walk through every line of code with comments, so that you can easily replicate this example (link to the full code below).

I’ll use the most popular dataset in these days of quarantine: CSSE COVID-19 dataset. It presents the time series of the number of confirmed cases of contagion reported by each country every day since the pandemic started. This dataset is freely available on the GitHub of the Johns Hopkins University (link below).

In particular, I will go through:

  • Setup of the environment
  • Back-end: Write the model to get, process and plot the data
  • Front-end: Build the app with Dash and Bootstrap
  • Deploy the app

Setup

First of all, I will install the following libraries through the terminal:

pip install dash
pip install dash-bootstrap-components
pip install pandas

The command to install dash will also download useful packages like dash-core-components, dash-html-components and plotly. Similarly, pandas installation includes numpy and scipy that I will use later as well. I assume you already know those, therefore I shall take a moment to introduce Dash Bootstrap Components: basically it’s what does the trick to integrate Bootstrap in Dash and makes easier to build consistently styled apps with complex and responsive layouts. I’m a fan of this library because it saves a huge number of lines of dash code, you’ll see later.

After installing all you need, I would recommend running the following command on the terminal to save the requirements on the appropriate text file:

pip freeze > requirements.txt

In regard to the folder structure, I put 4 fundamental elements on root level:

  • application folder: where all the dash code is going to be, in dash.py file
  • python folder: where I place the logic of the model
  • settings folder: where there are all the configurations
  • run.py file: that runs the whole thing if executed on the terminal with the following command
python run.py

Those mentioned so far are all I need to make the app work, however, there are some other useful but unnecessary things that I added like static images (in application folder), comments (in settings folder), Procfile and requirements.txt used in deployment (on root level).

To summarize, the app shall have the following structure:

Now that it’s all set, I will go through each python file and show the code in it. Let’s get started, shall we?

Back-end: get data, process, plot

Firstly, I will write the class to get Covid-19 infection data, then I will build the model that learns from past observation and forecast the future trend of the time series.

In data.py (inside the python folder) I’ll define the “Data” class with a method that shall be executed when the app starts, meaning that every time the page of the browser where the app runs is loaded, the back-end gets fresh data directly from the source (“get_data” function in the code below). It’s important to save the list of countries because it will be shown to users on the dashboard for selecting a specific country. The Data class has also the task to receive the input from the front-end, the country selected by the user, filter and process data (“process_data” function in the code below). Before filtering for a specific country, I’d create an aggregated time series called “World” which shall be the default selected country when the app starts.

In python terms, the data.py file looks like this:

import pandas as pd
class Data():
    
    def get_data(self):
        self.dtf_cases = pd.read_csv("https://raw.githubusercontent.com/CSSEGISandData/COVID-19/master/csse_covid_19_data/csse_covid_19_time_series/time_series_covid19_confirmed_global.csv", sep=",")
        self.countrylist = ["World"] + self.dtf_cases["Country/Region"].unique().tolist()
    @staticmethod
    def group_by_country(dtf, country):
        dtf = dtf.drop(['Province/State','Lat','Long'], axis=1).groupby("Country/Region").sum().T
        dtf["World"] = dtf.sum(axis=1)
        dtf = dtf[country]
        dtf.index = pd.to_datetime(dtf.index, infer_datetime_format=True)
        ts = pd.DataFrame(index=dtf.index, data=dtf.values, columns=["data"])
        return ts
    
  
    def process_data(self, country):
        self.dtf = self.group_by_country(self.dtf_cases, country)

Now, I’ll build the model to fit data and forecast. The purpose of this article is not to dig in what is the most appropriate model for this dataset, therefore I’ll keep it simple: I am going to use a parametric curve fitting approach, optimizing the parameters of a logistic function for each country time series. If you are interested in this basic modelling approach you can find it explained here.

In model.py (inside the python folder) I’ll define the “Model” class with a method (“forecast” function in the code below) that shall be executed on the World time series when the app starts and each time that a specific country is selected from the front-end. This class has the job to fit the best logistic function on the selected country data (with scipy) and produce a pandas dataframe with:

  • the actual data and the fitted logistic model, which shall be used to plot the total cases
  • the daily change of the actual data and the fitted logistic model (delta t = y t — y t-1), which shall be used to plot the active cases.

To give an illustration, the model.py file contains the following code:

import pandas as pd
import numpy as np
from scipy import optimize
class Model():
    
    def __init__(self, dtf):
        self.dtf = dtf
        
    
    @staticmethod
    def f(X, c, k, m):
        y = c / (1 + np.exp(-k*(X-m)))
        return y
    
  
    @staticmethod
    def fit_parametric(X, y, f, p0):
        model, cov = optimize.curve_fit(f, X, y, maxfev=10000, p0=p0)
        return model
    
    
    @staticmethod
    def forecast_parametric(model, f, X):
        preds = f(X, model[0], model[1], model[2])
        return preds
    
    
    @staticmethod
    def generate_indexdate(start):
        index = pd.date_range(start=start, periods=30, freq="D")
        index = index[1:]
        return index
    
    
    @staticmethod
    def add_diff(dtf):
        ## create delta columns
        dtf["delta_data"] = dtf["data"] - dtf["data"].shift(1)
        dtf["delta_forecast"] = dtf["forecast"] - dtf["forecast"].shift(1)     
        ## fill Nas
        dtf["delta_data"] = dtf["delta_data"].fillna(method='bfill')
        dtf["delta_forecast"] = dtf["delta_forecast"].fillna(method='bfill')   
        ## interpolate outlier
        idx = dtf[pd.isnull(dtf["data"])]["delta_forecast"].index[0]
        posx = dtf.index.tolist().index(idx)
        posx_a = posx - 1
        posx_b = posx + 1
        dtf["delta_forecast"].iloc[posx] = (dtf["delta_forecast"].iloc[posx_a] + dtf["delta_forecast"].iloc[posx_b])/2
        return dtf
    
     def forecast(self):
        ## fit
        y = self.dtf["data"].values
        t = np.arange(len(y))
        model = self.fit_parametric(t, y, self.f, p0=[np.max(y),1,1])
        fitted = self.f(t, model[0], model[1], model[2])
        self.dtf["forecast"] = fitted
        ## forecast
        t_ahead = np.arange(len(y)+1, len(y)+30)
        forecast = self.forecast_parametric(model, self.f, t_ahead)
        ## create dtf
        self.today = self.dtf.index[-1]
        idxdates = self.generate_indexdate(start=self.today)
        preds = pd.DataFrame(data=forecast, index=idxdates, columns=["forecast"])
        self.dtf = self.dtf.append(preds) 
        ## add diff
        self.dtf = self.add_diff(self.dtf)

It’s time to make some cool plots and the best tool for the job is Plotly as Dash is built on top of it. I will put in result.py (inside the python folder) the class that is going to take care of this with

  • the method to plot the total cases time series and its forecast (“plot_total” function in the code below):
  • the method to plot the active cases time series and its forecast (“plot_active” function in the code below):
  • the method to retrieve some statistics to show on the front-end (“get_panel” function in the code below):

Here’s the code full code in result.py:

import pandas as pd
import plotly.graph_objects as go
class Result():
    
    def __init__(self, dtf):
        self.dtf = dtf
        
    
    @staticmethod
    def calculate_peak(dtf):
        data_max = dtf["delta_data"].max()
        forecast_max = dtf["delta_forecast"].max()
        if data_max >= forecast_max:
            peak_day = dtf[dtf["delta_data"]==data_max].index[0]
            return peak_day, data_max
        else:
            peak_day = dtf[dtf["delta_forecast"]==forecast_max].index[0]
            return peak_day, forecast_max
    
    
    @staticmethod
    def calculate_max(dtf):
        total_cases_until_today = dtf["data"].max()
        total_cases_in_30days = dtf["forecast"].max()
        active_cases_today = dtf["delta_data"].max()
        active_cases_in_30days = dtf["delta_forecast"].max()
        return total_cases_until_today, total_cases_in_30days, active_cases_today, active_cases_in_30days
    def plot_total(self, today):
        ## main plots
        fig = go.Figure()
        fig.add_trace(go.Scatter(x=self.dtf.index, y=self.dtf["data"], mode='markers', name='data', line={"color":"black"}))
        fig.add_trace(go.Scatter(x=self.dtf.index, y=self.dtf["forecast"], mode='none', name='forecast', fill='tozeroy'))
        ## add slider
        fig.update_xaxes(rangeslider_visible=True)    
        ## set background color
        fig.update_layout(plot_bgcolor='white', autosize=False, width=1000, height=550)        
        ## add vline
        fig.add_shape({"x0":today, "x1":today, "y0":0, "y1":self.dtf["forecast"].max(), 
                       "type":"line", "line":{"width":2,"dash":"dot"} })
        fig.add_trace(go.Scatter(x=[today], y=[self.dtf["forecast"].max()], text=["today"], mode="text", line={"color":"green"}, showlegend=False))
        return fig
        
        
    def plot_active(self, today):
        ## main plots
        fig = go.Figure()
        fig.add_trace(go.Bar(x=self.dtf.index, y=self.dtf["delta_data"], name='data', marker_color='black'))
        fig.add_trace(go.Scatter(x=self.dtf.index, y=self.dtf["delta_forecast"], mode='none', name='forecast', fill='tozeroy'))
        ## add slider
        fig.update_xaxes(rangeslider_visible=True)
        ## set background color
        fig.update_layout(plot_bgcolor='white', autosize=False, width=1000, height=550)
        ## add vline
        fig.add_shape({"x0":today, "x1":today, "y0":0, "y1":self.dtf["delta_forecast"].max(), 
                       "type":"line", "line":{"width":2,"dash":"dot"} })
        fig.add_trace(go.Scatter(x=[today], y=[self.dtf["delta_forecast"].max()], text=["today"], mode="text", line={"color":"green"}, showlegend=False))
        return fig
    
        
    def get_panel(self):
        peak_day, num_max = self.calculate_peak(self.dtf)
        total_cases_until_today, total_cases_in_30days, active_cases_today, active_cases_in_30days = self.calculate_max(self.dtf)
        return peak_day, num_max, total_cases_until_today, total_cases_in_30days, active_cases_today, active_cases_in_30days

Front-end: Build the app with Dash and Bootstrap

Finally, here we are, about to code the app using Dash and Dash Bootstrap Components (henceforth as “dbc”), I am going to explain it step by step and also provide the full code of dash.py (inside the application folder).

For this we need the following imports:

import dash
from dash.dependencies import Input, Output, State
import dash_core_components as dcc
import dash_html_components as html
import dash_bootstrap_components as dbc

As start, I need to define the app instance and in doing this dbc already provides a great feature in choosing a Bootstrap CSS theme:

app = dash.Dash(external_stylesheets=[dbc.themes.LUX])

Following a visual order, I shall now approach the top navbar. I want something cool and reactive on click, with pop-ups and a drop-down menu, but I’d like to not waste too much time on writing CSS and JS code. To put it another way, I want to use Bootstrap like this:

https://getbootstrap.com/docs/4.0/components/navs/

Similarly to this html, we can use dbc to crate the navbar and its items:

dbc.Nav([
    dbc.NavItem(),
    dbc.NavItem(),
    dbc.DropdownMenu()
])

You got the gimmick, right? Dash and Dbc replicate the same structure and logic of the html syntax. With this in mind, inside each item we can add whatever we want:

Moving on with the input form, I‘d like to get a simple drop-down menu with all the possible countries as options and the “World” as default selection. In order to do this, it’s necessary to read the data before coding the drop-down menu object. Do you remember the Data class written before in data.py (python folder)? Well, now it’s the right time to use it:

from python.data import Data
data = Data()
data.get_data()

Now that we have the country list in the Data object, we can write the drop-down menu and set the options in it with a simple for loop:

dcc.Dropdown(id="country", options=[{"label":x,"value":x} for x in        
             data.countrylist], value="World")])

In Dash, if not specifically programmed, the output will be put in rows, one below the other. However, I’d like to have all contained in the screen size, so users do not need to scroll down. That’s why I am going to use tabs and each one will show one of the 2 plots I coded before in result.py (in python folder) with plotly. With dbc this is super easy:

dbc.Tabs([
         dbc.Tab(dcc.Graph(id="plot-total"), label="Total cases"),
         dbc.Tab(dcc.Graph(id="plot-active"), label="Active cases")
        ])

I bet you’re wondering “how does the app know that in the first tab it has to put the first plot and in the second the other?”. Well, you’re not wrong, the app needs a link between the html and the Python code output. In Dash this is done with callbacks. A callback is nothing more than a decorator, a function that takes another function and extends the behavior of the latter function without explicitly modifying it.

Let’s take the plot of total cases as an example: I need a function that takes the country selected from the front-end as input and returns the plot as output using the Model and Result classes I coded before (in python folder). Something like this:

def plot_total_cases(country):
    data.process_data(country) 
    model = Model(data.dtf)
    model.forecast()
    model.add_deaths(data.mortality)
    result = Result(model.dtf)
    return result.plot_total(model.today)

As you surely noticed, in the previous code where I defined the tabs, I put an id in the first one (id=”plot-total”). So I need to add, on top of this function, a callback decorator to tell the app that the figure the back-end will plot refers to that id and that the input is the country value.

@app.callback(output=Output("plot-total","figure"), 
              inputs=[Input("country","value")]) 

Ultimately, the panel on the right with some statistics is a little different because the python function doesn’t return a plot like before but an entire html div. In fact, the dash code this time is going to be inside the callback function that calculate those numbers. I’m talking about this:

This covers pretty much all the elements of the front-end layout, it’s a very basic application with one single input and few outputs (plots and numbers).

Full code of dash.py:

# Setup
import dash
from dash.dependencies import Input, Output, State
import dash_core_components as dcc
import dash_html_components as html
import dash_bootstrap_components as dbc
from settings import config, about
from python.data import Data
from python.model import Model
from python.result import Result
# Read data
data = Data()
data.get_data()
# App Instance
app = dash.Dash(name=config.name, assets_folder=config.root+"/application/static", external_stylesheets=[dbc.themes.LUX, config.fontawesome])
app.title = config.name
# Navbar
navbar = dbc.Nav(className="nav nav-pills", children=[
    ## logo/home
    dbc.NavItem(html.Img(src=app.get_asset_url("logo.PNG"), height="40px")),
    ## about
    dbc.NavItem(html.Div([
        dbc.NavLink("About", href="/", id="about-popover", active=False),
        dbc.Popover(id="about", is_open=False, target="about-popover", children=[
            dbc.PopoverHeader("How it works"), dbc.PopoverBody(about.txt)
        ])
    ])),
    ## links
    dbc.DropdownMenu(label="Links", nav=True, children=[
        dbc.DropdownMenuItem([html.I(className="fa fa-linkedin"), "  Contacts"], href=config.contacts, target="_blank"), 
        dbc.DropdownMenuItem([html.I(className="fa fa-github"), "  Code"], href=config.code, target="_blank")
    ])
])
# Input
inputs = dbc.FormGroup([
    html.H4("Select Country"),
    dcc.Dropdown(id="country", options=[{"label":x,"value":x} for x in data.countrylist], value="World")
])
# App Layout
app.layout = dbc.Container(fluid=True, children=[
    ## Top
    html.H1(config.name, id="nav-pills"),
    navbar,
    html.Br(),html.Br(),html.Br(),
    ## Body
    dbc.Row([
        ### input + panel
        dbc.Col(md=3, children=[
            inputs, 
            html.Br(),html.Br(),html.Br(),
            html.Div(id="output-panel")
        ]),
        ### plots
        dbc.Col(md=9, children=[
            dbc.Col(html.H4("Forecast 30 days from today"), width={"size":6,"offset":3}), 
            dbc.Tabs(className="nav nav-pills", children=[
                dbc.Tab(dcc.Graph(id="plot-total"), label="Total cases"),
                dbc.Tab(dcc.Graph(id="plot-active"), label="Active cases")
            ])
        ])
    ])
])
# Python functions for about navitem-popover
@app.callback(output=Output("about","is_open"), inputs=[Input("about-popover","n_clicks")], state=[State("about","is_open")])
def about_popover(n, is_open):
    if n:
        return not is_open
    return is_open
@app.callback(output=Output("about-popover","active"), inputs=[Input("about-popover","n_clicks")], state=[State("about-popover","active")])
def about_active(n, active):
    if n:
        return not active
    return active
# Python function to plot total cases
@app.callback(output=Output("plot-total","figure"), inputs=[Input("country","value")]) 
def plot_total_cases(country):
    data.process_data(country) 
    model = Model(data.dtf)
    model.forecast()
    model.add_deaths(data.mortality)
    result = Result(model.dtf)
    return result.plot_total(model.today)
# Python function to plot active cases
@app.callback(output=Output("plot-active","figure"), inputs=[Input("country","value")])
def plot_active_cases(country):
    data.process_data(country) 
    model = Model(data.dtf)
    model.forecast()
    model.add_deaths(data.mortality)
    result = Result(model.dtf)
    return result.plot_active(model.today)
# Python function to render output panel
@app.callback(output=Output("output-panel","children"), inputs=[Input("country","value")])
def render_output_panel(country):
    data.process_data(country) 
    model = Model(data.dtf)
    model.forecast()
    model.add_deaths(data.mortality)
    result = Result(model.dtf)
    peak_day, num_max, total_cases_until_today, total_cases_in_30days, active_cases_today, active_cases_in_30days = result.get_panel()
    peak_color = "white" if model.today > peak_day else "red"
    panel = html.Div([
        html.H4(country),
        dbc.Card(body=True, className="text-white bg-primary", children=[
            html.H6("Total cases until today:", style={"color":"white"}),
            html.H3("{:,.0f}".format(total_cases_until_today), style={"color":"white"}),
            
            html.H6("Total cases in 30 days:", className="text-danger"),
            html.H3("{:,.0f}".format(total_cases_in_30days), className="text-danger"),
            
            html.H6("Active cases today:", style={"color":"white"}),
            html.H3("{:,.0f}".format(active_cases_today), style={"color":"white"}),
            
            html.H6("Active cases in 30 days:", className="text-danger"),
            html.H3("{:,.0f}".format(active_cases_in_30days), className="text-danger"),
            
            html.H6("Peak day:", style={"color":peak_color}),
            html.H3(peak_day.strftime("%Y-%m-%d"), style={"color":peak_color}),
            html.H6("with {:,.0f} cases".format(num_max), style={"color":peak_color})
        
        ])
    ])
    return panel

How do we find out if we made any errors in the code? We run the application. Only one line of code is necessary to run the whole thing and I shall put it in the run.py file (on root level):

from application.dash import app
from settings import config
app.run_server(debug=config.debug, host=config.host, port=config.port)

Run the following command in the terminal:

python run.py

and you should see this:

Great job, the application is up and running!

Deploy

Do you want to make your application available for anyone? Then you have to deploy it somewhere. I usually use Heroku, a cloud platform as a service that allows deploying a PoC app with just a free account.

You can link a Github repo and deploy one of the branches.

In order for this to work, the app needs a requirements.txt and a Procfile. In the Setup section, I already put the command to create the text file with the required packages. In regard to the Procfile, it’s just the command line to run the app that I put in the previous section. Heroku will run it and there you go:

Conclusion

This article has been a tutorial to show how easy is to build a nice looking web application with Dash and Dash Bootstrap Components that embeds all the CSS and JS of Bootstrap. I used my Covid-19 infection forecaster app as example, going through every step from back-end to front-end and even deployment. Now that you know how it works, you can develop your own forecaster, for example changing the data source (i.e. yahoo finance) and the machine learning model (i.e. lstm neural network) you can build a stock price forecaster.

I hope you enjoyed it! Feel free to contact me for questions and feedback or just to share your interesting projects.

👉 Let’s Connect 👈

This article is part of the series Web Development with Python, see also:

Data Science
Programming
Visualization
Machine Learning
Web Development
Recommended from ReadMedium
avatarTechnocrat
Using Streamlit for UI

32 min read