Plotting election (and other county-level) data with Python Basemap
After my arduous search for open 2016 election data by county, as a first test I wanted one of those red-blue-purple charts of how Democratic or Republican each county's vote was.
I used the Basemap package for plotting. It used to be part of matplotlib, but it's been split off into its own toolkit, grouped under mpl_toolkits: on Debian, it's available as python-mpltoolkits.basemap, or you can find Basemap on GitHub.
It's easiest to start with the fillstates.py example that shows how to draw a US map with different states colored differently. You'll need the three shapefiles (because of ESRI's silly shapefile format): st99_d00.dbf, st99_d00.shp and st99_d00.shx, available in the same examples directory.
Of course, to plot counties, you need county shapefiles as well. The US Census has county shapefiles at several different resolutions (I used the 500k version). Then you can plot state and counties outlines like this:
from mpl_toolkits.basemap import Basemap import matplotlib.pyplot as plt def draw_us_map(): # Set the lower left and upper right limits of the bounding box: lllon = -119 urlon = -64 lllat = 22.0 urlat = 50.5 # and calculate a centerpoint, needed for the projection: centerlon = float(lllon + urlon) / 2.0 centerlat = float(lllat + urlat) / 2.0 m = Basemap(resolution='i', # crude, low, intermediate, high, full llcrnrlon = lllon, urcrnrlon = urlon, lon_0 = centerlon, llcrnrlat = lllat, urcrnrlat = urlat, lat_0 = centerlat, projection='tmerc') # Read state boundaries. shp_info = m.readshapefile('st99_d00', 'states', drawbounds=True, color='lightgrey') # Read county boundaries shp_info = m.readshapefile('cb_2015_us_county_500k', 'counties', drawbounds=True) if __name__ == "__main__": draw_us_map() plt.title('US Counties') # Get rid of some of the extraneous whitespace matplotlib loves to use. plt.tight_layout(pad=0, w_pad=0, h_pad=0) plt.show()
Accessing the state and county data after reading shapefiles
Great. Now that we've plotted all the states and counties, how do we get a list of them, so that when I read out "Santa Clara, CA" from the data I'm trying to plot, I know which map object to color?
After calling readshapefile('st99_d00', 'states'), m has two new members, both lists: m.states and m.states_info.
m.states_info[] is a list of dicts mirroring what was in the shapefile. For the Census state list, the useful keys are NAME, AREA, and PERIMETER. There's also STATE, which is an integer (not restricted to 1 through 50) but I'll get to that.
If you want the shape for, say, California,
iterate through m.states_info[] looking for the one where
m.states_info[i]["NAME"] == "California"
.
Note i; the shape coordinates will be in m.states[i]
n
(in basemap map coordinates, not latitude/longitude).
Correlating states and counties in Census shapefiles
County data is similar, with county names in
m.counties_info[i]["NAME"]
.
Remember that STATE integer? Each county has a STATEFP,
m.counties_info[i]["STATEFP"]
that matches some state's
m.states_info[i]["STATE"]
.
But doing that search every time would be slow. So right after calling readshapefile for the states, I make a table of states. Empirically, STATE in the state list goes up to 72. Why 72? Shrug.
MAXSTATEFP = 73 states = [None] * MAXSTATEFP for state in m.states_info: statefp = int(state["STATE"]) # Many states have multiple entries in m.states (because of islands). # Only add it once. if not states[statefp]: states[statefp] = state["NAME"]
That'll make it easy to look up a county's state name quickly when we're looping through all the counties.
Calculating colors for each county
Time to figure out the colors from the Deleetdk election results CSV file. Reading lines from the CSV file into a dictionary is superficially easy enough:
fp = open("tidy_data.csv") reader = csv.DictReader(fp) # Make a dictionary of all "county, state" and their colors. county_colors = {} for county in reader: # What color is this county? pop = float(county["votes"]) blue = float(county["results.clintonh"])/pop red = float(county["Total.Population"])/pop county_colors["%s, %s" % (county["name"], county["State"])] \ = (red, 0, blue)
But in practice, that wasn't good enough, because the county names in the Deleetdk names didn't always match the official Census county names.
Fuzzy matches
For instance, the CSV file had no results for Alaska or Puerto Rico, so I had to skip those. Non-ASCII characters were a problem: "Doña Ana" county in the census data was "Dona Ana" in the CSV. I had to strip off " County", " Borough" and similar terms: "St Louis" in the census data was "St. Louis County" in the CSV. Some names were capitalized differently, like PLYMOUTH vs. Plymouth, or Lac Qui Parle vs. Lac qui Parle. And some names were just different, like "Jeff Davis" vs. "Jefferson Davis".
To get around that I used SequenceMatcher to look for fuzzy matches when I couldn't find an exact match:
def fuzzy_find(s, slist): '''Try to find a fuzzy match for s in slist. ''' best_ratio = -1 best_match = None ls = s.lower() for ss in slist: r = SequenceMatcher(None, ls, ss.lower()).ratio() if r > best_ratio: best_ratio = r best_match = ss if best_ratio > .75: return best_match return None
Correlate the county names from the two datasets
It's finally time to loop through the counties in the map to color and plot them.
Remember STATE vs. STATEFP? It turns out there are a few counties in the census county shapefile with a STATEFP that doesn't match any STATE in the state shapefile. Mostly they're in the Virgin Islands and I don't have election data for them anyway, so I skipped them for now. I also skipped Puerto Rico and Alaska (no results in the election data) and counties that had no corresponding state: I'll omit that code here, but you can see it in the final script, linked at the end.
for i, county in enumerate(m.counties_info): countyname = county["NAME"] try: statename = states[int(county["STATEFP"])] except IndexError: print countyname, "has out-of-index statefp of", county["STATEFP"] continue countystate = "%s, %s" % (countyname, statename) try: ccolor = county_colors[countystate] except KeyError: # No exact match; try for a fuzzy match fuzzyname = fuzzy_find(countystate, county_colors.keys()) if fuzzyname: ccolor = county_colors[fuzzyname] county_colors[countystate] = ccolor else: print "No match for", countystate continue countyseg = m.counties[i] poly = Polygon(countyseg, facecolor=ccolor) # edgecolor="white" ax.add_patch(poly)
Moving Hawaii
Finally, although the CSV didn't have results for Alaska, it did have Hawaii. To display it, you can move it when creating the patches:
countyseg = m.counties[i] if statename == 'Hawaii': countyseg = list(map(lambda (x,y): (x + 5750000, y-1400000), countyseg)) poly = Polygon(countyseg, facecolor=countycolor) ax.add_patch(poly)The offsets are in map coordinates and are empirical; I fiddled with them until Hawaii showed up at a reasonable place.
Well, that was a much longer article than I intended. Turns out it takes a fair amount of code to correlate several datasets and turn them into a map. But a lot of the work will be applicable to other datasets.
Full script on GitHub: Blue-red map using Census county shapefile
[ 15:10 Jan 14, 2017 More programming | permalink to this entry | ]