Skip to content

Instantly share code, notes, and snippets.

@jimmyrocks
Last active November 12, 2020 01:50
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jimmyrocks/370f69771142cdcc8d402b1f36bda36b to your computer and use it in GitHub Desktop.
Save jimmyrocks/370f69771142cdcc8d402b1f36bda36b to your computer and use it in GitHub Desktop.
Lehigh County, PA Election Mapping 2020
Display the source blob
Display the rendered blob
Raw
{
"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>&gt; 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>&gt; 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