How to: Build an Immersive Geo Bubble Map with Plotly
By: Truett Bloxsom
Tags: Data Visualization, Covid 19, Editors Pick, Data Science, Plotly

gif by yours truly

In this tutorial, I will be creating a county-level geographic bubble map of COVID-19 cases in Texas using Plotly. The plot in the above gif can be found at the bottom of my website. My website is best accessed with a laptop rather than a mobile device. I also made another tutorial on how I created another interactive plotly graph and website using dash here. All the code and data I used to create this map can found on my Github. The .ipynb file has all of the code and graph included, but you must have all the .csv and .xlsx files downloaded to create the graph on your own as well as the associated plotly libraries. I will be assuming that you have some knowledge of python, pandas, and plotly. Let's go through the steps I went through to create this beautiful map:

Load in data and clean it using pandas Merge related data frames together and clean Format data to be input into the plotly graph with groupby() Create graph using plotly.express.chorograph() and plotly.graph_objects.Scattergeo()

One thing to note about the code snippets is you might have to change the directory the data files are located in before running the code. Ok, let's get started!

Load in data and clean it using pandas

import pandas as pd

import plotly.express as px

from urllib.request import urlopen

import json

import plotly.graph_objects as go

fips_data = pd.read_excel("PHR_MSA_County_masterlist.xlsx", dtype={'FIPS #': str}) #need this to plot counties on map, source:

fips_df = fips_data[["County Name", "FIPS #"]] # read in datafips_data = pd.read_excel("PHR_MSA_County_masterlist.xlsx", dtype={'FIPS #': str}) #need this to plot counties on map, source: https://www.dshs.texas.gov/chs/info/info_txco.shtm fips_df = fips_data[["County Name", "FIPS #"]] #add 48 to end of each county fips so it can be input into plotly to geo graph data

def add_48(s):

return "48" + str(s)

fips_df["FIPS #"] = fips_df["FIPS #"].map(add_48) cities_df = pd.read_csv("uscities.csv") #need for plotting texas cities, source: https://simplemaps.com/data/us-cities # for plotting major texas cities

mask = cities_df["state_id"] == "TX"

texas_cities = cities_df[mask]

mask2 = texas_cities["population"] > 200000

big_texas_cities = texas_cities[mask2]

cities_for_map = big_texas_cities[["city", "lat", "lng"]]

ts_df = pd.read_csv("time_series_plotly.csv").drop("Unnamed: 0", axis = 1) # I created this data from a previous medium post

county_loc = pd.read_csv("Texas_Counties_Centroid_Map.csv") #source: # load in additional data with long and lat of each county as well as covid-19 case counts for new bubble graphts_df = pd.read_csv("time_series_plotly.csv").drop("Unnamed: 0", axis = 1) # I created this data from a previous medium postcounty_loc = pd.read_csv("Texas_Counties_Centroid_Map.csv") #source: https://data.texas.gov/dataset/Texas-Counties-Centroid-Map/ups3-9e8m/data

Step 1 is pretty self-explanatory but it will be nice to see what all 4 data frames that I loaded in look like.

image by yours truly

image by yours truly

image by yours truly

The fips_df just has all the fips codes for each county in Texas which can be used with plotly to graph each county. The ts_df is a long-form data frame that has all the COVID-19 case counts for each county for each day since March. The county_loc has the longitude and latitude for each county but they are flipped: the Y (Long) column has the latitude not the longitude of the counties which is an easy thing to fix when plotting. And the cities_for_map has the longitude and latitude data for cities in Texas that have a population greater than 200,000.

2. Merge related data frames together and clean

#merge covid-19 case counts with county longitude and latitude data

ts_df2 = ts_df.merge(county_loc, how = "left", left_on = "County", right_on = "CNTY_NM") #merge above df with fips data

ts_df3 = ts_df2.merge(fips_df,how = "left", left_on = "County", right_on = "County Name")

def extract_month(s):

return s[5:7]

ts_df3["month"] = ts_df3["Date"].map(extract_month) # only extract last 4 months of data

mask = ts_df3["month"].astype(int) > 4

ts_df4 = ts_df3[mask]

A few things to note about the above code:

I am merging all three data frames using the County Name as the primary/foreign key I also did not have to merge the fips_df but was having trouble using the county_loc data frame fips column to graph. I will only be using the most recent 4 months of data since it is the most significant

image by yours truly

So now the data has a row for each day and each county in Texas with the COVID-19 case counts for the past 4 months.

3. Format data to be input into the plotly graph with groupby()

df = (ts_df4.groupby(["month", "County", "X (Lat)", "Y (Long)", "FIPS #"])["Case Count"].max()).reset_index() df["month"] = df["month"].astype(int)

Ok, so this is where it gets a little complicated. I need to group by month, county, long, lat, and FIPS then get the max case count. This will give me a data frame that has the total case count for each county for each month. The data frame is shown below.

image by yours truly

4. Create graph using plotly.express.chorograph() and plotly.graph_objects.Scattergeo()

The plotting of this graph takes quite a lot of coding and understanding of plotly so I will go over it in chunks.



counties = json.load(response) with urlopen(' https://raw.githubusercontent.com/plotly/datasets/master/geojson-counties-fips.json' ) as response:counties = json.load(response) fig = px.choropleth(df, geojson=counties, locations='FIPS #',

hover_name = "County",

scope = "usa",

title = "Total Cases"

)

First, we will start with the base figure that has all the counties in Texas mapped. To do this we use the plotly.express.chorograph() function and input the data frame we just created. Then specify that we want to use the geojson library to map the counties using the FIPS # column from our df. Plotly has great documentation on this here. If we just plotted this with fig.show() it would look like the image below.

colors = ['rgb(189,215,231)','rgb(107,174,214)','rgb(33,113,181)','rgb(239,243,255)']

months = {5: 'May', 6:'June',7:'July',8:'Aug'}

#plot the bubble cases for each month and each county

for i in range(5,9)[::-1]:

mask = df["month"] == i

df_month = df[mask]

#print(df_month)

fig.add_trace(go.Scattergeo(

locationmode = 'USA-states',

lon = df_month['X (Lat)'],

lat = df_month['Y (Long)'],

text = df_month[['County','Case Count']],

name = months[i],

mode = 'markers',

marker = dict(

size = df_month['Case Count'],

color = colors[i-6],

line_width = 0,

sizeref = 9,

sizemode = "area",

reversescale = True

)))

Ok, here comes the hardest part. First, we create a colors list that has the colors we want to use for each month. We also create a months dictionary for the graph’s legend.

Next comes the for loop which loops through 4 times, for each month. For each month, it plots a bubble for each county based on the location using the plotly.graph_objects.Scattergeo() function. You will notice that the lon and lat variables are switched but that is because the data I imported had this problem. I use the variable sizeref to normalize the bubble points since there are some counties with very large case counts which would dominate the map if not normalized. Plotly recommends this function with sizeref:

sizeref = 2. * max(array of size values) / (desired maximum marker size ** 2)

But I found that function to not work well and they don’t even use it in their documentation examples. I found that 9 worked best for me but the number is very sensitive and I tried a range of 0–10, with many numbers making the plot unreadable, before choosing 9. Below is the plot in its current form:

image by yours truly

So we are getting close to the final product but we still need a couple more things.

# to show texas cities on map

fig.add_trace(go.Scattergeo(

locationmode = 'USA-states',

lon = cities_for_map['lng'],

lat = cities_for_map['lat'],

hoverinfo = 'text',

text = cities_for_map['city'],

name = "Major Cities",

mode = 'markers',

marker = dict(

size = 4,

color = 'rgb(102,102,102)',

line = dict(

width = 3,

color = 'rgba(68, 68, 68, 0)'

)

))) fig.update_geos(fitbounds="locations")

fig.update_layout(title_text='Total Cases per month for last 4 months', title_x=0.5)

fig.show()

In the above code block, we plot the major Texas cities using long/lat data and the plotly.graph_objects.Scattergeo() function but without the for loop. Then we update the orientation of the graph, so Texas fits well into the frame of the graph. Then we update the title of the graph and center it. Finally, we can visualize its completed form!

Thanks for reading and possibly following my tutorial! If you want to follow me on medium or LinkedIn feel free. I am also always looking for feedback on my posts and coding, so if you have any to give me, I would greatly appreciate it.