Add files via upload
This commit is contained in:
parent
47bad76202
commit
dd37a16b0c
File diff suppressed because one or more lines are too long
300
ipynb/ElectoralVotesCode.ipynb
Normal file
300
ipynb/ElectoralVotesCode.ipynb
Normal file
@ -0,0 +1,300 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"<div align=\"right\"><i>Peter Norvig<br>12 August 2019</i></div>\n",
|
||||
"\n",
|
||||
"# Data and Code for [Tracking Trump: Electoral Votes Edition](Electoral%20Votes.ipynb)\n",
|
||||
"\n",
|
||||
"First fetch the state-by-state, month-by-month approval data from the **[Tracking Trump](https://morningconsult.com/tracking-trump/)** web page at *Morning Consult*\n",
|
||||
" and cache it locally: "
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 1,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"! curl -s -o evs.html https://morningconsult.com/tracking-trump-2/"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Now some imports: "
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 2,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"%matplotlib inline\n",
|
||||
"import matplotlib.pyplot as plt\n",
|
||||
"import re\n",
|
||||
"import ast\n",
|
||||
"from collections import namedtuple\n",
|
||||
"from IPython.display import display, Markdown\n",
|
||||
"from statistics import stdev"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Additional data: the variable `state_data` contains the [electoral votes by state](https://www.britannica.com/topic/United-States-Electoral-College-Votes-by-State-1787124) and the [partisan lean by state](https://github.com/fivethirtyeight/data/tree/master/partisan-lean) (how much more Republican (plus) or Democratic (minus) leaning the state is compared to the country as a whole, across recent elections). The variable `net_usa` has the [country-wide net presidential approval](https://projects.fivethirtyeight.com/trump-approval-ratings/) by month."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 3,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# From https://github.com/fivethirtyeight/data/tree/master/partisan+lean\n",
|
||||
"# a dict of {\"state name\": (electoral_votes, partisan_lean)}\n",
|
||||
"state_data = { \n",
|
||||
" \"Alabama\": (9, +27), \"Alaska\": (3, +15), \"Arizona\": (11, +9), \n",
|
||||
" \"Arkansas\": (6, +24), \"California\": (55, -24), \"Colorado\": (9, -1), \n",
|
||||
" \"Connecticut\": (7, -11), \"Delaware\": (3, -14), \"District of Columbia\": (3, -43),\n",
|
||||
" \"Florida\": (29, +5), \"Georgia\": (16, +12), \"Hawaii\": (4, -36), \n",
|
||||
" \"Idaho\": (4, +35), \"Illinois\": (20, -13), \"Indiana\": (11, +18), \n",
|
||||
" \"Iowa\": (6, +6), \"Kansas\": (6, +23), \"Kentucky\": (8, +23), \n",
|
||||
" \"Louisiana\": (8, +17), \"Maine\": (4, -5), \"Maryland\": (10, -23), \n",
|
||||
" \"Massachusetts\": (11, -29), \"Michigan\": (16, -1), \"Minnesota\": (10, -2), \n",
|
||||
" \"Mississippi\": (6, +15), \"Missouri\": (10, +19), \"Montana\": (3, +18), \n",
|
||||
" \"Nebraska\": (5, +24), \"Nevada\": (6, +1), \"New Hampshire\": (4, +2), \n",
|
||||
" \"New Jersey\": (14, -13), \"New Mexico\": (5, -7), \"New York\": (29, -22), \n",
|
||||
" \"North Carolina\": (15, +5), \"North Dakota\": (3, +33), \"Ohio\": (18, +7), \n",
|
||||
" \"Oklahoma\": (7, +34), \"Oregon\": (7, -9), \"Pennsylvania\": (20, +1), \n",
|
||||
" \"Rhode Island\": (4, -26), \"South Carolina\": (9, +17), \"South Dakota\": (3, +31), \n",
|
||||
" \"Tennessee\": (11, +28), \"Texas\": (38, +17), \"Utah\": (6, +31), \n",
|
||||
" \"Vermont\": (3, -24), \"Virginia\": (13, 0), \"Washington\": (12, -12), \n",
|
||||
" \"West Virginia\": (5, +30), \"Wisconsin\": (10, +1), \"Wyoming\": (3, +47)}\n",
|
||||
"\n",
|
||||
"# From https://projects.fivethirtyeight.com/trump-approval-ratings/\n",
|
||||
"# A dict of {'date': country-wide-net-approval}\n",
|
||||
"net_usa = { \n",
|
||||
" '1-Jan-17': +10, \n",
|
||||
" '1-Feb-17': 0, '1-Mar-17': -6, '1-Apr-17': -13, '1-May-17': -11,\n",
|
||||
" '1-Jun-17': -16, '1-Jul-17': -15, '1-Aug-17': -19, '1-Sep-17': -20,\n",
|
||||
" '1-Oct-17': -17, '1-Nov-17': -19, '1-Dec-17': -18, '1-Jan-18': -18,\n",
|
||||
" '1-Feb-18': -15, '1-Mar-18': -14, '1-Apr-18': -13, '1-May-18': -12,\n",
|
||||
" '1-Jun-18': -11, '1-Jul-18': -10, '1-Aug-18': -12, '1-Sep-18': -14,\n",
|
||||
" '1-Oct-18': -11, '1-Nov-18': -11, '1-Dec-18': -10, '1-Jan-19': -12,\n",
|
||||
" '1-Feb-19': -16, '1-Mar-19': -11, '1-Apr-19': -11, '1-May-19': -12,\n",
|
||||
" '1-Jun-19': -12, '1-Jul-19': -11, '1-Aug-19': -10, '1-Sep-19': -13,\n",
|
||||
" '1-Oct-19': -13}\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Now the code to parse and manipulate the data:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 4,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"class State(namedtuple('_', 'name, ev, lean, approvals, disapprovals')):\n",
|
||||
" '''A State has a name, the number of electoral votes, the partisan lean,\n",
|
||||
" and two dicts of {date: percent}: approvals and disapprovals'''\n",
|
||||
"\n",
|
||||
"def parse_page(filename='evs.html', data=state_data):\n",
|
||||
" \"Read data from the file and return (list of dates, list of `State`s, last date).\"\n",
|
||||
" # File format: Date headers, then [state, approval, disapproval ...]\n",
|
||||
" # [[\"Demographic\",\"1-Jan-17\",\"\",\"1-Feb-17\",\"\", ... \"1-Apr-19\",\"\"],\n",
|
||||
" # [\"Alabama\",\"62\",\"26\",\"65\",\"29\", ... \"61\",\"35\"], ... ] =>\n",
|
||||
" # State(\"Alabama\", 9, +27, approvals={\"1-Jan-17\": 62, ...}, \n",
|
||||
" # disapprovals={\"1-Jan-17\": 26, ...}), ...\n",
|
||||
" text = re.findall(r'\\[\\[.*?\\]\\]', open(filename).read())[0]\n",
|
||||
" header, *table = ast.literal_eval(text)\n",
|
||||
" dates = header[1::2] # Every other header entry is a date\n",
|
||||
" states = [State(name, *data[name],\n",
|
||||
" approvals=dict(zip(dates, map(int, numbers[0::2]))),\n",
|
||||
" disapprovals=dict(zip(dates, map(int, numbers[1::2]))))\n",
|
||||
" for (name, *numbers) in table]\n",
|
||||
" return states, dates, dates[-1]\n",
|
||||
"\n",
|
||||
"states, dates, now = parse_page()\n",
|
||||
"\n",
|
||||
"def EV(states, date=now, swing=0) -> int:\n",
|
||||
" \"Total electoral votes with net positive approval (plus half the votes for net zero).\"\n",
|
||||
" return sum(s.ev * (1/2 if net(s, date) + swing == 0 else int(net(s, date) + swing > 0))\n",
|
||||
" for s in states)\n",
|
||||
"\n",
|
||||
"def margin(states, date=now) -> int:\n",
|
||||
" \"What's the least swing that would lead to a majority?\"\n",
|
||||
" return next(swing for swing in range(-50, 50) if EV(states, date, swing) >= 270)\n",
|
||||
"\n",
|
||||
"def net(state, date=now) -> int: return state.approvals[date] - state.disapprovals[date]\n",
|
||||
"def undecided(state, date=now) -> int: return 100 - state.approvals[date] - state.disapprovals[date]\n",
|
||||
"def movement(state, date=now) -> float: return undecided(state, date) / 5 + 2 * 𝝈(state)\n",
|
||||
"def 𝝈(state, recent=dates[-12:]) -> float: return stdev(net(state, d) for d in recent)\n",
|
||||
"def is_swing(state) -> bool: return abs(net(state)) < movement(state)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Various functions for displaying data:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 5,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"def labels(xlab, ylab): plt.xlabel(xlab); plt.ylabel(ylab); plt.grid(True); plt.legend()\n",
|
||||
"\n",
|
||||
"def grid(): plt.minorticks_on(); plt.grid(which='minor', ls=':', alpha=0.7)\n",
|
||||
" \n",
|
||||
"def header(head) -> str: return head + '\\n' + '-'.join('|' * head.count('|'))\n",
|
||||
"\n",
|
||||
"def markdown(fn) -> callable: return lambda *args: display(Markdown('\\n'.join(fn(*args))))\n",
|
||||
"\n",
|
||||
"def parp(state, date=now) -> int: return net(state, date) - state.lean "
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 6,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"def show_months(states=states, dates=dates, swing=3):\n",
|
||||
" plt.rcParams[\"figure.figsize\"] = [10, 7]\n",
|
||||
" plt.style.use('fivethirtyeight')\n",
|
||||
" N = len(dates)\n",
|
||||
" err = [[EV(states, date) - EV(states, date, -swing) for date in dates],\n",
|
||||
" [EV(states, date, swing) - EV(states, date) for date in dates]]\n",
|
||||
" grid()\n",
|
||||
" plt.plot(range(N), [270] * N, color='darkorange', label=\"270 EVs\", lw=2)\n",
|
||||
" plt.errorbar(range(N), [EV(states, date) for date in dates], fmt='D-',\n",
|
||||
" yerr=err, ecolor='grey', capsize=7, label='Trump EVs ±3% swing', lw=2)\n",
|
||||
" labels('Months into term', 'Electoral Votes')"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 7,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"def show_approval(states=states, dates=dates):\n",
|
||||
" plt.rcParams[\"figure.figsize\"] = [10, 7]\n",
|
||||
" plt.style.use('fivethirtyeight')\n",
|
||||
" N = len(dates)\n",
|
||||
" grid()\n",
|
||||
" plt.plot(range(N), [0] * N, label='Net zero', color='darkorange')\n",
|
||||
" plt.plot(range(N), [-margin(states, date) for date in dates], 'D-', label='Margin to 270')\n",
|
||||
" plt.plot(range(N), [net_usa[date] for date in dates], 'go-', label='Country-wide Net')\n",
|
||||
" labels('Months into term', 'Net popularity')"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 8,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"@markdown\n",
|
||||
"def by_month(states, dates=dates[::-1]):\n",
|
||||
" yield header('|Month|EVs|Margin|Country|Undecided|')\n",
|
||||
" for date in dates:\n",
|
||||
" month = date.replace('1-', '').replace('-', ' 20')\n",
|
||||
" yield (f'|{month}|{int(EV(states, date))}|{margin(states, date)}%|{net_usa[date]:+d}%'\n",
|
||||
" f'|{sum(s.ev * undecided(s, date) for s in states) / 538:.0f}% '\n",
|
||||
" f'({sum(undecided(s, date) > 5 for s in states)} states)')"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 9,
|
||||
"metadata": {
|
||||
"scrolled": false
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"@markdown\n",
|
||||
"def show_states(states=states, d=now, ref='1-Jan-17'):\n",
|
||||
" total = 0\n",
|
||||
" yield header(f'|State|Net|Move|EV|ΣEV|+|−|?|𝝈|Δ|')\n",
|
||||
" for s in sorted(states, key=net, reverse=True):\n",
|
||||
" total += s.ev\n",
|
||||
" b = '**' * is_swing(s)\n",
|
||||
" yield (f'|{swing_name(s)}|{b}{net(s, d):+d}%{b}|{b}±{movement(s):.0f}%{b}|{s.ev}|{total}'\n",
|
||||
" f'|{s.approvals[d]}%|{s.disapprovals[d]}%|{undecided(s, now)}%|±{𝝈(s):3.1f}%'\n",
|
||||
" f'|{net(s, d) - net(s, ref):+d}%|')\n",
|
||||
" \n",
|
||||
"def swing_name(s) -> str: return ('**' + s.name.upper() + '**') if is_swing(s) else s.name"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 10,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"@markdown\n",
|
||||
"def show_parp(states=states, dates=(now, '1-Jan-19', '1-Jan-18', '1-Jan-17')):\n",
|
||||
" def year(date): return '' if date == now else date[-2:]\n",
|
||||
" fields = [f'PARP {year(date)}|Net {year(date)}' for date in dates]\n",
|
||||
" yield header(f'|State|Lean|EV|{\"|\".join(fields)}|')\n",
|
||||
" for s in sorted(states, key=parp, reverse=True):\n",
|
||||
" fields = [f'{parp(s, date):+d}|{net(s, date):+d}' for date in dates]\n",
|
||||
" yield f'|{swing_name(s)}|{s.lean:+d}|{s.ev}|{\"|\".join(fields)}|'"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"I really should have some more tests."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 11,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"assert len(states) == 51, \"50 states plus DC\"\n",
|
||||
"assert all(s.ev >= 3 for s in states), \"All states have two senators and at least one rep.\"\n",
|
||||
"assert sum(s.ev for s in states) == 538, \"Total of 538 electoral votes.\""
|
||||
]
|
||||
}
|
||||
],
|
||||
"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.2"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 2
|
||||
}
|
Loading…
Reference in New Issue
Block a user