The Current State of COVID-19 From 3 Different Perspectives

Let the Data and Visuals Do the Talking — A Streamlit Web App

Navid Mashinchi
Towards Data Science

--

Image by author

Did you ever want to share your project with anyone in the world in just a few hours and not weeks? If so, then you are at the right place here. This article will take you through how I created my own COVID-19 dashboard by using Streamlit. Streamlit allows you to create custom web applications for machine learning and data science in a simple way. Below is the link to my app:

https://share.streamlit.io/navido89/covid19-dashboard-dataviz/app.py

Objective:

First, to better understand my project, I will take you through what I tried to accomplish. The purpose of this project was to have a look at the current state of COVID-19 using visualizations from three different perspectives. The plots have been created by using data visualization tools such as Plotly and Folium. The project is broken down into three parts.

1. Global COVID-19 Situation: I display a folium map that shows the total cases, total deaths, total cases per 100,000, and total deaths per 100,000. In addition to that, I display various time series plots to understand better how the disease spreads over time across the globe.

2. COVID-19 Situation by World Health Organization Region (WHO): In the following section, I look at the disease from the World Health Organization’s regional perspective. I compare the different regions based on their total cases, total deaths, total cases per 100 million and total deaths per 100 million deaths.

3. COVID-19 Situation in the United States: Last but not least, I look into the United States and explore the current situation in the US based on the cases, deaths (with and without per capita), vaccine status, and the status of the different variants spreading across the states.

The data is pulled from various resources, and it will be updated daily. Please click here to get access to my repo, and you will be able to pull the data.

Methods:

Before I get into Streamlit, I created a function for each of the plots. As mentioned above, I used Plotly and Folium for the visualizations. The reason for that is because I wanted all my graphs to be interactive, and Plotly and Folium do a great job for that. Once I have created the functions for each of the plots, I then created a main function. The main function is where I write the code for my Streamlit app and call each of the plots' functions. Before I get into the Streamlit portion, I will first take you through the methods I had to implement to get the plots ready.

Data Cleaning:

As many of you know, we spend most of the time on data cleaning when it comes to data science projects. The same goes for my web app. Cleaning all the different data that I pulled in to create the plots took me the most time. I am not going into much detail on how I cleaned the data for each of the plots, but there was a common pattern for many of them. The steps that I had to take to get the data ready for the graphs included the following:

  • read in the data
  • removing columns & rows
  • replace strings with a new string value
  • group by function
  • merge datasets

Feature Engineering:

Again, I am not going into detail regarding feature engineering, but there were some common patterns that I will highlight now. For example, in the time series plots, I had to change the type from object to DateTime. In addition to that, I created columns that show the total cases or deaths per capita. For those particular columns, I had to make the calculation by using the population, the total cases, and deaths for the specific location. The purpose of getting the numbers per capita was to understand the situation between different locations better.

Data Visualization:

As mentioned earlier in the article, I used Plotly and Folium for the visualizations. For Plotly plots, Streamlit has a function called plotly_chart(), and for Folium, you can import the following library:

from streamlit_folium import folium_static

foloum_static is a function that allows you to pass in a folium map as an argument so it can render in your app.

There are two advanced plots in my project that I will talk about in more detail so that you will be able to create those in your own projects. The other plots are pretty straightforward, and I highly suggest going over the Plotly documentation. Please see the link below:

First, let’s talk about the Folium Choropleth maps in the app. I wrote a tutorial earlier on how to plot more advanced multi-layer Folium Choropleth maps. If you aren’t familiar with Folium, please check out the article to pick up your folium skills.

In the article above, I am going over an example where I implement a more advanced map. One problem that I wasn’t able to figure out when I wrote the article was how we could delete the choropleth’s legend and create a legend for each layer because, by default, all legends for each column will be displayed on the plots. In my tutorial, I had a separate button for each of the legend in addition to the layers. However, my ultimate goal was to bind each layer with a legend. In other words, when I click on the layer controller and on a specific column, the layer will automatically appear with its associated legend. Below I will take you through the steps on how to create the map in my app.

In the picture below, you can see one of the data frames for one of the Folium Choropleth maps in my app. After cleaning and feature engineering, we get the following:

Image by author

There is one JavaScript function that we are going to implement. First, we create a BindColormap class that binds a customized colormap to a given layer. To make it work, you need to import MacroElement and Template, as shown below.

from branca.element import MacroElementfrom jinja2 import Templateclass BindColormap(MacroElement):
"""Binds a colormap to a given layer.
Parameters
----------
colormap : branca.colormap.ColorMap
The colormap to bind.
"""
def __init__(self, layer, colormap):
super(BindColormap, self).__init__()
self.layer = layer
self.colormap = colormap
self._template = Template(u"""
{% macro script(this, kwargs) %}
{{this.colormap.get_name()}}.svg[0][0].style.display = 'block';
{{this._parent.get_name()}}.on('overlayadd', function (eventLayer) {
if (eventLayer.layer == {{this.layer.get_name()}}) {
{{this.colormap.get_name()}}.svg[0][0].style.display = 'block';
}});
{{this._parent.get_name()}}.on('overlayremove', function (eventLayer) {
if (eventLayer.layer == {{this.layer.get_name()}}) {
{{this.colormap.get_name()}}.svg[0][0].style.display = 'none';
}});
{% endmacro %}
""")

Once we have the function above, it’s now time to create the customized legends for each of the columns.

# We create the colors for the custom legends
colors = ["YlGn","OrRd","BuPu","GnBu"]
# Create a custom legend using branca for each column
cmap1 = branca.colormap.StepColormap(
colors=['#ffffcc','#d9f0a3','#addd8e','#78c679','#238443'],
vmin=0,
vmax=df_global_folium['covid_total'].max(),
caption='Covid Total')
cmap2 = branca.colormap.StepColormap(
colors=["#fef0d9",'#fdcc8a','#fc8d59','#d7301f'],
vmin=0,
vmax=df_global_folium['covid_deaths'].max(),
caption='Covid Deaths')

cmap3 = branca.colormap.StepColormap(
colors=branca.colormap.step.BuPu_09.colors,
vmin=0,
vmax=df_global_folium['covid_cases_per_100k'].max(),
caption='covid_cases_per_100k')

cmap4 = branca.colormap.StepColormap(
colors=branca.colormap.step.GnBu_09.colors,
vmin=0,
vmax=df_global_folium['covid_deaths_per_100k'].max(),
caption='covid_deaths_per_100k')
cmaps = [cmap1, cmap2,cmap3,cmap4]

We use the Branca library, as you can see above, to use the StepColormap functionality. Here we are creating 4 individual legends for the following columns from the above data frame:

  • covid_total
  • covid_deaths
  • covid_cases_per_100k
  • covid_deaths_per_100k

To better understand the attributes of the StepColormap function, please check out the documentation below:

Let’s focus on cmap4 in the code above, which is the legend for covid_deaths_per_100k. The process is the same for the other columns, but I want you to see what the code actually outputs. At the top of the last code snippet, I create a color list representing each of the legends' colors. First, I set the colors attribute to a Branca step color with the color palette GnBu_09. Then I set the vmin value, which is an attribute that allows us to set a minimum number in the leged. The vmax, on the other hand, allows us to set a maximum number. The last attribute caption allows us to set a name for the legend. The legend that we created in cmap4 can be seen below.

Image by author

In the last line of the code, I create a list called cmaps, consisting of all the individual legends that I have created for the four columns mentioned above.

The next step is to create the folium map and bind the individual legends with each layer.

columns_list_global_map = ["covid_total","covid_deaths","covid_cases_per_100k","covid_deaths_per_100k"]sample_map = folium.Map(location=[51,10], zoom_start=2)# Set up Choropleth map
for color, cmap, i in zip(colors, cmaps, columns_list_global_map):

choropleth = folium.Choropleth(
geo_data=df_global_folium,
data=df_global_folium,
name=i,
columns=['name',i],
key_on="feature.properties.name",
fill_color=color,
colormap= cmap,
fill_opacity=1,
line_opacity=0.2,
show=False
)

# this deletes the legend for each choropleth you add
for child in choropleth._children:
if child.startswith("color_map"):
del choropleth._children[child]
style_function1 = lambda x: {'fillColor': '#ffffff',
'color':'#000000',
'fillOpacity': 0.1,
'weight': 0.1}
highlight_function1 = lambda x: {'fillColor': '#000000',
'color':'#000000',
'fillOpacity': 0.50,
'weight': 0.1}
NIL1 = folium.features.GeoJson(
data = df_global_folium,
style_function=style_function1,
control=False,
highlight_function=highlight_function1,
tooltip=folium.features.GeoJsonTooltip(
fields= ['name',"covid_total","covid_deaths",
"covid_cases_per_100k",
"covid_deaths_per_100k"],
aliases= ['name',"covid_total","covid_deaths",
"covid_cases_per_100k",
"covid_deaths_per_100k"],
style=("background-color: white; color: #333333; font-
family: arial; font-size: 12px; padding: 10px;")
)
)
sample_map.add_child(NIL1)
sample_map.keep_in_front(NIL1)
# add cmap to `sample_map`
sample_map.add_child(cmap)

# add choropleth to `sample_map`
sample_map.add_child(choropleth)

# bind choropleth and cmap
bc = BindColormap(choropleth, cmap)

# add binding to `m`
sample_map.add_child(bc)

# Add dark and light mode.
folium.TileLayer('cartodbdark_matter',name="dark mode",control=True).add_to(sample_map)

folium.TileLayer('cartodbpositron',name="light mode",control=True).add_to(sample_map)
sample_map.add_child(folium.LayerControl())sample_map

First, we create a list of each of the columns that we want to iterate over. Next, we set up our folium map and call it smaple_map. After that, we iterate over a zip object, an iterator of tuples consisting of colors, cmaps, country_lists_global_map. The first zip object that we loop over would be -> (“YlGn,” cmap1, covid_total). The second zip object would be (“OrRd,” cmap2, covid_deaths) and so on. Next, we create a choropleth variable and set it to the folium.choropleth() function. Inside the function, we set the attributes to the values that you can see below under the data frame. NOTE df_global_folium is the data frame from above. See below again:

Image by author
        geo_data=df_global_folium,
data=df_global_folium,
name=i,
columns=['name',i],
key_on="feature.properties.name",
fill_color=color,
colormap= cmap,
fill_opacity=1,
line_opacity=0.2,
show=False

Once we create the layer, we immediately delete the legend that will be displayed by default. The reason for deleting the legend is to avoid having multiple legends appear on one plot. Please see below the code that removes the legend.

# this deletes the legend for each choropleth you add
for child in choropleth._children:
if child.startswith("color_map"):
del choropleth._children[child]

After we delete the legend, we implement the hover functionality to display the data on the map.

style_function1 = lambda x: {'fillColor': '#ffffff', 
'color':'#000000',
'fillOpacity': 0.1,
'weight': 0.1}
highlight_function1 = lambda x: {'fillColor': '#000000',
'color':'#000000',
'fillOpacity': 0.50,
'weight': 0.1}
NIL1 = folium.features.GeoJson(
data = df_global_folium,
style_function=style_function1,
control=False,
highlight_function=highlight_function1,
tooltip=folium.features.GeoJsonTooltip(
fields= ['name',"covid_total","covid_deaths",
"covid_cases_per_100k",
"covid_deaths_per_100k"],
aliases= ['name',"covid_total","covid_deaths",
"covid_cases_per_100k",
"covid_deaths_per_100k"],
style=("background-color: white; color: #333333; font-
family: arial; font-size: 12px; padding: 10px;")
)
)
sample_map.add_child(NIL1)
sample_map.keep_in_front(NIL1)

We create a style function and a highlight function, which we will use inside the folium.features.GeoJson() function. We set the style_function and highlight_function as style_function1 and highlight_function1 respectively. Those attributes set up the visual appearance of when we hover over a location. In the tooltip variable, we assign the data to be displayed from the data frame when we hover over a location. We then add the NIL1 variable to the map as a child and keep it in the front of the map.

        # add cmap to `sample_map`
sample_map.add_child(cmap)

# add choropleth to `sample_map`
sample_map.add_child(choropleth)

# bind choropleth and cmap
bc = BindColormap(choropleth, cmap)

# add binding to `m`
sample_map.add_child(bc)

# Add dark and light mode.
folium.TileLayer('cartodbdark_matter',name="dark mode",control=True).add_to(sample_map)
folium.TileLayer('cartodbpositron',name="light mode",control=True).add_to(sample_map)
sample_map.add_child(folium.LayerControl())sample_map

As you can see above, we then add the individual legend (“cmap”) to the plot, add the choropleth to the map, bind the choropleth and the legend and add it as a child to the map. Note that the steps above were just for one iteration. Once the loop comes to an end, the code continues to the next line. Finally, we add the dark mode, light mode, and layer controller to the map. There you have it. Below is the final Folium Choropleth map.

Gif by author

The second plot that I will go into more detail is about the Plotly plot, where I add two drop-down menus to one figure. The purpose of having two drop-down menus was to select different states and compare the situation concerning the total cases of the different COVID-19 Variants recorded. First, let’s look at the plot, and we will then dive into the code.

Gif by author

As you can see above, we can compare the different states by their COVID-19 cases caused by the different variants. Let’s have a look at the data frame first before we dive into the code. Below you can see the data frame of the Plotly plot above, which is called cdc_variants_data.

Image by author

The index consists of three different variants, and the other columns are all the different states. The code to plot the graph above is the following:

df = cdc_variants_data# plotly figure setup
fig = go.Figure()
# one trace for each df column
fig.add_trace(go.Bar(name="Selection 1",x=df.index, y=df["Alabama"].values))
# one trace for each df column
fig.add_trace(go.Bar(name="Selection 2",x=df.index, y=df["Alabama"].values))
# one button for each df column
updatemenu= []
buttons=[]
# add second buttons
buttons2 = []
for i in list(df.columns):
buttons.append(dict(method='restyle',label = str(i),args=[{'x':[df.index],'y':[df[i].values]},[0]])
)

for i in list(df.columns):
buttons2.append(dict(method='restyle',label = str(i),args=[{'x':[df.index],'y':[df[i].values]},[1]])
)
# some adjustments to the updatemenus
button_layer_1_height = 1.23
updatemenu = list([dict(
buttons = buttons,
direction="down",
pad={"r":10,"t":10},
showactive=True,
x= 0.1,
xanchor="left",
y= button_layer_1_height,
yanchor="top",
font = dict(color = "blue")
),dict(
buttons = buttons2,
direction="down",
pad={"r":10,"t":10},
showactive=True,
x= 0.37,
xanchor="left",
y=button_layer_1_height,
yanchor="top",font = dict(color = "red"))])
fig.update_layout(showlegend=True, updatemenus=updatemenu)
fig

We are using Plotly’s graph objects. We first create a variable called df and set it to the data frame cdc_variants_data. We then add two traces to have two different bars on the same figure. We use the Bar function and set the x value as the index and the y axis as the Alabama column. After that, we create three empty lists. The first one is one to update the menu called updatemenu. The other two are called buttons1 and buttons2. After that, we have two separated for loops where we iterate over all the columns (states) and add them to each of the buttons. The args attribute is important since we add the index from the data frame to x and the column values for each state to y. Further, the [0] and [1] represent the first and second buttons, respectively. In addition to that, we create a list of dictionaries that we call updatemenu. In each of the dictionaries, we set the buttons attribute to the button variables that we created before the two for loops. Finally, we call the Plotly update_layout() function, and we set the updatemenus attribute to the updatemenu variable. I highly suggest you grab the code above and grab a sample data frame and play around with it, so you get a better understanding of how to plot it.

Now that we have covered the different methods (Data Cleaning, Feature Engineering, and Data Visualization), we can now shift our focus on how to create the app.

Creating the Web App:

To understand how I created my app in Streamlit, I will first take you through a simple example. First, you need to install the library in your environment. Please check the document below on how to install Streamlit.

Next, open a new python script and copy-paste the following:

import streamlit as stdef main():
st.title("Deploy me to Heroku")
st.subheader("Welcome to my app.")
if __name__ == '__main__':
main()

As you can see above, we first import the streamlit library. After that, I create a main function. Inside, the main function is where I start working on the front-end of my app. I am calling st.title() to add a title to my app, followed by a subheader I call with st.subheader(). Next, save your file, and in your command-line interface (CLI), go to the location where your python script is saved. Next, to see what the code above outputs, just run the code below:

streamlit run app.py

A window will pop up automatically in your browser, and you will see the following:

Image by author

Voila, there you have your app. The output above will run on your local machine. Once you are done with your app, and you would like to deploy it, you can use Streamlit Sharing. You will need to request an invite here. According to the documents, Streamlit sharing is available only by invitation. Once you make your request, it will take a couple of days to receive the invitation. If you can’t wait and would like to host your app to Heroku right away, please check out my article below, which is a step-by-step guide on how to deploy your Streamlit app to Heroku.

Now that you have a better understanding, I will take you through my own application. As the example above, my main() function consists of the code of my Streamlit app. My application consists of one sidebar that allows the user to navigate through the app and the following pages:

  • Home Page
  • Global Situation
  • Situation by WHO Region
  • Situation in the United States

The components that I am using in my app are the following:

  • st.title
  • st.subheader
  • st.markdown
  • st.sidebar.radio
  • st.sidebar.markdown
  • st.info
  • st.image
  • st.table
  • st.plotly_chart
  • folium_static

To get a better understanding of each of the functionalities, I highly recommend the documentation. Click here for the documentation. It took me a couple of days to get the hang of it to create my app. Adding a sidebar, images, gifs, and different kinds of plots are really straightforward. As mentioned earlier, for my Plotly and folium plots, I used the st.plotly_chart() and folium_static() function. To get full access to my main function that creates the Streamlit app, please click on the link below and scroll all the way down to the main() function.

Streamlit’s performance is overall very impressive. To improve the performance of your app, I highly suggest taking advantage of the caching mechanism. Click here to go over the documentation on how to implement the caching mechanism to your app.

Conclusion:

Overall I was very impressed with how simple it was to use Streamlit. What impressed me even more, is its performance regarding the data's loading time and the plots' rendering time. As I was writing this article today (March 18th, 2021), Strimlit announced its custom theming. You can now turn on the dark mode and light mode in your app. In addition to that, you can customize the style of your app. As soon as I saw the news on its new updates, I jumped on this opportunity to add some styling to my app. Please see below on how you can switch between the different themes.

Gif by author

I hope you enjoyed this article and got inspired to create your own Streamlit app. Let me know if you have any questions on this topic or have some feedback on my app. There is always room for improvement. If you enjoyed this article, I’d be very grateful if you would share it on any social media platforms. Until next time️! ✌️

--

--

Data Scientist at Kohl’s | Adjunct Professor at University of Denver | Data Science Mentor at SharpestMinds