I just solved this problem for my own project. I rewrote your code and split it up with comments.
No changes here, except adding a couple common packages and maybe the location of some code.
import plotly.graph_objects as go
from plotly.subplots import make_subplots
import pandas as pd
import numpy as np
df_shootings = pd.read_csv('https://raw.githubusercontent.com/washingtonpost/data-police-shootings/master/fatal-police-shootings-data.csv')
state_count = df_shootings.groupby(['state', 'race']).size().reset_index(name='total')
races = pd.DataFrame({'W': 'White, non-Hispanic',
'B': 'Black, non-Hispanic',
'A': 'Asian',
'N': 'Native American',
'H': 'Hispanic'}, index=[0])
Declare a figure of subplots. You need to specify the number of rows and columns, you need to prepare the subplots to receive choropleths specifically, and we'll use your races
dataframe to declare your subplot titles.
rows = 2
cols = 3
fig = make_subplots(
rows=rows, cols=cols,
specs = [[{'type': 'choropleth'} for c in np.arange(cols)] for r in np.arange(rows)],
subplot_titles = list(races.loc[0,:]))
I use enumerate
in order to calculate the correct row and column of each subplot graph. I don't know if this is pythonic. The value i
is just counting from 0 as we go through races
. Each time through the loop we're going to add a trace to the appropriate row and column. In order to make sure that we use the same colorscale for each subplot, we set zmin
and zmax
to 0 and the maximum state_count['total']
respectively. The calculations for row
and col
use the count from enumerate
to determine which row and column we graph this in.
for i, race in enumerate(races):
result = state_count[['state', 'total']][state_count.race == race]
fig.add_trace(go.Choropleth(
locations=result.state,
z = result.total,
locationmode = 'USA-states', # set of locations match entries in `locations`
marker_line_color='white',
zmin = 0,
zmax = max(state_count['total']),
colorbar_title = "Shooting deaths",
), row = i//cols+1, col = i%cols+1)
I made up what I thought was an appropriate title for the entire figure. The next line was a heck of a trick for me, but that's what sets each subplot to a plot of the USA instead of the default map of the entire world. Without it, only the first subplot would use the USA map. I'll give you my best understanding of what was going on, and I couldn't find or figure out a better solution. Basically, each subplot has a kinda sorta location that are named, in order, geo
, geo2
, geo3
, geo4
, etc. Each one has to have its scope set to usa
. 'Magic' underscores didn't work, so I had to put together a **kwargs (keyword argument) that was equivalent to geo_scope = 'usa', geo2_scope = 'usa', geo3_scope = 'usa', geo4_scope = 'usa', geo5_scope = 'usa'
. Granted that wasn't so bad to type out, but in my project, I had 50 subplots, so I coded it. Basically I made a dictionary and then converted it to kwargs with **. The fact that the keyword argument list starts with geo
instead of geo0
or even geo1
is why that line is as complicated as it is.
fig.update_layout(
title_text = 'Shooting Deaths by Race',
**{'geo' + str(i) + '_scope': 'usa' for i in [''] + np.arange(2,rows*cols+1).tolist()},
)
for index, trace in enumerate(fig.data):
fig.data[index].hovertemplate = 'State: %{location}<br>Shooting deaths: %{z:.2f}
<extra></extra>'
fig.show()
And here's the result:
Shooting Deaths by Race