diff --git a/api.go b/api.go index 1c17778..54e74e1 100644 --- a/api.go +++ b/api.go @@ -13,7 +13,7 @@ import ( ) // sort polls by most recent first so we use the most recent data -const apiUrl = "http://elections.huffingtonpost.com/pollster/api/polls.json?sort=updated&topic=2012-president&state=" +const apiUrl = "http://elections.huffingtonpost.com/pollster/api/polls.json?sort=updated&topic=%s&state=%s" type Responses struct { Choice *string @@ -61,8 +61,9 @@ type Poll struct { } // readPollingApi reads the data from the Pollster API. -func readPollingApi(state string) []byte { - resp, err := http.Get(apiUrl + state) +func readPollingApi(topic, state string) []byte { + url := fmt.Sprintf(apiUrl, topic, state) + resp, err := http.Get(url) if err != nil { log.Fatal(err) } diff --git a/cdf_test.go b/cdf_test.go index 3f90395..895e151 100644 --- a/cdf_test.go +++ b/cdf_test.go @@ -1,7 +1,7 @@ // // cdf_test.go // -// go test election2012.go state.go api.go cdf.go parse.go college.go cdf_test.go +// $ go test github.com/GaryBoone/PresidentialMonteCarlo // package main diff --git a/college.go b/college.go index 5b537d9..22062ad 100644 --- a/college.go +++ b/college.go @@ -5,11 +5,69 @@ package main type College struct { - votes int - dem2008 bool + votes int + lastElection bool } +// 2012 Electoral College +// format: "State": College{number of votes, did Democrat win?}, var college = map[string]College{ + "AL": College{9, false}, + "AK": College{3, false}, + "AZ": College{11, false}, + "AR": College{6, false}, + "CA": College{55, true}, + "CO": College{9, true}, + "CT": College{7, true}, + "DE": College{3, true}, + "DC": College{3, true}, + "FL": College{29, true}, + "GA": College{16, false}, + "HI": College{4, true}, + "ID": College{4, false}, + "IL": College{20, true}, + "IN": College{11, false}, + "IA": College{6, true}, + "KS": College{6, false}, + "KY": College{8, false}, + "LA": College{8, false}, + "ME": College{4, true}, + "MD": College{10, true}, + "MA": College{11, true}, + "MI": College{16, true}, + "MN": College{10, true}, + "MS": College{6, false}, + "MO": College{10, false}, + "MT": College{3, false}, + "NE": College{5, false}, + "NV": College{6, true}, + "NH": College{4, true}, + "NJ": College{14, true}, + "NM": College{5, true}, + "NY": College{29, true}, + "NC": College{15, false}, + "ND": College{3, false}, + "OH": College{18, true}, + "OK": College{7, false}, + "OR": College{7, true}, + "PA": College{20, true}, + "RI": College{4, true}, + "SC": College{9, false}, + "SD": College{3, false}, + "TN": College{11, false}, + "TX": College{38, false}, + "UT": College{6, false}, + "VT": College{3, true}, + "VA": College{13, true}, + "WA": College{12, true}, + "WV": College{5, false}, + "WI": College{10, true}, + "WY": College{3, false}, +} + +// 2008 Electoral College +// format: "State": College{number of votes, did Democrat win?}, +var college2008 = map[string]College{ "AL": College{9, false}, "AK": College{3, false}, "AZ": College{11, false}, diff --git a/election2012.go b/election2012.go index 95b53bb..92df711 100644 --- a/election2012.go +++ b/election2012.go @@ -1,25 +1,31 @@ // -// election2012.go +// election.go // -// An electoral college Monte Carlo simulation based on 2012 presidential polling. -// -// To run: -// $ go run election2012.go state.go api.go cdf.go parse.go college.go +// An electoral college Monte Carlo simulation based on 2016 presidential polling. // +// To build and run: +// $ cd $GOPATH +// $ mkdir -p src/github.com/GaryBoone/ +// $ cd src/github.com/GaryBoone/ +// $ git clone https://github.com/GaryBoone/PresidentialMonteCarlo +// $ cd ../../.. +// $ go install github.com/GaryBoone/PresidentialMonteCarlo +// $ bin/PresidentialMonteCarlo// // Author: Gary Boone gary.boone@gmail.com -// History: 2012-09-17 • initial version -// 2012-09-21 • cleanup, upload to github +// History: +// 2012-09-25 • simulations in parallel // 2012-09-24 • minimum σ // • command line parameters // • days until election countdown -// 2012-09-25 • simulations in parallel +// 2012-09-21 • cleanup, upload to github +// 2012-09-17 • initial version // Notes: // // The state-by-state presidential polling data is provided by the Pollster API: // http://elections.huffingtonpost.com/pollster/api // // Example API call: -// wget -O - 'http://elections.huffingtonpost.com/pollster/api/polls.json?topic=2012-president&state=OH' +// wget -O - 'http://elections.huffingtonpost.com/pollster/api/polls.json?topic=2016-president&state=OH' // // Read the logfile for details. // @@ -38,12 +44,21 @@ import ( "time" ) -const swingStates = "FL,OH,NC,VA,WI,CO,IA,NV,NH" +const ( + democraticCandidate = "Clinton"// "Obama" + republicanCandidate = "Trump" // "Romney" + electionYear = 2016 // 2012 + electionDay = 8 // 6 + swingStates = "CO,FL,IA,NC,NH,NV,OH,PA,VA,WI" +) var ( acceptableSize int numSimulations int min_σ float64 + pollTopic = fmt.Sprintf("%d-president", electionYear) + loc, _ = time.LoadLocation("America/New_York") + electionDate = time.Date(electionYear, time.November, electionDay, 0, 0, 0, 0, loc) ) func init() { @@ -92,17 +107,17 @@ func loadStateData(state string, polls []Poll) (prob StateProbability) { continue } - var obama, romney, size int - obama, romney, size = parsePoll(state, poll) - if obama == 0 || romney == 0 { - log.Printf(" Missing value (Obama=%v, Romney=%v) for %v state poll by '%v'. Skipping.\n", - obama, romney, state, *poll.Pollster) + var democrat, republican, size int + democrat, republican, size = parsePoll(state, poll, pollTopic) + if democrat == 0 || republican == 0 { + log.Printf(" Missing value (Democrat=%v, Republican=%v) for %v state poll by '%v'. Skipping.\n", + democrat, republican, state, *poll.Pollster) continue } - log.Printf(" adding %-30s %10s : O(%v), R(%v), N(%v)\n", - truncateString(pollster, 30), date[:10], obama, romney, size) - prob.update(obama, romney, size) + log.Printf(" adding %-30s %10s : Democrat(%v), Republican(%v), poll size(%v)\n", + truncateString(pollster, 30), date[:10], democrat, republican, size) + prob.update(democrat, republican, size) if prob.N > float64(acceptableSize) { return } @@ -120,9 +135,15 @@ func simulateObamaVotes(states []StateProbability, r *rand.Rand) int { } func loadProbability(state string) StateProbability { - body := readPollingApi(state) + body := readPollingApi(pollTopic, state) polls := parseJson(body) - log.Printf("Found %v polls in %v.\n", len(polls), state) + + msg := "" + if strings.Contains(swingStates, state) { + msg = ", a swing state" + } + log.Printf("Found %v polls in %v%s.\n", len(polls), state, msg) + prob := loadStateData(state, polls) prob.logStateProbability() return prob @@ -142,7 +163,7 @@ func initializeSimulations() []StateProbability { prob := <-results stateProbabilities[i] = prob if i == 0 { - fmt.Printf("Collecting survey data for the great state of %v", prob.state) + fmt.Printf("Collecting survey data for the great states of %v", prob.state) } else { fmt.Printf(", %v", prob.state) } @@ -186,35 +207,46 @@ func runSimulations(probs []StateProbability) (int, int) { return wins, votes } -// Let's say election day begins on midnight Eastern Time on Nov 6, 2012 func daysUntilElection() int { now := time.Now() - // Midnight Nov 6 is Eastern Standard Time, not DST, so 5 hours behind UTC - electionDay := time.Date(2012, time.November, 6, 5, 0, 0, 0, time.UTC) - return int(math.Ceil(float64(electionDay.Sub(now)) / (24 * 60 * 60 * 1000000000.0))) + return int(math.Ceil(float64(electionDate.Sub(now)) / (24 * 60 * 60 * 1000000000.0))) +} + +func reportProbalities(probs []StateProbability) { + numStatesWithoutPolls := 0 + fmt.Println("\nSwing States:") + for _, st := range probs { + if st.N==0 { + numStatesWithoutPolls++ + if strings.Contains(swingStates, st.state) { + fmt.Printf("%s has no polls yet, so it is assigned to %s based on %d outcome.\n", st.state, democraticCandidate, electionYear-4) + } + } else { + if strings.Contains(swingStates, st.state) { + fmt.Printf("Probability of %s winning %v: %4.2f%%\n", democraticCandidate, st.state, 100.0*st.DemocratProbability) + } + } + } + fmt.Printf("%d states have no polls, so were assigned %d outcomes\n", numStatesWithoutPolls, electionYear-4) } func main() { flag.Parse() initializeLog() - fmt.Println("Election 2012 Monte Carlo Simulation") + fmt.Println("Election %s Monte Carlo Simulation", electionYear) fmt.Printf("There are %v days until the election.\n\n", daysUntilElection()) stateProbalities := initializeSimulations() - - fmt.Println("\nSwing States:") - for _, st := range stateProbalities { - if strings.Contains(swingStates, st.state) { - fmt.Printf("Probability of Obama winning %v: %4.2f%%\n", st.state, 100.0*st.ObamaProbability) - } - } - + reportProbalities(stateProbalities) + wins, totalVotes := runSimulations(stateProbalities) - fmt.Printf("\nObama re-election probability: %.2f%% \n", 100.0*float64(wins)/float64(numSimulations)) + demWinProb := 100.0*float64(wins)/float64(numSimulations) + fmt.Printf("\n%s election probability: %.2f%%\n", democraticCandidate, demWinProb) + fmt.Printf("%s election probability: %.2f%%\n", republicanCandidate, 100.0 - demWinProb) avgVotes := float64(totalVotes) / float64(numSimulations) roundedVotes := int(math.Floor(avgVotes + 0.5)) - fmt.Printf("Average electoral votes for Obama: %v\n\n", roundedVotes) - + fmt.Printf("Average electoral votes for %s: %v\n", democraticCandidate, roundedVotes) + fmt.Printf("Average electoral votes for %s: %v\n", republicanCandidate, 538 - roundedVotes) } diff --git a/license.txt b/license.txt index a6cb84b..c7d892a 100644 --- a/license.txt +++ b/license.txt @@ -1,4 +1,4 @@ -Copyright (c) 2012 Gary Boone +Copyright (c) 2012-2016 Gary Boone Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: diff --git a/parse.go b/parse.go index 03bf596..19bdf18 100644 --- a/parse.go +++ b/parse.go @@ -6,10 +6,11 @@ package main import ( "log" + "fmt" "strings" ) -func parseResponses(state string, poll Poll, responses []Responses) (obama, romney int) { +func parseResponses(state string, poll Poll, responses []Responses) (democrat, republican int) { for _, resp := range responses { if resp.Choice == nil { log.Printf(" No Choice for %v state poll by '%v'. Skipping.\n", @@ -21,17 +22,17 @@ func parseResponses(state string, poll Poll, responses []Responses) (obama, romn state, *poll.Pollster) continue } - if strings.EqualFold(*resp.Choice, "obama") { - obama = *resp.Value + if strings.EqualFold(*resp.Choice, democraticCandidate) { + democrat = *resp.Value } - if strings.EqualFold(*resp.Choice, "romney") { - romney = *resp.Value + if strings.EqualFold(*resp.Choice, republicanCandidate) { + republican = *resp.Value } } return } -func parseSubpopulation(state string, poll Poll, sub Subpopulations) (obama, romney, size int) { +func parseSubpopulation(state string, poll Poll, sub Subpopulations) (democrat, republican, size int) { if sub.Observations == nil { log.Printf(" No N for %v state poll by '%v'. Skipping.\n", state, *poll.Pollster) @@ -39,7 +40,7 @@ func parseSubpopulation(state string, poll Poll, sub Subpopulations) (obama, rom } size = *sub.Observations - obama, romney = parseResponses(state, poll, sub.Responses) + democrat, republican = parseResponses(state, poll, sub.Responses) return } @@ -59,24 +60,26 @@ func parseDateAsString(poll Poll) string { return date } -func parsePoll(state string, poll Poll) (obama, romney, size int) { +func parsePoll(state string, poll Poll, topic string) (democrat, republican, size int) { for _, question := range poll.Questions { - if question.Topic != nil && strings.EqualFold(*question.Topic, "2012-president") { + if question.Topic != nil && strings.EqualFold(*question.Topic, topic) { // given multiple subpopulations, prefer likely voters switch len(question.Subpopulations) { case 1: - obama, romney, size = parseSubpopulation(state, poll, question.Subpopulations[0]) + democrat, republican, size = parseSubpopulation(state, poll, question.Subpopulations[0]) default: foundLikelyVoters := false for _, sub := range question.Subpopulations { if sub.Name != nil && strings.EqualFold(*sub.Name, "Likely Voters") { - obama, romney, size = parseSubpopulation(state, poll, sub) + democrat, republican, size = parseSubpopulation(state, poll, sub) foundLikelyVoters = true } } if !foundLikelyVoters { - log.Printf(" No Likely voters in multi-subpopulation poll for "+ + msg := fmt.Sprintf(" No Likely voters in multi-subpopulation poll for "+ "%v state poll by '%v'. Skipping.\n", state, *poll.Pollster) + fmt.Printf(msg) + log.Printf(msg) } } } diff --git a/state.go b/state.go index d499616..68da736 100644 --- a/state.go +++ b/state.go @@ -12,43 +12,43 @@ import ( type StateProbability struct { state string - Obama float64 - Romney float64 + Democrat float64 + Republican float64 N float64 - obamaPerc float64 + democratPerc float64 σ float64 - ObamaProbability float64 + DemocratProbability float64 } // Update state data with a new poll. The new N is calculated with the actual -// number of votes for Obama and Romney, not the N of the poll. The effect +// number of votes for the Democrat and Republican, not the N of the poll. The effect // is to not count undecideds and Others. Essentially, the poll is reduced // to a new poll between the two potential winners. In both cases, that's // what actually happens. Because the N is reduced, the uncertainty is // increased as it should be. func (s *StateProbability) update(oPerc, rPerc, pollSize int) { - obamaVotes := float64(oPerc) * float64(pollSize) / 100.0 - romneyVotes := float64(rPerc) * float64(pollSize) / 100.0 - s.Obama += obamaVotes - s.Romney += romneyVotes - s.N += obamaVotes + romneyVotes + democratVotes := float64(oPerc) * float64(pollSize) / 100.0 + republicanVotes := float64(rPerc) * float64(pollSize) / 100.0 + s.Democrat += democratVotes + s.Republican += republicanVotes + s.N += democratVotes + republicanVotes - s.obamaPerc = s.Obama / s.N - s.σ = math.Sqrt((s.obamaPerc - s.obamaPerc*s.obamaPerc) / s.N) + s.democratPerc = s.Democrat / s.N + s.σ = math.Sqrt((s.democratPerc - s.democratPerc*s.democratPerc) / s.N) if min_σ != 0.0 && s.σ < min_σ { s.σ = min_σ } - s.ObamaProbability = prOverX(0.50, s.obamaPerc, s.σ) + s.DemocratProbability = prOverX(0.50, s.democratPerc, s.σ) } func (s *StateProbability) simulateElection(r *rand.Rand) int { if s.N != 0 { - if r.Float64() < s.ObamaProbability { + if r.Float64() < s.DemocratProbability { return college[s.state].votes } } else { - // give state to 2008 winner - if college[s.state].dem2008 { + // give state to winner of last election + if college[s.state].lastElection { return college[s.state].votes } } @@ -57,13 +57,13 @@ func (s *StateProbability) simulateElection(r *rand.Rand) int { func (s *StateProbability) logStateProbability() { if s.N != 0 { - log.Printf(" %v: Obama polling=%6.4f, N=%d, σ=%6.4f --> Pr(Obama)=%6.4f\n", - s.state, s.obamaPerc, int(s.N), s.σ, s.ObamaProbability) + log.Printf(" %v: Democrat polling=%6.4f, N=%d, σ=%6.4f --> Pr(Democrat)=%6.4f\n", + s.state, s.democratPerc, int(s.N), s.σ, s.DemocratProbability) } else { - if college[s.state].dem2008 { - log.Printf(" %s voted Democratic in 2008.\n", s.state) + if college[s.state].lastElection { + log.Printf(" %s voted Democratic in the last election. Assuming %d votes for the Democrat.\n", s.state, college[s.state].votes) } else { - log.Printf(" %s voted Republican in 2008.\n", s.state) + log.Printf(" %s voted Republican in the last election. Assuming %d votes for the Republican\n", s.state, college[s.state].votes) } } }