Lecture 17: Plotting Data with Maps
We have figured out how to plot geographic data using geopandas. We needed shape files loaded into geoDataFrames with one column assigned as the geoDataFrame's geometry. Once that is set up, plotting is easy.
In this notebook we are going to learn how to assign colors to maps based on a variable — a chloropeth. An example of this kind of map are the 2016 voting patterns in Georgia. By the end of this workbook, we will have made a similar map but with 2020 data.
Here's an outline of today's lecture:
Class Announcements
PS4 is on eLC and is due by end-of-day Sunday, October 23rd. Usual office hours via Zoom tomorrow. See syllabus for the zoom link.
1. What is a Choropeth Map? (return home)¶
A choropeth map is one with regions assigned colors based on a variable.
- 'column' is the column name that holds the variable to color by
- 'cmap' is the color scheme. There are many
Let's return to the South American map from the Maps notebook.
import pandas as pd # pandas for data management
import geopandas # geopandas for maps work
import matplotlib.pyplot as plt # matplotlib for plotting details
# geopandas comes with some datasets that define maps.
# Here, we grab a low-resolution Natural Earth map and load it into a GeoDataFrame.
world = geopandas.read_file(geopandas.datasets.get_path('naturalearth_lowres'))
world.sample(5)
pop_est | continent | name | iso_a3 | gdp_md_est | geometry | |
---|---|---|---|---|---|---|
31 | 32510453.0 | South America | Peru | PER | 226848 | POLYGON ((-69.89364 -4.29819, -70.79477 -4.251... |
21 | 5347896.0 | Europe | Norway | NOR | 403336 | MULTIPOLYGON (((15.14282 79.67431, 15.52255 80... |
125 | 2854191.0 | Europe | Albania | ALB | 15279 | POLYGON ((21.02004 40.84273, 20.99999 40.58000... |
67 | 5380508.0 | Africa | Congo | COG | 12267 | POLYGON ((18.45307 3.50439, 18.39379 2.90044, ... |
164 | 6777452.0 | Africa | Libya | LBY | 52091 | POLYGON ((25.00000 22.00000, 25.00000 20.00304... |
The dataset includes measures of population and GDP. Let's create a map where the color of each country depends on GDP per capita.
world['gdp_cap'] = world['gdp_md_est']/world['pop_est']
world['gdp_cap'].describe()
count 177.000000 mean 0.016193 std 0.025676 min 0.000261 25% 0.001817 50% 0.005790 75% 0.017828 max 0.200000 Name: gdp_cap, dtype: float64
These numbers don't make much sense. There must be a scaling issue. This is why we always do sanity checks; ie, we ask "Do the data look like we expect?"
The GDP data are in millions (eg, US GDP is roughly 20 trillion). The population data are not adjusted. (US population is around 350 million).
print('{0:,}'.format(float(world[world['iso_a3']=='USA']['gdp_md_est'])))
21,433,226.0
print('{0:,}'.format(float(world[world['iso_a3']=='USA']['pop_est'])))
328,239,523.0
world['gdp_cap'] = world['gdp_md_est']/world['pop_est']*1000000
print('US GDP per capita is {0:,.0f}'.format(float(world[world['iso_a3']=='USA']['gdp_cap'])))
US GDP per capita is 65,298
That looks better. Let's check it out once more.
world.sort_values('gdp_cap', ascending=False)
pop_est | continent | name | iso_a3 | gdp_md_est | geometry | gdp_cap | |
---|---|---|---|---|---|---|---|
159 | 4490.0 | Antarctica | Antarctica | ATA | 898 | MULTIPOLYGON (((-48.66062 -78.04702, -48.15140... | 200000.000000 |
128 | 619896.0 | Europe | Luxembourg | LUX | 71104 | POLYGON ((6.04307 50.12805, 6.24275 49.90223, ... | 114703.111490 |
23 | 140.0 | Seven seas (open ocean) | Fr. S. Antarctic Lands | ATF | 16 | POLYGON ((68.93500 -48.62500, 69.58000 -48.940... | 114285.714286 |
20 | 3398.0 | South America | Falkland Is. | FLK | 282 | POLYGON ((-61.20000 -51.85000, -60.00000 -51.2... | 82989.994114 |
127 | 8574832.0 | Europe | Switzerland | CHE | 703082 | POLYGON ((9.59423 47.52506, 9.63293 47.34760, ... | 81993.676378 |
... | ... | ... | ... | ... | ... | ... | ... |
66 | 4745185.0 | Africa | Central African Rep. | CAF | 2220 | POLYGON ((27.37423 5.23394, 27.04407 5.12785, ... | 467.842666 |
12 | 10192317.3 | Africa | Somalia | SOM | 4719 | POLYGON ((41.58513 -1.68325, 40.99300 -0.85829... | 462.995790 |
71 | 18628747.0 | Africa | Malawi | MWI | 7666 | POLYGON ((32.75938 -9.23060, 33.73972 -9.41715... | 411.514526 |
154 | 6081196.0 | Africa | Eritrea | ERI | 2065 | POLYGON ((36.42951 14.42211, 36.32322 14.82249... | 339.571361 |
75 | 11530580.0 | Africa | Burundi | BDI | 3012 | POLYGON ((30.46967 -2.41385, 30.52766 -2.80762... | 261.218430 |
177 rows × 7 columns
The data for small places is crazy. For example, Antarctica's GDP per capita doesn't really make sense. Let's only plot countries with populations greater than 50,000. I'm sorry to do this to you, Falkland Islands. BTW Who owns the Falkland Islands anyways (story)?
world[world['pop_est']<50000]
pop_est | continent | name | iso_a3 | gdp_md_est | geometry | gdp_cap | |
---|---|---|---|---|---|---|---|
20 | 3398.0 | South America | Falkland Is. | FLK | 282 | POLYGON ((-61.20000 -51.85000, -60.00000 -51.2... | 82989.994114 |
23 | 140.0 | Seven seas (open ocean) | Fr. S. Antarctic Lands | ATF | 16 | POLYGON ((68.93500 -48.62500, 69.58000 -48.940... | 114285.714286 |
159 | 4490.0 | Antarctica | Antarctica | ATA | 898 | MULTIPOLYGON (((-48.66062 -78.04702, -48.15140... | 200000.000000 |
fig, gax = plt.subplots(figsize=(20,10))
world[world['pop_est']>50000].plot(ax = gax, column='gdp_cap', edgecolor='black', cmap = 'Reds', legend=False)
plt.axis('off')
plt.show()
That legend is atrocious. Never trust the defaults. Unfortunately, we are going to have to dig into the guts of matplotlib to fix it. It's a good thing we are already experienced with matplotlib.
We are going to add a separate axis to our plot. Then we will tell the geopandas plot command to add the legend to the new axis.
from mpl_toolkits.axes_grid1 import make_axes_locatable
fig, gax = plt.subplots(figsize=(20,10))
# Adapted from https://geopandas.org/mapping.html
# https://matplotlib.org/3.1.1/gallery/axes_grid1/demo_colorbar_with_axes_divider.html
# Create an axis divider object
divider = make_axes_locatable(gax)
# Add a second axis to the right of the original axes.
# Make the new axes 5% of the original and add some padding between them.
legend_ax = divider.append_axes('right', size='5%', pad=0.1)
world[world['pop_est']>50000].plot(
ax = gax, column='gdp_cap', # ax is the main axes
edgecolor='black', cmap = 'Reds',
legend=True, cax=legend_ax) # cax is the legend axes
# Here I ony turn off the axis of the main plot
gax.axis('off')
plt.show()
You can experiment with different padding, widths, locations, and color maps.
2. Georgia Presidential Returns (return home)¶
Let's make the map we ended with last lecture much cooler by introducing colors to reflect voting patterns in the 2020 Presidential election.
The steps:
A. Merge data on votes with geographical data.
B. ~~Plot the borders ~~. We already did this last lecture so this part will be cake.
C. Color the map using vote shares.
A. Get the vote totals and merge them with the DataFrame containing geographical information¶
Time to add the voter totals. I downloaded the results from the Georgia Office of the Secretary of State (link). I would have called him on the phone to request a favor but...
I saved the download as GAvoting_2000.xls
. The spreadsheet actually has all of the November 3, 2020 election results but we're just interested in the Presidential election which is on sheet '1'.
There's some work we need to do before we can create a choropleth with these data.
Step 1. Load 'GAvoting_2000.xls' to a DataFrame named df
. Note, this is not a GeoDataFrame, this data doesn't have a geometery to it, it just has county names and vote counts.
df = pd.read_excel('./Data/GAvoting_2000.xls',sheet_name='1',skiprows=[0,1])
df.head()
County | Trump: Election Day Votes | Trump: Absentee by Mail Votes | Trump: Advanced Voting Votes | Trump: Provisional Votes | Trump: Total Votes | Biden: Election Day Votes | Biden: Absentee by Mail Votes | Biden: Advanced Voting Votes | Biden: Provisional Votes | Biden: Total Votes | Jorgenseon: Election Day Votes | Jorgenseon: Absentee by Mail Votes | Jorgenseon: Advanced Voting Votes | Jorgenseon: Provisional Votes | Jorgenseon: Total Votes | Total | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | APPLING | 1753 | 890 | 3874 | 9 | 6526 | 334 | 587 | 855 | 3 | 1779 | 5 | 5 | 26 | 0 | 36 | 8341 |
1 | ATKINSON | 716 | 164 | 1419 | 1 | 2300 | 250 | 130 | 445 | 0 | 825 | 14 | 3 | 13 | 0 | 30 | 3155 |
2 | BACON | 431 | 487 | 3099 | 1 | 4018 | 140 | 196 | 288 | 1 | 625 | 8 | 4 | 13 | 0 | 25 | 4668 |
3 | BAKER | 291 | 138 | 466 | 2 | 897 | 149 | 234 | 269 | 0 | 652 | 2 | 2 | 2 | 0 | 6 | 1555 |
4 | BALDWIN | 1873 | 1290 | 5736 | 4 | 8903 | 1527 | 3000 | 4612 | 1 | 9140 | 63 | 38 | 107 | 0 | 208 | 18251 |
Step 2. Create a variable called 'trump_share' that is the share of trump votes out of the total vote count.
df['Trump: Share'] = 100*df['Trump: Total Votes']/df['Total']
df['Biden: Share'] = 100*df['Biden: Total Votes']/df['Total']
df['Jorgenseon: Share'] = 100*df['Jorgenseon: Total Votes']/df['Total']
Step 3. Use melt
' to convert the data from wide to long. Call the new df results
. Use split
and partition
to create a field called 'candidate' and 'category'.
results = pd.melt(df, id_vars=['County'], value_vars=['Trump: Election Day Votes',
'Trump: Absentee by Mail Votes',
'Trump: Advanced Voting Votes',
'Trump: Provisional Votes',
'Trump: Total Votes',
'Trump: Share',
'Biden: Election Day Votes',
'Biden: Absentee by Mail Votes',
'Biden: Advanced Voting Votes',
'Biden: Provisional Votes',
'Biden: Total Votes',
'Biden: Share',
'Jorgenseon: Election Day Votes',
'Jorgenseon: Absentee by Mail Votes',
'Jorgenseon: Advanced Voting Votes',
'Jorgenseon: Provisional Votes',
'Jorgenseon: Total Votes',
'Jorgenseon: Share'
])
results['candidate'] = results['variable'].str.partition(':')[0].str.strip()
results['category'] = results['variable'].str.partition(':')[2].str.strip()
results.drop(columns='variable', inplace=True)
results.rename(columns={'value':'count'},inplace=True)
results.head()
County | count | candidate | category | |
---|---|---|---|---|
0 | APPLING | 1753.0 | Trump | Election Day Votes |
1 | ATKINSON | 716.0 | Trump | Election Day Votes |
2 | BACON | 431.0 | Trump | Election Day Votes |
3 | BAKER | 291.0 | Trump | Election Day Votes |
4 | BALDWIN | 1873.0 | Trump | Election Day Votes |
The county names in the map data are in title case. The county names in the vote data are in all caps. We know how to fix this up.
Step 4. Convert the county names in results
to title case.
Step 5 Strip the whitespace out of the county names in results
, too. (Trust me, there is some extra space in some of the county names...) Try str.strip
.
results['County'] = results['County'].str.title().str.strip()
results.head(5)
County | count | candidate | category | |
---|---|---|---|---|
0 | Appling | 1753.0 | Trump | Election Day Votes |
1 | Atkinson | 716.0 | Trump | Election Day Votes |
2 | Bacon | 431.0 | Trump | Election Day Votes |
3 | Baker | 291.0 | Trump | Election Day Votes |
4 | Baldwin | 1873.0 | Trump | Election Day Votes |
Step 6. Load map data. Strip any whitespace from the NAME variable in the ga_counties
GeoDataFrame.
Step 7. Convert the NAME variable to title case in the ga_counties
GeoDataFrame.
counties = geopandas.read_file('./Data/cb_2017_us_county_5m/cb_2017_us_county_5m.shp')
counties['NAME'] = counties['NAME'].str.title().str.strip()
# GA fips code is 13.
ga_counties = counties[counties['STATEFP']=='13']
# Change the projection to Mercador
ga_counties = ga_counties.to_crs({'init': 'epsg:3395'})
ga_counties.head()
C:\Users\jt83241\Anaconda3\envs\geo_env\lib\site-packages\pyproj\crs\crs.py:141: FutureWarning: '+init=<authority>:<code>' syntax is deprecated. '<authority>:<code>' is the preferred initialization method. When making the change, be mindful of axis order changes: https://pyproj4.github.io/pyproj/stable/gotchas.html#axis-order-changes-in-proj-6 in_crs_string = _prepare_from_proj_string(in_crs_string)
STATEFP | COUNTYFP | COUNTYNS | AFFGEOID | GEOID | NAME | LSAD | ALAND | AWATER | geometry | |
---|---|---|---|---|---|---|---|---|---|---|
8 | 13 | 143 | 00350637 | 0500000US13143 | 13143 | Haralson | 06 | 730804590 | 2616530 | POLYGON ((-9505182.817 3991780.817, -9485844.5... |
9 | 13 | 023 | 00347451 | 0500000US13023 | 13023 | Bleckley | 06 | 559100209 | 8447343 | POLYGON ((-9293411.509 3793795.634, -9293245.8... |
31 | 13 | 261 | 00343504 | 0500000US13261 | 13261 | Sumter | 06 | 1250167780 | 25799665 | POLYGON ((-9400241.039 3736618.850, -9399040.1... |
34 | 13 | 199 | 00346892 | 0500000US13199 | 13199 | Meriwether | 06 | 1298162528 | 10832504 | POLYGON ((-9447029.178 3871454.062, -9446834.5... |
90 | 13 | 097 | 01686467 | 0500000US13097 | 13097 | Douglas | 06 | 518261551 | 2517004 | POLYGON ((-9451284.254 3968959.296, -9451212.6... |
Step 8. Merge ga_counties
and results
.
ga_votes = pd.merge(left=ga_counties, right=results, left_on='NAME', right_on='County', how='right')
ga_votes.sample(5)
STATEFP | COUNTYFP | COUNTYNS | AFFGEOID | GEOID | NAME | LSAD | ALAND | AWATER | geometry | County | count | candidate | category | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
134 | 13 | 273 | 00352238 | 0500000US13273 | 13273 | Terrell | 06 | 8.687853e+08 | 5861795.0 | POLYGON ((-9417603.986 3730246.439, -9401299.4... | Terrell | 665.000000 | Trump | Election Day Votes |
924 | 13 | 253 | 00351263 | 0500000US13253 | 13253 | Seminole | 06 | 6.092453e+08 | 55231610.0 | POLYGON ((-9464781.206 3620506.319, -9453520.8... | Seminole | 67.224511 | Trump | Share |
330 | 13 | 021 | 01672039 | 0500000US13021 | 13021 | Bibb | 06 | 6.456998e+08 | 14503560.0 | POLYGON ((-9338806.039 3852017.169, -9336672.0... | Bibb | 13234.000000 | Trump | Advanced Voting Votes |
691 | 13 | 105 | 00347828 | 0500000US13105 | 13105 | Elbert | 06 | 9.093037e+08 | 59911773.0 | POLYGON ((-9250147.745 4032005.602, -9248201.1... | Elbert | 6226.000000 | Trump | Total Votes |
2628 | 13 | 139 | 01686953 | 0500000US13139 | 13139 | Hall | 06 | 1.017805e+09 | 94061469.0 | POLYGON ((-9357252.792 4029432.015, -9354255.6... | Hall | 1322.000000 | Jorgenseon | Total Votes |
B. Make the Borders¶
The code below is just recycled from the previous lecture.
fig, gax = plt.subplots(figsize=(10,10))
# Plot the counties
ga_counties.plot(ax=gax, edgecolor='black', color = 'white')
plt.axis('off')
plt.show()
C. Color the map¶
Create a choropeth map with colors that correspond to Trump's share of the vote.
Remember the extra arguments.
column
is set to the column name of the data we want to be 'colored'cmap
determines the color scheme. I am using red colors.legend
turn on the legend
fig, gax = plt.subplots(1, 2, figsize = (20,10))
# TRUMP MAP
ga_votes[(ga_votes['candidate']=='Trump')&(ga_votes['category']=='Share')].plot(ax=gax[0], edgecolor='black', column='count', legend=True, cmap='Reds', scheme='quantiles')
gax[0].set_title('Trump Vote Share',fontsize=18)
gax[0].get_legend().set_title('')
gax[0].get_legend().draw_frame(False)
# I don't want the axis with long and lat
gax[0].axis('off')
# BIDEN MAP
ga_votes[(ga_votes['candidate']=='Biden')&(ga_votes['category']=='Share')].plot(ax=gax[1], edgecolor='black', column='count', legend=True, cmap='Blues', scheme='quantiles')
gax[1].set_title('Biden Vote Share',fontsize=18)
gax[1].get_legend().set_title('')
gax[1].get_legend().draw_frame(False)
# I don't want the axis with long and lat
gax[1].axis('off')
fig.subplots_adjust(hspace=0.0, wspace=0.0)
plt.show()
- Print out, in a full sentence, the share of votes for Trump in a county of your choice. Express the shares with 3 decimal places.
#county_name = 'Clarke'
county_name = 'Gwinnett'
#county_name = 'Fulton'
tval = ga_votes.loc[(ga_votes['County']==county_name)&(ga_votes['candidate']=='Trump')&(ga_votes['category']=='Share'),'count'].mean()
bval = ga_votes.loc[(ga_votes['County']==county_name)&(ga_votes['candidate']=='Biden')&(ga_votes['category']=='Share'),'count'].mean()
# Print Results. I'm using the f-string approach to printing the results. This is the cool new way!
print(f'The share of votes attained by Donald J. Trump in {county_name} county was {tval:.3f} percent.')
print(f'The share of votes attained by Joseph R. Biden in {county_name} county was {bval:.3f} percent.')
The share of votes attained by Donald J. Trump in Gwinnett county was 40.209 percent. The share of votes attained by Joseph R. Biden in Gwinnett county was 58.431 percent.
Practice ¶
- What percent of total votes were cast before election-day, on election day, and via absentee?
df = ga_votes[ga_votes['category']!='Share'].groupby('category',as_index=False)['count'].sum()
df['Share'] = df['count']/df.loc[df['category']=='Total Votes','count'].values
df.set_index('category',inplace=True)
val = 100*df.loc['Advanced Voting Votes','Share']
print(f'Votes BEFORE election day: {val:.3f} percent of total.')
val = 100*df.loc['Election Day Votes','Share']
print(f'Votes ON election day: {val:.3f} percent of total.')
val = 100*df.loc['Absentee by Mail Votes','Share']
print(f'ABSENTEE votes: {val:.3f} percent of total.')
Votes BEFORE election day: 53.914 percent of total. Votes ON election day: 19.517 percent of total. ABSENTEE votes: 26.347 percent of total.
- What share of before election-day, on election day, and via absentee voting did Biden get?
df = ga_votes[ga_votes['category']!='Share'].groupby(['candidate','category'],as_index=False)['count'].sum()
df['Total'] = df.groupby(['category'])['count'].transform('sum')
df['Share']= 100*df['count']/df['Total']
val = df.loc[(df['candidate']=='Biden')&(df['category']=='Advanced Voting Votes'),'Share'].values[0]
print(f'Biden vote share BEFORE election day: {val:.1f} percent of total.')
val = df.loc[(df['candidate']=='Biden')&(df['category']=='Election Day Votes'),'Share'].values[0]
print(f'Biden vote share ON election day: {val:.3f} percent of total.')
val = df.loc[(df['candidate']=='Biden')&(df['category']=='Absentee by Mail Votes'),'Share'].values[0]
print(f'Biden vote share of ABSENTEE votes: {val:.3f} percent of total.')
Biden vote share BEFORE election day: 46.4 percent of total. Biden vote share ON election day: 37.641 percent of total. Biden vote share of ABSENTEE votes: 64.523 percent of total.
- What share of before election-day, on election day, and via absentee voting did Trump get?
val = df.loc[(df['candidate']=='Trump')&(df['category']=='Advanced Voting Votes'),'Share'].values[0]
print(f'Trump vote share BEFORE election day: {val:.1f} percent of total.')
val = df.loc[(df['candidate']=='Trump')&(df['category']=='Election Day Votes'),'Share'].values[0]
print(f'Trump vote share ON election day: {val:.3f} percent of total.')
val = df.loc[(df['candidate']=='Trump')&(df['category']=='Absentee by Mail Votes'),'Share'].values[0]
print(f'Trump vote share of ABSENTEE votes: {val:.3f} percent of total.')
Trump vote share BEFORE election day: 52.7 percent of total. Trump vote share ON election day: 60.243 percent of total. Trump vote share of ABSENTEE votes: 34.258 percent of total.
- Draw a four-by-four table of chloropleth maps where colums are election-day and mail-in votes while rows correspond to Trump and Biden. Comment on the results.
1.175394
# In the code below I set the maximum value for the color scale so the color schemes would be consistent across the plots.
# I also removed the legend and the quantiles argument.
fig, gax = plt.subplots(2, 2, figsize = (10,10))
maxval = ga_votes.loc[(ga_votes['category']!='Total Votes')&(ga_votes['County']!='Total:'),'count'].max()/2
#----------------------------------------------------------
# Election Day
#----------------------------------------------------------
# TRUMP MAP
ga_votes[(ga_votes['candidate']=='Trump')&(ga_votes['category']=='Election Day Votes')].plot(ax=gax[0,0],
edgecolor='black',
column='count',
legend=False,
cmap='Reds',
vmin=0,
vmax=maxval)
val = ga_votes.loc[(ga_votes['candidate']=='Trump')&(ga_votes['category']=='Election Day Votes'),'count'].sum()/1e+6
gax[0,0].set_title(f'Trump Election Day Votes\n({val:3.2f} Million)',fontsize=18)
# gax[0,0].get_legend().set_title('')
# gax[0,0].get_legend().draw_frame(False)
# I don't want the axis with long and lat
gax[0,0].axis('off')
# BIDEN MAP
ga_votes[(ga_votes['candidate']=='Biden')&(ga_votes['category']=='Election Day Votes')].plot(ax=gax[0,1],
edgecolor='black',
column='count',
legend=False,
cmap='Blues',
vmin=0,
vmax=maxval)
val = ga_votes.loc[(ga_votes['candidate']=='Biden')&(ga_votes['category']=='Election Day Votes'),'count'].sum()/1e+6
gax[0,1].set_title(f'Biden Election Day Votes\n({val:3.2f} Million)',fontsize=18)
# gax[0,1].get_legend().set_title('')
# gax[0,1].get_legend().draw_frame(False)
# I don't want the axis with long and lat
gax[0,1].axis('off')
#----------------------------------------------------------
# Absentee
#----------------------------------------------------------
# TRUMP MAP
ga_votes[(ga_votes['candidate']=='Trump')&(ga_votes['category']=='Absentee by Mail Votes')].plot(ax=gax[1,0],
edgecolor='black',
column='count',
legend=False,
cmap='Reds',
vmin=0,
vmax=maxval)
val = ga_votes.loc[(ga_votes['candidate']=='Trump')&(ga_votes['category']=='Absentee by Mail Votes'),'count'].sum()/1e+6
gax[1,0].set_title(f'Trump Absentee Votes\n({val:3.2f} Million)',fontsize=18)
# gax[1,0].get_legend().set_title('')
# gax[1,0].get_legend().draw_frame(False)
# I don't want the axis with long and lat
gax[1,0].axis('off')
# BIDEN MAP
ga_votes[(ga_votes['candidate']=='Biden')&(ga_votes['category']=='Absentee by Mail Votes')].plot(ax=gax[1,1],
edgecolor='black',
column='count',
legend=False,
cmap='Blues',
vmin=0,
vmax=maxval)
val = ga_votes.loc[(ga_votes['candidate']=='Biden')&(ga_votes['category']=='Absentee by Mail Votes'),'count'].sum()/1e+6
gax[1,1].set_title(f'Biden Absentee Votes\n({val:3.2f} Million)',fontsize=18)
# gax[1,1].get_legend().set_title('')
# gax[1,1].get_legend().draw_frame(False)
# I don't want the axis with long and lat
gax[1,1].axis('off')
# fig.subplots_adjust(hspace=0.0, wspace=0.0)
plt.show()