Choropleth Maps using Plotly | Towards Data Science

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.
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 notebookimport pandas as pdimport numpy as npimport matplotlib.pyplot as pltimport plotly.express as pximport plotly.graph_objects as gofrom plotly.subplots import make_subplots#import raw datadf = 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.
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 codedf[‘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 catgeorydf = 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 dataframesdf = (pd.merge(df, state_code, left_on=’STATEAB’, right_on=’Postal Code’, how=’inner’)).drop([‘Postal Code’], axis = 1)
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()
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 adherentsadh_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)
#creating county level data for %of adherentsadh_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)
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 2010fig1 = 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.
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 2010fig1 = 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 = 1reltrad[‘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 = 2cols = 2fig = 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. You can also hop on to Neptune.ai’s Plotly Python tutorial here for more tips and tricks.
PS: Use this only as a guide to Plotly and Choropleth Maps. Do not take the data very seriously. The source of the data is based on surveys conducted by the ARDA and hence may miss smaller religious groups especially in the years 1980 and 1990.
PPS: First-time writer, so please excuse my mistakes. Constructive feedback is welcome. Thanks in advance.