Choropleth Maps using Plotly
By: Shreyeshi Somya
Tags: Choropleth Map, Plotly, Python, Data Visualization

I have been working as a Data Analyst for almost 5 years now but, in this time I have mostly used business intelligence software for all my data visualization tasks. So my experience with visualization has been limited to knowing when to plot a bar chart vs a line chart. To correct this, I have recently taken up to learn matplotlib and other plotting and graphing libraries. This is an attempt in that direction. You can use this as a guide to choropleth maps, or plotly, or both. Hope this helps.

Map by Our World in Data

Choropleth Maps are a type of thematic map in which areas are shaded or patterned in proportion to a statistical variable (from Wikipedia). This map for example shows the share of adults who were obese in 2016.

Plotly is a company based in Canada, that develops analytics and data visualization tools. They have created an open-source scientific graphing library for Python, which I am using here.

For this task, I am looking at data for Religious Adherents in the United States (Source: theARDA.com). The data contains County-level information for different religious groups for the years 1980, 1990, 2000 & 2010. It also includes the population of the counties in that year, along with some other information.

So let’s first load the data and the libraries we are going to need.

#import libraries

%matplotlib notebook

import pandas as pd

import numpy as np

import matplotlib.pyplot as plt

import plotly.express as px

import plotly.graph_objects as go

from plotly.subplots import make_subplots #import raw data

df = pd.read_excel(‘RCMSMGCY.xlsx’, dtype={“FIPSMERG”: str})

df.head()

Now, the next 3 steps involve cleaning, filtering, and summarizing the data for analysis, so if you want you can skip this and move directly to the Choropleth maps.

Data Cleaning

To plot the US county data, I am going to use the FIPS code, stored in column FIPSMERG here. The FIPS code is a unique 5-digit code that identifies counties in the United States. As you can see from the data snapshot above, in the first 5 rows, the FIPSMERG has only 4 digits. This means, that the leading zero is missing. Let’s add that using the lambda function.

#adding 0 to FIPS code

df[‘FIPSMERG’] = df[‘FIPSMERG’].apply(lambda x: str(0)+str(x) if len(str(x))<5 else x)

Now, each religious group has been categorized into a religious tradition and family which is represented by the columns RELTRAD & FAMILY. I am adding the names of these categories and the State names to make the data more comprehensible.

#religious family name mapping (FAMILY)

rel_fam = pd.read_csv(‘religious_family.csv’)

#religious tradition name mapping (RELTRAD)

rel_trad = pd.read_csv(‘religious_tradition.csv’)

#merging the 2 dataframes on group Code to get the corresponding religious family name & religious tradition catgeory

df = pd.merge(pd.merge(df, rel_fam, left_on=’FAMILY’, right_on=’FAMILY’, how=’left’), rel_trad, left_on=’RELTRAD’, right_on=’RELTRAD’, how=’left’)

print(‘Shape: ‘+str(df.shape)) #state names mapping (STATEAB)

state_code = pd.read_csv(‘us_state_code_data.csv’)

#merging the dataframes

df = (pd.merge(df, state_code, left_on=’STATEAB’, right_on=’Postal

Code’, how=’inner’)).drop([‘Postal Code’], axis = 1)

Filtering the data

For the first part of the analysis, I want to look at the percentage of adherents in 2010 at the state and county level. For this, I need the population and the count of adherents.

#filtering data for only 2010

df_2010 = df[df['YEAR']==2010] #population at county level (year wise)

pop_county = df[[‘YEAR’,’FIPSMERG’,’STATEAB’, ‘State Name’, ‘CNTYNM’, ‘TOTPOP’]].drop_duplicates().reset_index(drop=True) #population at state level (year wise)

pop_state = pop_county.groupby(by=[‘YEAR’,’STATEAB’, ‘State Name’]).agg({‘TOTPOP’:sum}).reset_index()

Summarizing the data

The next step is to calculate the percentage of adherents at the state and county level. Using the pandas groupby function and the population estimates, I have created two dataframes, one summarizing the data at the state level and the other at the county level.

#creating state level data for %of adherents

adh_state = df_2010.groupby(by=['STATEAB','State Name']).agg({'ADHERENT':sum}).reset_index()

adh_state = pd.merge(adh_state, pop_state[['YEAR','STATEAB', 'TOTPOP']], left_on='STATEAB', right_on='STATEAB', how='inner')

adh_state['PER_ADH'] = np.round(adh_state['ADHERENT']/adh_state['TOTPOP']*100,decimals=1)

State-level summary

#creating county level data for %of adherents

adh_county = df_2010.groupby(by=[‘FIPSMERG’, ‘CNTYNM’, ‘STATEAB’]).agg({‘ADHERENT’:sum}).reset_index()

adh_county = pd.merge(adh_county, pop_county[[‘FIPSMERG’, ‘TOTPOP’]], left_on=’FIPSMERG’, right_on=’FIPSMERG’, how=’inner’)

adh_county[‘PER_ADH’] = np.round(adh_county[‘ADHERENT’]/adh_county[‘TOTPOP’]*100,decimals=1)

County-level summary

Choropleth Maps

Now, that we have the dataframes ready for state and county-level, we can plot the Choropleth maps. I am using the function choropleth from plotly.express.

The locations parameter of the choropleth function works in tandem with the locationmode parameter. For US State data, the locationmode = “USA-states”, and the location parameter will take the state code (AL, CA, etc).

#shows %adherents at state level for the year 2010

fig1 = px.choropleth(adh_state, locations=adh_state[‘STATEAB’], locationmode=”USA-states”,

color=’PER_ADH’,color_continuous_scale=”inferno”,

range_color=(0, 100),scope=”usa”,labels={‘PER_ADH’:’%Adherents’},hover_name=’State Name’,

hover_data={‘STATEAB’:False,’State Name’:False,’ADHERENT’:False,’TOTPOP’:False,’PER_ADH’:True}) fig1.update_layout(margin={“r”:0,”t”:0,”l”:0,”b”:0})

fig1.show()

To plot data for counties use the parameter geojson, which takes in a feature collection of polygons. You can find this collection for US counties here. Also, set the locations parameter to the FIPS code.



counties = json.load(response) with urlopen(‘ https://raw.githubusercontent.com/plotly/datasets/master/geojson-counties-fips.json' ) as response:counties = json.load(response) #plot %adherents at county level for the year 2010

fig1 = px.choropleth(adh_county, geojson=counties, locations=’FIPSMERG’, color=’PER_ADH’,color_continuous_scale=”inferno”,

range_color=(0, 100),scope=”usa”,labels={‘PER_ADH’:’%Adherents’},hover_name=’CNTYNM’,

hover_data={‘FIPSMERG’:False,’CNTYNM’:False,’STATEAB’:False,’ADHERENT’:False,

‘TOTPOP’:False,’PER_ADH’:True})

fig1.update_layout(margin={“r”:0,”t”:0,”l”:0,”b”:0})

fig1.show()

So now that we know how to create Choropleth maps, let’s try to add some more functionality to them. For the second part of the analysis, I want to look at how has the largest religious traditional group in each state changed between 1980 and 2010. I will add a slider to my choropleth map, and use that to change the years.

First I need to determine the largest traditional group in each state for each year.

df_ = df.copy() reltrad = df_.groupby([‘YEAR’,’STATEAB’,’State Name’,’RELTRADNM’]).agg({‘ADHERENT’:sum}).reset_index()

reltrad = pd.merge(reltrad, pop_state, left_on=[‘YEAR’,’STATEAB’,’State Name’], right_on=[‘YEAR’,’STATEAB’,’State Name’], how=’inner’)

reltrad[‘PER_ADH’] = (reltrad[‘ADHERENT’]/reltrad[‘TOTPOP’])*100 #adding Ranks and filtering Rank = 1

reltrad[‘RANK’] = reltrad.groupby([‘YEAR’,’STATEAB’])[‘PER_ADH’].rank(ascending=False,method=’first’)

reltrad_top = reltrad[reltrad[‘RANK’]==1].reset_index(drop=True)

To add the slider, use the parameter animation_frame of the function choropleth.

n = [‘Evangelical Protestant ‘,’Mainline Protestant ‘,’Black Protestant ‘,’Catholic ‘,’Orthodox ‘,’Other ‘]

cdm = dict(zip(n, [‘#003f5c’,’#444e86',’#955196',’#dd5182',’#ff6e54',’#ffa600'])) figz = px.choropleth(reltrad_top, locations = ‘STATEAB’, locationmode = “USA-states”, scope = “usa”,

color = ‘RELTRADNM’, hover_name = ‘State Name’,

color_discrete_map = cdm,

hover_data = {‘YEAR’:False,’STATEAB’:False,’State Name’:True,’RELTRADNM’:False,

‘ADHERENT’:False,’TOTPOP’:False,’PER_ADH’:False,’RANK’:False},

animation_frame=’YEAR’) figz.update_layout(margin={“r”:0,”t”:0,”l”:0,”b”:0})

figz[“layout”].pop(“updatemenus”)

figz.show()

I don’t think the slider serves the intended purpose here. It’s cumbersome to compare the states when you have to keep moving between the years. So, I think subplots would serve better.

Now because a choropleth is not referenced to a cartesian system of coordinates, we can’t use the plotly.express’ Choropleth function in subplots. So I will use the choropleth function from plotly.graph_objects here.

ndf = reltrad_top[[‘YEAR’,’STATEAB’,’RELTRADNM’,’State Name’]]

ndf = pd.merge(ndf, df_[[‘RELTRADNM’,’RELTRAD’]].drop_duplicates(), left_on=’RELTRADNM’, right_on=’RELTRADNM’, how=’left’)

ndf[‘text’] = ‘State: ‘ + ndf[‘State Name’] + ‘<br>’ + ‘Tradition: ‘+ ndf[‘RELTRADNM’] years = (ndf[‘YEAR’].sort_values(ascending=True).unique()).tolist() cscale = [‘#003f5c’,’#444e86',’#dd5182',’#ffa600',’#ff6e54',’#955196']

rows = 2

cols = 2

fig = make_subplots(rows=rows, cols=cols,

specs = [[{‘type’: ‘choropleth’} for c in np.arange(cols)] for r in np.arange(rows)],

subplot_titles = years, vertical_spacing=0.1, horizontal_spacing=0) for i, y in enumerate(years):

fig.add_trace(go.Choropleth(locations=ndf.STATEAB[ndf[‘YEAR’]==y],z = ndf.RELTRAD[ndf[‘YEAR’]==y],

locationmode = ‘USA-states’, zmin=1, zmax=6, colorscale=cscale,hoverinfo=’text’,

hovertext=ndf.text[ndf[‘YEAR’]==y]), row = i//cols+1, col = i%cols+1) fig.update_layout(title={‘text’:’Religious Tradition by Year’, ‘xanchor’: ‘center’,’x’:0.5},

**{‘geo’ + str(i) + ‘_scope’: ‘usa’ for i in [‘’] + np.arange(2,rows*cols+1).tolist()},

coloraxis_showscale=False, margin={“r”:10,”t”:70,”l”:10,”b”:0},

hoverlabel=dict(bgcolor=’#e6e6e6', font_size=12, font_family=”Rockwell”)) fig.update_traces(showscale=False)

fig.show()

And, there we have it. Hope this guide can help you in some way. You can find the data and the full source code here.