The U.S. presidential election and the dreaded electoral college

Polls are inherently volatile. Because we are only able to sample a tiny percentage of the voting population, we can only estimate the true percentage of voters favoring a candidate. For this reason, most polls include a margin of error, or a percentage value on either side of the polling percentage. For example, a poll may indicate 51% support for one candidate with a 3% margin of error, in which case the poll projects with some degree of confidence (say, 95% confidence) that the true percentage supporting that candidate is between 48% and 54%.

We will take a moment to review how the U.S. presidential election works. Each of the 50 states, along with the District of Columbia (D.C.), receives two fixed electoral votes and a number of additional of electoral votes that are directly related to their population. With two exceptions (Maine and Nebraska), each state is winner take all; the candidate who receives the most votes in a state earns all of the electoral votes in that state. There are 538 total votes (hence FiveThirtyEight’s name), and so a candidate must win 270 votes to win the presidency.

The actual election process is more complicated, since electoral votes are used to “elect” party members from the winning candidate’s party. This is by design — the Electoral College was created to provide a final barrier to prevent Americans from electing a highly undesirable candidate. In 2016, a record seven of the 538 electors defected (five Democrats and two Republicans), all of whom voting for someone who was not running in the election.

Monte Carlo simulation

Because the presidential election is determined by the Electoral College, it makes sense to use state polling data to forecast the election. Our plan is therefore to simulate the election state-by-state many times, determining who wins the most Electoral College votes each time (or whether there was a tie). At the end, we can estimate our confidence in a candidate winning the election as the fraction of all simulations that candidate wins (see figure below).

A FiveThirtyEight figure showing the variability in the number of electoral votes won by Clinton and Trump across thousands of simulations. The height of each peak represents the percentage of simulations in which a given candidate won the corresponding number of electoral votes in a simulation. The charts are mirror images of each other because in a given election simulation, there are always a fixed number (538) of electoral votes to be divided between the two candidates.

A key point is that we must allow for some variation in our simulations by incorporating randomness, or else the simulations will all turn out the same. The principle of using a large number of randomized simulations to provide an estimate of a desired result is called Monte Carlo simulation.

Monte Carlo simulation is ubiquitous in practical applications. When your phone tells you that there is a 40% chance of rain at 3 PM, it is because meteorologists have run many simulations and found that it is raining at 3 PM in approximately 40% of these simulations. If you want to gain an edge in a daily fantasy sports contest, you might run many Monte Carlo simulations based on previous performance to identify a strong lineup of players. And for another political example, if you wanted to prove that legislative districts have been drawn unfairly, then you could draw many political maps randomly and compare how many districts favor one party in the simulations and in reality, an argument made by a Carnegie Mellon professor that led to a 2018 redistricting of Pennsylvania (see figures below).

Pennsylvania’s 7th Congressional District over six decades, as it changed from a relatively convex shape to a barely contiguous region that has been likened to Goofy kicking Donald Duck. Source: https://www.fairdistrictspa.com/the-problem/about-gerrymandering.
The 2016 (left) and 2018 (right) Congressional district maps of Pennsylvania. The 2018 district map was drawn by the Supreme Court of Pennsylvania, which ruled that the 2016 map had been unfairly drawn to benefit Republicans. Pivotal testimony was given by a Carnegie Mellon mathematics professor, who ran trillions of random simulations to show that the map on the left was more unfair than practically every map that had been randomly generated.

In this chapter, our goal is to apply Monte Carlo simulation to implement and interpret our own election forecast algorithm. But before we get ahead of ourselves, let’s learn a bit more about Monte Carlo simulation by applying it to a simpler example.

An example of Monte Carlo simulation: estimating the house edge of craps

Monte Carlo simulation earns its name from the famed casino in Monaco, and so let’s see how random simulations can answer a question about a game of chance. In particular, if you play a given game with a fixed strategy, what is your average payout or loss in a single game over time? Because games are constructed in favor of the casino (or “house”), this average result is always a loss to the player and is called the house edge.

We can compute the house edge of a casino game via Monte Carlo simulation by programming a computer to “play” the game millions of times with the same strategy. The computer sums the total payout/loss over all these trials and then divides this payout/loss by the number of trials.

The simplest type of casino game we will call a binary game, which consists of two equal and opposite outcomes: the player either loses the game along with their wager, or the player wins and receives back their wager in addition to an amount equal to their wager. If we assume that the player wagers a single unit of currency each time, then a win adds one unit to the player’s holdings, and a loss subtracts one unit from them.

Most casino games are not binary games because they have payouts that vary depending on the outcome as well as the player’s decisions., We will instead focus on a simple binary formulation of craps, in which a player makes a wager and rolls two six-sided dice. There are three possibilities based on the sum of the dice on the first roll, which we denote x.

  1. If x is 7 or 11, then the player wins, and receives their wager back in addition to the amount of the wager.
  2. If x is 2, 3, or 12, then the player loses the wager.
  3. If x has some other value, then the player continues to roll the dice. On these subsequent rolls, the game stops if x is rolled, in which case the player wins, or if 7 is rolled, in which case the player loses. (Note that this is different to the first roll, when 7 is a winner.)
Craps House Edge Problem

Input: An integer numTrials.

Output: An estimate of the house edge of craps resulting from running numTrials randomized simulations.

We can estimate the house edge of craps with the following pseudocode function ComputeCrapsHouseEdge(). This function assumes a subroutine that we will soon write called PlayCrapsOnce() that simulates one game of craps and returns true if the player wins and false if the player loses. Note also that the for loop does not declare a new variable since one is not needed within the loop.

ComputeCrapsHouseEdge(numTrials)
    count ← 0
    for numTrials total trials
        outcome ← PlayCrapsOnce()
        if outcome = true
            count ← count + 1
        else
            count ← count − 1
    return count/numTrials

No randomness is apparent in ComputeCrapsHouseEdge() as we have passed this detail to PlayCrapsOnce(). Furthermore, the ComputeCrapsHouseEdge() function could be used for any binary game; implementing the randomness and the rules of craps will be passed down to the subroutine. This exemplifies a paradigm of program design that we will delve into in the next chapter.

Exercise: Before continuing, try writing a PlayCrapsOnce() function that takes no parameters as input and that returns true or false based on whether the simulated player wins a single game of craps. You should assume a subroutine called SumTwoDice() that takes no input and that returns an integer between 2 and 12 based on the sum on two simulated dice.

We will now write PlayCrapsOnce(), shown below. This function does not take any inputs — we are only interested in the outcome of playing the game, and the rules of the game are built into the function. Implementing these rules will rely on a collection of nested loops and if statements. Also, note that we are still not concerned about randomization details, as we have passed these to a SumTwoDice() subroutine.

PlayCrapsOnce()
    firstRoll ← SumTwoDice()
    if firstRoll = 2, 3, or 12
        return false (player loses)
    else if firstRoll = 7 or 11
        return true (player wins)
    else
        while true
            newRoll ← SumTwoDice()
            if newRoll = firstRoll 
               return true
            else if newRoll = 7
                return false
STOP: Why does the loop while true not produce an infinite loop? (Hint: do you think that anyone who plays a game of craps ever worries about getting stuck in between their first roll and the end of the game?)

We already know all the control structures needed to implement ComputeHouseEdge() and PlayCrapsOnce() on a computer, except for the two lines that ask us to compute the sum of rolling two dice. To be precise, we could even state this as a computational problem.

Sum of Two Dice Problem

Input: (No input)

Output: The sum of the values on two simulated six-sided fair dice.

To complete our craps simulator, we now just need to implement the randomized process of rolling two dice; how hard could that be?

close

Love P4❤️? Join us and help share our journey!

Page Index