Mapping smoking rates by state with Matplotlib and geopandas

5 hours ago 1

# these could be function parameters, but I'm not going to pretend that this code is that reusable.

gdf = us_states_data

variable = 'smoking_rate'

colormap = 'YlOrBr'

color_bar_label = f'cigarette smoking rate,\n% of adults'

# main plot title

plot_title = 'Adult Cigarette Smoking Rates by US State (2022)'

subtitle = '% of adults >= 18 who smoked >= 1 cigarette in last 30 days & >= 100 cigarettes in lifetime'

subtitle_font_size = 16

title_font_size = 30

source_notes = 'Source: American Lung Association,\n CDC Behavioral Risk Factor Surveillance System'

note_font_size = 16

# color of state borders and arrows between small state labels and their state centroids.

edgecolor = '0.2'

# properties for state & state-data labels

label_alpha = 0.9

label_size = 14

# function to get label text for state & state-data labels (centralize config)

def get_label_text(row):

usps = row['usps']

value = row[variable]

return f"{usps}\n{value:.1%}"

# Apply binned colors

gdf = makeColorColumn(gdf, variable, colormap)

# Join USPS abbreviations with gdf using state_to_abbr

gdf = gdf.merge(state_to_abbr, left_on='state', right_on='state', how='left')

# Separate Alaska, Hawaii, and mainland states

mainland = gdf[~gdf['state'].isin(['Alaska', 'Hawaii'])].copy()

# Reproject mainland states to Albers Equal Area projection

mainland = mainland.to_crs('EPSG:2163')

# create figure and axes for with Matplotlib for main map

fig, ax = plt.subplots(1, figsize=(18, 14))

# remove the axis box from the main map

ax.axis('off')

# TITLE CONFIGURATION (this is always going to require some manual tweaking)

text = ax.text(0.01, 0.98, subtitle, fontsize=subtitle_font_size, transform=ax.transAxes)

ex = text.get_window_extent()

x, y = text.get_position()

t = transforms.offset_copy(text._transform, y=ex.height + 10, units='dots')

ax.text(x, y, plot_title, fontsize=title_font_size, fontweight='bold', transform=t)

# add colorbar axes to the figure

# This will take some iterating to get it where you want it [l,b,w,h] right

# l:left, b:bottom, w:width, h:height; in normalized unit (0-1)

cbax = fig.add_axes([0.92, 0.24, 0.03, 0.31])

# place color bar label on bottom

cbax.text(0.5, -0.15, color_bar_label, fontdict={'fontsize': '15', 'fontweight' : 'bold'}, transform=cbax.transAxes, ha='center')

# Create a continuous colormap

base_cmap = mpl.colormaps.get_cmap(colormap)

vmin = gdf[variable].min()

vmax = gdf[variable].max()

norm = mpl.colors.Normalize(vmin=vmin, vmax=vmax)

sm = plt.cm.ScalarMappable(cmap=base_cmap, norm=norm)

sm._A = []

# Create colorbar with custom formatting for percentages

cbar_fmt = FuncFormatter(lambda x, p: format(x, '.0%'))

cbar = fig.colorbar(sm, cax=cbax, format=cbar_fmt, spacing='proportional')

tick_font_size = 15

cbax.tick_params(labelsize=tick_font_size)

# plot mainland states

mainland.plot(color=mainland['c_hex'], linewidth=0.8, ax=ax, edgecolor=edgecolor)

# Define small states that need external labels with arrows

# Offsets are in projected coordinate units (meters for EPSG:2163)

# NOTE: all position adjustments are highly style-dependent.

# Any changes to font/size will require calibration.

small_states = {

'Vermont': {'offset': (-250000, 200000), 'ha': 'right'},

'New Hampshire': {'offset': (-70000, 270000), 'ha': 'right'},

'Massachusetts': {'offset': (200000, 180000), 'ha': 'left'},

'Rhode Island': {'offset': (390000, 30000), 'ha': 'left'},

'Connecticut': {'offset': (200000, -60000), 'ha': 'left'},

'New Jersey': {'offset': (120000, -70000), 'ha': 'left'},

'Delaware': {'offset': (180000, -140000), 'ha': 'left'},

'Maryland': {'offset': (280000, -330000), 'ha': 'left'},

'District of Columbia': {'offset': (590000, 15000), 'ha': 'left'},

}

# States that need custom positioning without arrows

custom_positioned_states = {

'Florida': {'offset': (90000, 0), 'ha': 'center'}, # Move north to center in peninsula

'Louisiana': {'offset': (-50000, 0), 'ha': 'center'},

'Michigan': {'offset': (50000, -50000), 'ha': 'center'},

'West Virginia': {'offset': (-10000, 0), 'ha': 'center'},

}

# Add labels to mainland states

for idx, row in mainland.iterrows():

state_name = row['state']

usps = row['usps']

value = row[variable]

x, y = row['geometry'].centroid.x, row['geometry'].centroid.y

bg_color = row['c_hex']

text_color = get_text_color(bg_color)

label = get_label_text(row)

if state_name in small_states:

# Use annotate with arrow for small states

# Calculate absolute position for text

config = small_states[state_name]

text_x = x + config['offset'][0]

text_y = y + config['offset'][1]

ax.annotate(label, xy=(x, y), xytext=(text_x, text_y),

xycoords='data', textcoords='data',

fontsize=label_size, ha=config['ha'], va='center', color=text_color, weight='bold',

bbox=dict(boxstyle='round,pad=0.4', facecolor=bg_color, edgecolor='gray', alpha=label_alpha), arrowprops=dict(arrowstyle='-', color=edgecolor, lw=1.2))

elif state_name in custom_positioned_states:

# Custom positioned labels without arrows or bounding boxes

config = custom_positioned_states[state_name]

label_x = x + config['offset'][0]

label_y = y + config['offset'][1]

ax.text(label_x, label_y, label, fontsize=label_size, ha=config['ha'], va='center',

color=text_color, weight='bold')

else:

# Place label at centroid for larger states (no arrow or bounding box)

ax.text(x, y, label, fontsize=label_size, ha='center', va='center', color=text_color, weight='bold',)

# Add Alaska Axis: (x, y, width, height) inside the main map

akax = fig.add_axes([0.1, 0.17, 0.2, 0.19])

akax.axis('off')

# Need to clip before projecting, then project

alaska = gdf[gdf['state'] == 'Alaska'].copy()

# polygon to clip western islands (in WGS84 coordinates)

ak_polygon = Polygon([(-170,50),(-170,72),(-140, 72),(-140,50)])

alaska = alaska.clip(ak_polygon)

# Now project the clipped version for display

alaska = alaska.to_crs('EPSG:3338')

alaska.plot(color=alaska['c_hex'].iloc[0], linewidth=0.8, ax=akax, edgecolor=edgecolor)

# Add Alaska label using centroid from projected clipped geometry

ak_centroid = alaska.geometry.centroid.iloc[0]

ak_bg_color = alaska['c_hex'].iloc[0]

ak_text_color = get_text_color(ak_bg_color)

ak_label = get_label_text(alaska.iloc[0])

# no need for bounding box around label, AK is large enough to be visible

akax.text(ak_centroid.x, ak_centroid.y, ak_label, fontsize=label_size, ha='center', va='center',

color=ak_text_color, weight='bold')

# Add Hawaii Axis(x, y, width, height)

hiax = fig.add_axes([.28, 0.20, 0.1, 0.1])

hiax.axis('off')

# Need to clip before projecting, then project

hawaii = gdf[gdf['state'] == 'Hawaii'].copy()

# clipping polygon for Hawaii (in WGS84 coordinates)

hi_polygon = Polygon([(-160,0),(-160,90),(-120,90),(-120,0)])

hawaii = hawaii.clip(hi_polygon)

# Now project the clipped version for display

hawaii = hawaii.to_crs('EPSG:32604')

_ = hawaii.plot(color=hawaii['c_hex'].iloc[0], linewidth=0.8, ax=hiax, edgecolor=edgecolor)

# Add Hawaii label - position to the right of the Big Island

# Get bounds to position label to the right

hi_bounds = hawaii.geometry.bounds.iloc[0]

hi_label_x = hi_bounds['maxx'] + 50000 # 50km to the right of the Big Island

hi_label_y = (hi_bounds['miny'] + hi_bounds['maxy']) / 2 - 75000

hi_bg_color = hawaii['c_hex'].iloc[0]

hi_text_color = get_text_color(hi_bg_color)

hi_label = get_label_text(hawaii.iloc[0])

_ = hiax.text(hi_label_x, hi_label_y, hi_label, fontsize=label_size, ha='left', va='center',

color=hi_text_color, weight='bold',

bbox=dict(boxstyle='round,pad=0.4', facecolor=hi_bg_color, edgecolor='gray', alpha=label_alpha))

# SOURCE NOTES (will always require some manual tweaking)

text = ax.text(0.01, -0.07, source_notes, fontsize=note_font_size, transform=ax.transAxes)

# website link

ax.text(0.85, -0.05, 'aaronjbecker.com', fontweight='bold', fontsize=title_font_size, transform=ax.transAxes)

plt.show()

Read Entire Article