Week 8, Lecture 14
October 22, 2026
(A “blue wave” means large Democratic gains across many races.)
“The big unknown behind Democrats’ polling leads” Axios, October 10, 2026
Over the next two weeks, we’ll build simulations to investigate these questions.
for loops and vectorization)Michigan, just before the 2024 presidential election:
| Pollster | Field dates, 2024 | Responses | Harris’s lead |
|---|---|---|---|
| Marist | Oct. 27–30 | 1,214 | +3 points |
| Emerson | Oct. 30–Nov. 2 | 790 | +2 points |
| Research Co. | Nov. 2–3 | 450 | +2 points |
| NYT/Siena | Oct. 29–Nov. 2 | 998 | Tied |
| AtlasIntel | Nov. 3–4 | 1,113 | −1.5 points |
Marist · Emerson · AtlasIntel
We’ll use Research Co.’s sample size for our first simulation.
The poll put Harris ahead by two points, reported a margin of error of ±4.6 points for each candidate’s share, and Trump won by 1.41 points. Does that outcome surprise you? How could we work out how often a poll would put the losing candidate ahead?
Research Co. surveyed 450 likely voters.
poll_example |>
select(harris_pct, trump_pct, harris_lead, reported_moe)The margin of error describes uncertainty in each candidate’s share.
For an estimated proportion \(\hat p\), the approximate 95% margin of error is \(1.96\sqrt{\hat p(1-\hat p)/n}\).
Research Co. surveyed 450 people, and 49% supported Harris. Using the binomial model, what margin of error would we expect? Does it match the reported ±4.6 percentage points? Give your calculation and answer.
100 * 1.96 * sqrt(.49 * (1 - .49) / 450)The result is approximately 4.62 percentage points, matching the reported ±4.6 after rounding.
Our simulation uses the same sample size and a Harris proportion close to 50%, giving approximately the same margin of error.
How easy is it for a poll to put the losing candidate ahead? We’ll study how to answer this question using both theoretical calculations and simulations.
election_result gives the certified Harris and Trump vote counts.
election_resultFor our first model, keep these two candidates and treat their final proportions as fixed.
What is the probability that a randomly selected Harris-or-Trump voter supports Harris?
p_harris <- 2736533 / (2736533 + 2816636)
p_harrisEach vote supports Harris with probability about 49.28%.
Let \(H\) be the number of Harris responses in a poll of 450 people.
\[ H \sim \operatorname{Binomial}(450,\ p_{\text{Harris}}). \]
rbinom() generates a random count from a binomial model.
harris_votes <- rbinom(n = 1, size = 450, prob = p_harris)
harris_votesn = 1 asks for one imaginary poll.size = 450 gives the number of people polled.prob = p_harris gives each person’s chance of supporting Harris.The result is how many of those 450 people support Harris.
The remaining responses support Trump. Convert the counts to a lead:
harris_votes <- rbinom(1, size = 450, prob = p_harris)
trump_votes <- 450 - harris_votes
lead <- 100 * (harris_votes - trump_votes) / 450
leadRun it several times. Does your imaginary poll always put Trump ahead?
Changing the n = ... asks R to run more polls.
harris_counts <- rbinom(10, size = 450, prob = p_harris)
harris_counts
100 * (2 * harris_counts / 450 - 1)About what fraction of the time is Harris leading in these imaginary polls? Run the code a few times and use the results to estimate the fraction.
Generate enough polls to estimate that frequency.
harris_counts <- rbinom(10000, size = 450, prob = p_harris)
harris_leads <- 100 * (2 * harris_counts / 450 - 1)
100 * mean(harris_leads > 0)The answer should be around 36%. Run it again: why does the estimate change?
We’ll build the poll one response at a time so we can add these steps.
sample() randomly chooses responses. The vote counts provide their relative probabilities through prob.
votes <- sample(election_result$candidate, size = 450,
replace = TRUE, prob = election_result$votes)
head(votes)replace = TRUE keeps the probabilities the same on every independent draw.
votes == "Harris" checks each response. Its mean is Harris’s share.
votes <- sample(election_result$candidate, size = 450,
replace = TRUE, prob = election_result$votes)
harris_share <- mean(votes == "Harris")
harris_lead <- 100 * (2 * harris_share - 1)
harris_leadThe next sample will generally give us a different lead.
election_result
# Draw one poll and calculate Harris's lead.votes <- sample(election_result$candidate, size = 450,
replace = TRUE, prob = election_result$votes)harris_share <- mean(votes == "Harris")harris_lead <- 100 * (2 * harris_share - 1)
harris_leadRun the completed poll several times.
We want a place to record each polling lead.
numeric(3) creates a vector with three entries, initially zero.
leads <- numeric(3)
leadsWe’ll replace each entry with the result of one simulated poll.
Square brackets select a position in a vector. leads[1] is its first entry.
leads <- numeric(3)
votes <- sample(election_result$candidate, size = 450,
replace = TRUE, prob = election_result$votes)
leads[1] <- 100 * (2 * mean(votes == "Harris") - 1)
leadsWhat would we change to record the second poll?
for (i in 1:3) runs the code inside the braces three times. The value of i is 1, then 2, then 3.
leads <- numeric(3)
for (i in 1:3) {
votes <- sample(election_result$candidate, size = 450,
replace = TRUE, prob = election_result$votes)
leads[i] <- 100 * (2 * mean(votes == "Harris") - 1)
}
leadsi = 1, we draw a poll and store its lead in leads[1].i = 2, we draw a new poll and store its lead in leads[2].i = 3, we draw another poll and store its lead in leads[3].The population and sample size stay fixed. The sampled responses change.
leads <- numeric(10000)
for (i in 1:10000) {
# Draw a poll of 450 people.
# Save Harris's lead in leads[i].
}leads <- numeric(10000)
for (i in 1:10000) {
votes <- sample(election_result$candidate, size = 450,
replace = TRUE, prob = election_result$votes)
leads[i] <- 100 * (2 * mean(votes == "Harris") - 1)
}simulated_polls <- tibble(harris_lead = leads)tibble() puts our vector into a column named harris_lead. Each row is one simulated poll.
simulated_polls |> head(5)What should its histogram look like, based on the distribution we calculated?
simulated_polls
# Plot Harris's lead across the simulated polls.Make a histogram of Harris’s lead across the 10,000 simulated polls. Label the horizontal axis in percentage points. Submit the plot and describe how it relates to the imaginary polls generated with rbinom().
simulated_polls |>
ggplot(aes(harris_lead)) +
geom_histogram(fill = "#346888", color = "white") + geom_vline(xintercept = 0, color = "#B85D24", linewidth = 1) +
coord_cartesian(xlim = c(-20, 20)) +
labs(x = "Harris's polling lead (percentage points)",
y = "Number of polls")simulated_polls |> head(3)
# Find the percentage of polls putting Harris ahead.simulated_polls |>
summarize(percent = 100 * mean(harris_lead > 0))Both approaches should put Harris ahead in about 36% of polls. More repetitions make the estimate more stable.
What do you think will happen if we increase the number of people in each poll? Predict how the distribution of polling leads will change and explain your reasoning.
n <- 450leads <- numeric(10000)
for (i in 1:10000) {
votes <- sample(election_result$candidate, size = n,
replace = TRUE, prob = election_result$votes)
leads[i] <- 100 * (2 * mean(votes == "Harris") - 1)
}
large_polls <- tibble(harris_lead = leads)
# Calculate how often Harris is ahead.The distribution narrows around the same underlying lead.
What changes if the people answering the poll differ from the people who vote?
We’ll use Census data to give our model Michigan’s age distribution.
michigan_ages contains Census estimates of Michigan residents who are U.S. citizens aged 18 or older, grouped by age.
michigan_agesAbout one in five are aged 18–29.
The age groups have different sizes. We want those sizes reflected in our random sample.
prob tells sample() how much weight to give each possible age group. We can use the Census counts as weights.
ages <- sample(michigan_ages$age_group, size = 1000,
replace = TRUE, prob = michigan_ages$citizens)
table(ages)Keep the Census age distribution. For this experiment, assume:
These support and turnout rates are classroom assumptions.
Within each age group, simulate preference and participation independently.
Younger voters favor Harris in this model, but are less likely to vote. How should the lead among actual voters differ from the lead among all sampled citizens? Describe the steps needed to simulate that change.
A coin toss has two outcomes. Code heads as 1 and tails as 0.
rbinom(n = 1, size = 1, prob = .5)n = 1 simulates one toss.size = 1 gives one trial per toss.prob = .5 makes heads and tails equally likely.Run it several times. What would changing prob to .4 do?
We need to decide whether each person votes.
Treat voting as a coin toss with a 40% chance of returning 1. Here 1 means the person votes; 0 means they stay home.
rbinom(n = 1, size = 1, prob = .4)
rbinom(n = 10, size = 1, prob = .4)Sample 1,000 ages, then attach our support and turnout assumptions.
people <- tibble(
age_group = sample(michigan_ages$age_group, 1000, replace = TRUE,
prob = michigan_ages$citizens)
) |>
mutate(
harris_prob = ifelse(age_group == "18-29", .65, .475),
turnout_prob = ifelse(age_group == "18-29", .4, .8)
)
head(people)Use separate coin tosses for preference and turnout. Giving rbinom() a column of probabilities lets each person have their own chance of a 1.
people <- tibble(
age_group = sample(michigan_ages$age_group, 1000, replace = TRUE,
prob = michigan_ages$citizens)
) |>
mutate(harris_prob = ifelse(age_group == "18-29", .65, .475),
turnout_prob = ifelse(age_group == "18-29", .4, .8))
people <- people |>
mutate(harris = rbinom(n = 1000, size = 1, prob = harris_prob),
voted = rbinom(n = 1000, size = 1, prob = turnout_prob))
head(people)harris records preference; voted records participation.
people <- tibble(
age_group = sample(michigan_ages$age_group, 1000, replace = TRUE,
prob = michigan_ages$citizens)
) |>
mutate(harris_prob = ifelse(age_group == "18-29", .65, .475),
turnout_prob = ifelse(age_group == "18-29", .4, .8),
harris = rbinom(n = 1000, size = 1, prob = harris_prob),
voted = rbinom(n = 1000, size = 1, prob = turnout_prob))
people |> summarize(lead = 100 * (2 * mean(harris) - 1))
people |> filter(voted == 1) |>
summarize(lead = 100 * (2 * mean(harris) - 1))The second calculation includes only people who voted.
We want to rerun this process at different younger-voter turnout rates. A function names the procedure and gives us an input we can change.
function(young_turnout) defines that input. The last expression supplies the result.
turnout_poll <- function(young_turnout) {
people <- tibble(
age_group = sample(michigan_ages$age_group, 1000, replace = TRUE,
prob = michigan_ages$citizens)
) |>
mutate(harris_prob = ifelse(age_group == "18-29", .65, .475),
turnout_prob = ifelse(age_group == "18-29", young_turnout, .8),
harris = rbinom(n = 1000, size = 1, prob = harris_prob),
voted = rbinom(n = 1000, size = 1, prob = turnout_prob))
voters <- people |> filter(voted == 1)
100 * (2 * mean(voters$harris) - 1)
}
turnout_poll(.4)The loop now calls our function once per repetition.
turnout_poll <- function(young_turnout) {
people <- tibble(
age_group = sample(michigan_ages$age_group, 1000, replace = TRUE,
prob = michigan_ages$citizens)
) |>
mutate(harris_prob = ifelse(age_group == "18-29", .65, .475),
turnout_prob = ifelse(age_group == "18-29", young_turnout, .8),
harris = rbinom(n = 1000, size = 1, prob = harris_prob),
voted = rbinom(n = 1000, size = 1, prob = turnout_prob))
voters <- people |> filter(voted == 1)
100 * (2 * mean(voters$harris) - 1)
}
leads <- numeric(2000)
for (i in 1:2000) {
leads[i] <- turnout_poll(.4)
}
turnout_results <- tibble(harris_lead = leads)
turnout_results |>
summarize(mean_lead = mean(harris_lead),
percent_harris_ahead = 100 * mean(harris_lead > 0))turnout_poll <- function(young_turnout) {
people <- tibble(
age_group = sample(michigan_ages$age_group, 1000, replace = TRUE,
prob = michigan_ages$citizens)
) |>
mutate(harris_prob = ifelse(age_group == "18-29", .65, .475),
turnout_prob = ifelse(age_group == "18-29", young_turnout, .8),
harris = rbinom(n = 1000, size = 1, prob = harris_prob),
voted = rbinom(n = 1000, size = 1, prob = turnout_prob))
voters <- people |> filter(voted == 1)
100 * (2 * mean(voters$harris) - 1)
}
leads <- numeric(2000)
for (i in 1:2000) {
leads[i] <- turnout_poll(.4)
}
turnout_results <- tibble(harris_lead = leads)
ggplot(turnout_results, aes(harris_lead)) +
geom_histogram(binwidth = 1, fill = "#346888", color = "white") +
geom_vline(xintercept = 0) +
labs(x = "Harris lead among voters (percentage points)", y = "Simulations")Open the simulation fold on the preceding slide and change .4 to .8 in the function call.
Which parts of the model stayed fixed? What changed the outcome?
Under these assumptions:
We generated the distribution by repeatedly executing the sampling, preference, and turnout steps.
Explain how Harris can lead among the people sampled but trail among those who vote in our model. Use a result from the simulation. What would you need to learn about Michigan voters to choose more realistic turnout probabilities?