Lehigh County, PA Election Mapping 2020
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"cells": [ | |
{ | |
"cell_type": "code", | |
"execution_count": null, | |
"metadata": {}, | |
"outputs": [], | |
"source": [ | |
"# Step 1\n", | |
"# Download the Lehigh County, PA Precincts\n", | |
"\n", | |
"import urllib\n", | |
"import requests\n", | |
"import subprocess\n", | |
"import json\n", | |
"import re\n", | |
"import os\n", | |
"\n", | |
"output_file = \"/tmp/lehigh_county_precincts.geojson\"\n", | |
"\n", | |
"url = 'https://services1.arcgis.com/XWDNR4PQlDQwrRCL/arcgis/rest/services/Precincts2/FeatureServer/0/query'\n", | |
"query_string = {\n", | |
" \"where\": \"1=1\",\n", | |
" \"f\": \"json\",\n", | |
" \"outfields\": \"*\"\n", | |
"}\n", | |
"url += \"?\" + \"&\".join(list(map(lambda x: x + '=' + urllib.parse.quote(query_string[x]), query_string)))\n", | |
"\n", | |
"if not os.path.exists(output_file):\n", | |
" # Only download it if it doesn't already exist\n", | |
" ogrtask = subprocess.run([\n", | |
" \"ogr2ogr\",\n", | |
" \"-f\", \"GeoJSON\", output_file,\n", | |
" url,\n", | |
" \"-s_srs\", \"EPSG:3857\", \"-t_srs\", \"EPSG:4326\"\n", | |
" ])\n", | |
"\n", | |
"precincts_geojson = json.load(open(output_file))\n", | |
"print ('Done Step 1')" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": null, | |
"metadata": {}, | |
"outputs": [], | |
"source": [ | |
"# Step 2\n", | |
"# Download and parse the reslts from the county\n", | |
"\n", | |
"# I'm not sure why the cert doesn't seem to play nicely with my Docker Image\n", | |
"import ssl\n", | |
"ssl_ignore = ssl.create_default_context()\n", | |
"ssl_ignore.check_hostname = False\n", | |
"ssl_ignore.verify_mode = ssl.CERT_NONE\n", | |
"\n", | |
"races = {\n", | |
" \"Presidential Electors\": \"317E7E526163657E46343344363839452D354634342D343832442D383532392D3343333741374644353330377E30\",\n", | |
" \"7th Congressional District\": \"317E7E526163657E42323135443941322D354237452D344145322D423345432D4335423831323837414445447E30\",\n", | |
" \"Attorney General\": \"317E7E526163657E34433932463336432D333638412D343843332D384444362D3138423333374431423342397E30\",\n", | |
" \"Auditor General\": \"317E7E526163657E44453734453741342D373438422D343543362D423546362D4542443242373041413937427E30\", \n", | |
"}\n", | |
"\n", | |
"for race in races:\n", | |
" results_url = \"https://home.lehighcounty.org/TallyHo/MapSupport/ElectionResultsHandler.ashx?racekey=\" + races[race]\n", | |
" results_response = urllib.request.urlopen(results_url, context=ssl_ignore)\n", | |
" races[race] = json.loads(results_response.read())\n", | |
" \n", | |
"print ('Done Step 2')" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": null, | |
"metadata": {}, | |
"outputs": [], | |
"source": [ | |
"# Step 3\n", | |
"# Unnest some of the data so we can use the results\n", | |
"precinct_results_clean = {}\n", | |
"\n", | |
"for race in races:\n", | |
" nested_precinct_data = races[race]['Precincts']\n", | |
" for precinct in nested_precinct_data:\n", | |
" cleaned_record = {}\n", | |
" for candidate in precinct['PrecinctCandidates']:\n", | |
" candidate_name = candidate['PartyCode'] + ' - ' + candidate['CandidateName'] + ' (' + race + ')'\n", | |
" cleaned_record[candidate_name] = candidate['VoteCount']\n", | |
" if precinct['PrecinctId'] in precinct_results_clean.keys():\n", | |
" precinct_results_clean[precinct['PrecinctId']] = {**precinct_results_clean[precinct['PrecinctId']], **cleaned_record}\n", | |
" else:\n", | |
" precinct_results_clean[precinct['PrecinctId']] = cleaned_record\n", | |
" \n", | |
"# precinct_results_clean\n", | |
"print (\"Step 3 done\")" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": null, | |
"metadata": {}, | |
"outputs": [], | |
"source": [ | |
"# Step 4\n", | |
"# Bring on the Pandas!\n", | |
"\n", | |
"import pandas as pd\n", | |
"df = pd.DataFrame(precinct_results_clean).T\n", | |
"df" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": null, | |
"metadata": {}, | |
"outputs": [], | |
"source": [ | |
"#Step 5\n", | |
"# Lets add in some ratios (They need to be changed to float to convert it back to JSON at some point)\n", | |
"\n", | |
"calculated_elections = {\n", | |
" 'Presidential Electors': {\n", | |
" 'dem': 'Dem - Biden and Harris (Presidential Electors)',\n", | |
" 'rep': 'Rep - Trump and Pence (Presidential Electors)',\n", | |
" 'lib': 'Lib - Jorgensen and Cohen (Presidential Electors)'\n", | |
" },\n", | |
" 'PA Attorney General': {\n", | |
" 'dem': \"Dem - Josh Shapiro (Attorney General)\",\n", | |
" 'rep': \"Rep - Heather Heidelbaugh (Attorney General)\",\n", | |
" 'lib': \"Lib - Daniel Wassmer (Attorney General)\",\n", | |
" 'gre': \"Gre - Richard L Weiss (Attorney General)\"\n", | |
" },\n", | |
" 'PA Auditor General': {\n", | |
" 'dem': \"Dem - Nina Ahmad (Auditor General)\",\n", | |
" 'rep': \"Rep - Timothy DeFoor (Auditor General)\",\n", | |
" 'lib': \"Lib - Jennifer Moore (Auditor General)\",\n", | |
" 'gre': \"Gre - Olivia Faison (Auditor General)\"\n", | |
" },\n", | |
" 'PA Representative in Congress 7th Congressional District': {\n", | |
" 'dem': \"Dem - Susan Wild (7th Congressional District)\",\n", | |
" 'rep': \"Rep - Lisa Scheller (7th Congressional District)\"\n", | |
" }\n", | |
"}\n", | |
"\n", | |
" \n", | |
"# Create the columns\n", | |
"for seat in calculated_elections:\n", | |
" seat_votes = 0\n", | |
" for party in calculated_elections[seat]:\n", | |
" seat_votes += pd.to_numeric(df[calculated_elections[seat][party]])\n", | |
" for party in calculated_elections[seat]:\n", | |
" df[seat + ' Pct ' + party.upper()] = ((\n", | |
" pd.to_numeric(df[calculated_elections[seat][party]]) /\n", | |
" seat_votes * 100).astype(float))\n", | |
"\n", | |
"print(\"Step 5 Done\")\n", | |
"df" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": null, | |
"metadata": {}, | |
"outputs": [], | |
"source": [ | |
"# Step 6\n", | |
"# Join the GeoJSON data to the dataframe\n", | |
"\n", | |
"for feature in precincts_geojson[\"features\"]:\n", | |
" if feature[\"properties\"][\"PRECINCTID\"] in df.index:\n", | |
" feature[\"properties\"] = {\n", | |
" **feature[\"properties\"],\n", | |
" **dict(df.loc[feature[\"properties\"][\"PRECINCTID\"]])\n", | |
" }\n", | |
" \n", | |
"print(\"Step 6 Done\")" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": null, | |
"metadata": {}, | |
"outputs": [], | |
"source": [ | |
"# Step 7\n", | |
"# Create a map\n", | |
"\n", | |
"# I'm going to use mapboxgljs, but I don't have an API key, so I will use CARTO tiles instead\n", | |
"\n", | |
"from IPython.core.display import display, HTML\n", | |
"from string import Template \n", | |
"\n", | |
"import uuid #the divs will use a unique UUID so that don't stomp on each other\n", | |
"\n", | |
"html = Template(\"\"\"\n", | |
"<link href=\"https://api.mapbox.com/mapbox-gl-js/v1.12.0/mapbox-gl.css\" rel=\"stylesheet\" />\n", | |
"<style>\n", | |
" #$map_div { width: 100%; height: 275px; }\n", | |
"</style>\n", | |
"\n", | |
"$legend\n", | |
"<div id=\"$map_div\"></div>\n", | |
"\n", | |
"<script>\n", | |
" window.loadmap = function() {\n", | |
"\n", | |
" require.config({\n", | |
" paths: {\n", | |
" \"mapboxgl\": \"https://api.mapbox.com/mapbox-gl-js/v1.12.0/mapbox-gl\"\n", | |
" }\n", | |
" });\n", | |
"\n", | |
" require([\"mapboxgl\"], function(mapboxgl) {\n", | |
" var map = new mapboxgl.Map({\n", | |
" container: '$map_div',\n", | |
" style: {\n", | |
" 'version': 8,\n", | |
" 'sources': {\n", | |
" 'raster-tiles': {\n", | |
" 'type': 'raster',\n", | |
" 'tiles': [\n", | |
" 'https://a.basemaps.cartocdn.com/rastertiles/light_all/{z}}/{x}/{y}.png',\n", | |
" 'https://b.basemaps.cartocdn.com/rastertiles/light_all/{z}}/{x}/{y}.png',\n", | |
" 'https://c.basemaps.cartocdn.com/rastertiles/light_all/{z}}/{x}/{y}.png',\n", | |
" 'https://d.basemaps.cartocdn.com/rastertiles/light_all/{z}}/{x}/{y}.png'\n", | |
" ],\n", | |
" 'tileSize': 256,\n", | |
" 'attribution': '© <a href=\"http://www.openstreetmap.org/copyright\" target=\"_blank\">OpenStreetMap</a> contributors, © <a href=\"https://carto.com/attributions\" target=\"_blank\">CARTO</a>'\n", | |
" }\n", | |
" },\n", | |
" 'layers': [\n", | |
" {\n", | |
" 'id': 'simple-tiles',\n", | |
" 'type': 'raster',\n", | |
" 'source': 'raster-tiles',\n", | |
" 'minzoom': 0,\n", | |
" 'maxzoom': 22\n", | |
" }\n", | |
" ]\n", | |
" },\n", | |
" center: [-75.25, 40.605],\n", | |
" zoom: 8.6\n", | |
" });\n", | |
"\n", | |
" map.on('load', function () {\n", | |
" map.addSource('$layer_name', {\n", | |
" 'type': 'geojson',\n", | |
" 'data': $geojson\n", | |
" });\n", | |
" map.addLayer({\n", | |
" 'id': 'precincts',\n", | |
" 'type': 'fill',\n", | |
" 'source': '$layer_name',\n", | |
" 'layout': {},\n", | |
" 'paint': $style\n", | |
" });\n", | |
" });\n", | |
" \n", | |
" $more_code\n", | |
" \n", | |
" });\n", | |
" };\n", | |
" loadmap();\n", | |
"</script>\n", | |
"\"\"\")\n", | |
"\n", | |
"print('Step 7 Done')" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": null, | |
"metadata": {}, | |
"outputs": [], | |
"source": [ | |
"# Step 8\n", | |
"# Create the style and view the map\n", | |
"\n", | |
"# First lets try to create a spectrum from Red (0% DEM vote) and Blue (100% DEM Vote)\n", | |
"\n", | |
"style = Template(\"\"\"\n", | |
"{\n", | |
" 'fill-color': [\n", | |
" \"case\",\n", | |
" [\">=\", ['-', ['get', '$election_name Pct DEM'], ['get', '$election_name Pct REP']],\n", | |
" 15],\n", | |
" ['to-color', '#2171b5'],\n", | |
" [\">=\", ['-', ['get', '$election_name Pct DEM'], ['get', '$election_name Pct REP']],\n", | |
" 10],\n", | |
" ['to-color', '#6baed6'],\n", | |
" [\">=\", ['-', ['get', '$election_name Pct DEM'], ['get', '$election_name Pct REP']],\n", | |
" 5],\n", | |
" ['to-color', '#bdd7e7'],\n", | |
" [\">=\", ['-', ['get', '$election_name Pct DEM'], ['get', '$election_name Pct REP']],\n", | |
" 0],\n", | |
" ['to-color', '#eff3ff'],\n", | |
" [\">=\", ['-', ['get', '$election_name Pct DEM'], ['get', '$election_name Pct REP']],\n", | |
" -5],\n", | |
" ['to-color', '#fee5d9'],\n", | |
" [\">=\", ['-', ['get', '$election_name Pct DEM'], ['get', '$election_name Pct REP']],\n", | |
" -10],\n", | |
" ['to-color', '#fcae91'],\n", | |
" [\">=\", ['-', ['get', '$election_name Pct DEM'], ['get', '$election_name Pct REP']],\n", | |
" -15],\n", | |
" ['to-color', '#fb6a4a'],\n", | |
" [\"<\", ['-', ['get', '$election_name Pct DEM'], ['get', '$election_name Pct REP']],\n", | |
" -15],\n", | |
" ['to-color', '#cb181d'],\n", | |
" ['to-color', '#fff']\n", | |
" ],\n", | |
" 'fill-outline-color': '#000',\n", | |
" 'fill-opacity': [\"case\", \n", | |
" [\"==\", ['get', '$election_name Pct DEM'], null],\n", | |
" 0,\n", | |
" 0.6]\n", | |
"}\n", | |
"\"\"\")\n", | |
"\n", | |
"\n", | |
"# The map on click code\n", | |
"layer_name = 'precincts'\n", | |
"more_code = Template(\"\"\"\n", | |
" map.on('click', '$layer_name', function (e) {\n", | |
" new mapboxgl.Popup()\n", | |
" .setLngLat(e.lngLat)\n", | |
" .setHTML($popup_html)\n", | |
" .addTo(map);\n", | |
" });\n", | |
"\n", | |
" // Change the cursor to a pointer when the mouse is over the states layer.\n", | |
" map.on('mouseenter', '$layer_name', function () {\n", | |
" map.getCanvas().style.cursor = 'pointer';\n", | |
" });\n", | |
"\n", | |
" // Change it back to a pointer when it leaves.\n", | |
" map.on('mouseleave', '$layer_name', function () {\n", | |
" map.getCanvas().style.cursor = '';\n", | |
" });\n", | |
" \n", | |
"\"\"\")\n", | |
"\n", | |
"\n", | |
"# Creating a javascript string inside a python string is a little strange\n", | |
"# So this is a function to do it for us\n", | |
"def make_table(election_name): \n", | |
" # Define the HTML popup with some code\n", | |
" display_fields = [\n", | |
" \"NAME\",\n", | |
" calculated_elections[election_name]['dem'],\n", | |
" calculated_elections[election_name]['rep']\n", | |
" ]\n", | |
" popup_html='\"<table>\" + '\n", | |
"\n", | |
" for field in display_fields:\n", | |
" popup_html += '\"<tr><td>%s</td><td>\" + e.features[0].properties[\"%s\"] + \"</td></tr>\" + ' % (field, field)\n", | |
"\n", | |
" # Since we're using the percentage for the display, we should add that in as well\n", | |
" popup_html += '\"<tr><td>%s</td><td>\" + ' % ('Lead Percentage')\n", | |
" popup_html += Template(\"\"\"\n", | |
" Math.abs((e.features[0].properties[\"$dem\"] - e.features[0].properties[\"$rep\"]).toFixed(2)) + \"% \" +\n", | |
" ((e.features[0].properties[\"$dem\"] - e.features[0].properties[\"$rep\"]) > 0 ? \"Dem\" : \"Rep\") +\n", | |
" \"\"\").substitute(dem=election_name + ' Pct DEM',\n", | |
" rep=election_name + ' Pct REP')\n", | |
" popup_html += '\"</td></tr>\" + '\n", | |
" popup_html += '\"</table>\"'\n", | |
"\n", | |
" return popup_html\n", | |
"\n", | |
"def make_legend(election_name, map_id):\n", | |
" legend_html=Template(\"\"\"\n", | |
" <style>\n", | |
" .legend {\n", | |
" background-color: #fff;\n", | |
" border-radius: 3px;\n", | |
" bottom: 30px;\n", | |
" box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);\n", | |
" font: 12px/20px 'Helvetica Neue', Arial, Helvetica, sans-serif;\n", | |
" padding: 10px;\n", | |
" position: absolute;\n", | |
" right: 10px;\n", | |
" z-index: 1;\n", | |
" }\n", | |
"\n", | |
" .legend h4 {\n", | |
" margin: 0 0 10px;\n", | |
" }\n", | |
"\n", | |
" .legend div span {\n", | |
" border-radius: 50%;\n", | |
" display: inline-block;\n", | |
" height: 10px;\n", | |
" margin-right: 5px;\n", | |
" width: 10px;\n", | |
" }\n", | |
" </style>\n", | |
"\n", | |
"\n", | |
" <div id='legend-$map_id' class='legend'>\n", | |
" <h4 style=\"max-width:200px\">$election_name % Lead</h4>\n", | |
" <div><span style=\"background-color: #2171b5\"></span>> 15% DEM</div>\n", | |
" <div><span style=\"background-color: #6baed6\"></span>10%-15% DEM</div>\n", | |
" <div><span style=\"background-color: #bdd7e7\"></span>5%=10% DEM</div>\n", | |
" <div><span style=\"background-color: #eff3ff\"></span>0%-5% DEM</div>\n", | |
" <div><span style=\"background-color: #fee5d9\"></span>0%-5% REP</div>\n", | |
" <div><span style=\"background-color: #fcae91\"></span>5%-10% REP</div>\n", | |
" <div><span style=\"background-color: #fb6a4a\"></span>10%-15% REP</div>\n", | |
" <div><span style=\"background-color: #cb181d\"></span>> 15% REP</div>\n", | |
" <small>Source: <a href=\"https://www.lehighcounty.org/Departments/Voter-Registration/Election-Results\">Lehigh County, PA</a></small>\n", | |
" </div>\n", | |
" \"\"\")\n", | |
" return legend_html.substitute(election_name=election_name, map_id=map_id)\n", | |
"\n", | |
"#The popup talks to the layer, so let's use a variable so we don't need to type it more than once\n", | |
"# let's put it into a function to make it easier to call\n", | |
"\n", | |
"def draw_map(election_name):\n", | |
" print(election_name)\n", | |
" map_div = 'map' + str(uuid.uuid4())\n", | |
" layer_name = 'precincts-' + map_div\n", | |
" popup_html = make_table(election_name)\n", | |
" legend_html = make_legend(election_name, map_div)\n", | |
" \n", | |
" return display(HTML(html.substitute(\n", | |
" map_div=map_div,\n", | |
" legend=legend_html,\n", | |
" style=style.substitute(\n", | |
" election_name=election_name\n", | |
" ),\n", | |
" geojson=json.dumps(precincts_geojson),\n", | |
" more_code=more_code.substitute(\n", | |
" layer_name=layer_name,\n", | |
" popup_html=popup_html\n", | |
" ),\n", | |
" layer_name=layer_name\n", | |
" )))\n", | |
"\n", | |
"draw_map('Presidential Electors')" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": null, | |
"metadata": {}, | |
"outputs": [], | |
"source": [ | |
"draw_map('PA Attorney General')" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": null, | |
"metadata": { | |
"scrolled": true | |
}, | |
"outputs": [], | |
"source": [ | |
"draw_map('PA Representative in Congress 7th Congressional District')" | |
] | |
} | |
], | |
"metadata": { | |
"kernelspec": { | |
"display_name": "Python 3", | |
"language": "python", | |
"name": "python3" | |
}, | |
"language_info": { | |
"codemirror_mode": { | |
"name": "ipython", | |
"version": 3 | |
}, | |
"file_extension": ".py", | |
"mimetype": "text/x-python", | |
"name": "python", | |
"nbconvert_exporter": "python", | |
"pygments_lexer": "ipython3", | |
"version": "3.7.4" | |
} | |
}, | |
"nbformat": 4, | |
"nbformat_minor": 4 | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment