Poststratification after estimation. See Chapter 17 in Regression and Other Stories.

The CBS News poll conducted from 12--16 October 2016 reported that, among likely voters who preferred one of the two major-party candidates, 45% intended to vote for Donald Trump and 55% for Hillary Clinton. Of these respondents, Party ID 33% Republican, 40% Republican, 27% independent.

source: http://www.cbsnews.com/news/cbs-poll-clintons-lead-over-trump-widens-with-three-weeks-to-go/ and https://www.scribd.com/document/327938789/CBS-News-Poll-10-17-toplines

Effective sample size of likely voters

254 Republican, 282 Democrat, 242 Independent

Compare to:

exit polls 2012  32 38 29
exit polls 2016  33 36 31
Republicans:  77% Trump,  8% Clinton (must normalize to 100%)
Democrats:     5% Trump, 89% Clinton (must normalize to 100%)
Independents: 36% Trump, 38% Clinton (must normalize to 100%)

Load packages

library("rprojroot")
root<-has_file(".ROS-Examples-root")$make_fix_file()
library("rstanarm")

Simulate fake data

n_pid <- c(254, 282, 242)
n <- sum(n_pid)
pid_names <- c("Republican", "Democrat", "Independent")
pid <- rep(pid_names, n_pid)
n_vote <- as.list(rep(NA, 3))
n_vote[[1]] <- round(c(0.77, 0.08)*n_pid[1])
n_vote[[2]] <- round(c(0.05, 0.89)*n_pid[2])
n_vote[[3]] <- round(c(0.36, 0.38)*n_pid[3])
vote <- NULL
y_bar_cells <- rep(NA, 3)
for (j in 1:3){
  n_vote[[j]]<- c(n_vote[[j]], n_pid[j] - sum(n_vote[[j]]))
  vote <- c(vote, rep(c(1, 0, NA), n_vote[[j]]))
  y_bar_cells[j] <- mean(vote[pid==pid_names[j]], na.rm=TRUE)
  round(y_bar_cells[j], 3)
}
poll <- data.frame(vote, pid)
# write.csv(poll, root("Poststrat/data","poll.csv"), row.names=FALSE)
# poll <- read.csv(root("Poststrat/data","poll.csv"))
head(poll)
  vote        pid
1    1 Republican
2    1 Republican
3    1 Republican
4    1 Republican
5    1 Republican
6    1 Republican
summary(poll)
      vote               pid     
 Min.   :0.00   Democrat   :282  
 1st Qu.:0.00   Independent:242  
 Median :0.00   Republican :254  
 Mean   :0.45                    
 3rd Qu.:1.00                    
 Max.   :1.00                    
 NA's   :118                     

Simple poststrat

poststrat_data <- data.frame(pid=c("Republican", "Democrat", "Independent"),
                             N=c(0.33, 0.36, 0.31))
round(sum(poststrat_data$N * y_bar_cells), 3)
[1] 0.469

Linear model

Raw estimate

round(mean(poll$vote, na.rm=TRUE), 3)
[1] 0.45

stan_glm

fit_1 <- stan_glm(vote ~ factor(pid), data = poll, refresh = 0)
print(fit_1, digits=2)
stan_glm
 family:       gaussian [identity]
 formula:      vote ~ factor(pid)
 observations: 660
 predictors:   3
------
                       Median MAD_SD
(Intercept)            0.05   0.02  
factor(pid)Independent 0.43   0.03  
factor(pid)Republican  0.85   0.03  

Auxiliary parameter(s):
      Median MAD_SD
sigma 0.34   0.01  

------
* For help interpreting the printed output see ?print.stanreg
* For info on the priors used see ?prior_summary.stanreg

Poststrat using posterior_linpred()

epred_1 <- posterior_epred(fit_1, newdata=poststrat_data)
poststrat_est_1 <- epred_1 %*% poststrat_data$N/sum(poststrat_data$N)
print(c(mean(poststrat_est_1), mad(poststrat_est_1)), digits=2)
[1] 0.469 0.013

Add extra uncertainty

n_sim <- nrow(epred_1)
poststrat_est_2 <- poststrat_est_1 + rnorm(n_sim, 0, 0.02)
print(c(mean(poststrat_est_2), mad(poststrat_est_2)), digits=2)
[1] 0.469 0.025

Logistic model

Fit the regression

fit <- stan_glm(vote ~ factor(pid), family=binomial(link="logit"), data = poll, refresh = 0)
print(fit, digits=2)
stan_glm
 family:       binomial [logit]
 formula:      vote ~ factor(pid)
 observations: 660
 predictors:   3
------
                       Median MAD_SD
(Intercept)            -2.88   0.28 
factor(pid)Independent  2.83   0.31 
factor(pid)Republican   5.17   0.37 

------
* For help interpreting the printed output see ?print.stanreg
* For info on the priors used see ?prior_summary.stanreg

Raw estimate

round(mean(poll$vote, na.rm=TRUE), 3)
[1] 0.45

Poststrat using the predict function

X_population <- data.frame(pid=c("Republican", "Democrat", "Independent"))
N_population <- c(0.33, 0.36, 0.31)
predict_poststrat <- colMeans(posterior_epred(fit, newdata=X_population))
poststrat_est_2 <- sum(N_population*predict_poststrat)/sum(N_population)
round(poststrat_est_2, digits=3)
[1] 0.469

Just to compare, poststrat using the original data

predict_a <- predict(fit, type="response")
round(mean(predict_a), 3)
[1] 0.45

This doesn't work--it just spits back the raw estimate--because it's not using the external population info which is what makes poststrat work.

LS0tCnRpdGxlOiAiUmVncmVzc2lvbiBhbmQgT3RoZXIgU3RvcmllczogUG9zdHN0cmF0aWZpY2F0aW9uIgphdXRob3I6ICJBbmRyZXcgR2VsbWFuLCBKZW5uaWZlciBIaWxsLCBBa2kgVmVodGFyaSIKZGF0ZTogImByIGZvcm1hdChTeXMuRGF0ZSgpKWAiCm91dHB1dDoKICBodG1sX2RvY3VtZW50OgogICAgdGhlbWU6IHJlYWRhYmxlCiAgICB0b2M6IHRydWUKICAgIHRvY19kZXB0aDogMgogICAgdG9jX2Zsb2F0OiB0cnVlCiAgICBjb2RlX2Rvd25sb2FkOiB0cnVlCi0tLQpQb3N0c3RyYXRpZmljYXRpb24gYWZ0ZXIgZXN0aW1hdGlvbi4gU2VlIENoYXB0ZXIgMTcgaW4gUmVncmVzc2lvbgphbmQgT3RoZXIgU3Rvcmllcy4KClRoZSBDQlMgTmV3cyBwb2xsIGNvbmR1Y3RlZCBmcm9tIDEyLS0xNiBPY3RvYmVyIDIwMTYgcmVwb3J0ZWQgdGhhdCwKYW1vbmcgbGlrZWx5IHZvdGVycyB3aG8gcHJlZmVycmVkIG9uZSBvZiB0aGUgdHdvIG1ham9yLXBhcnR5CmNhbmRpZGF0ZXMsIDQ1XCUgaW50ZW5kZWQgdG8gdm90ZSBmb3IgRG9uYWxkIFRydW1wIGFuZCA1NVwlIGZvcgpIaWxsYXJ5IENsaW50b24uICBPZiB0aGVzZSByZXNwb25kZW50cywgUGFydHkgSUQgMzNcJSBSZXB1YmxpY2FuLAo0MFwlIFJlcHVibGljYW4sIDI3XCUgaW5kZXBlbmRlbnQuCgpzb3VyY2U6ICBodHRwOi8vd3d3LmNic25ld3MuY29tL25ld3MvY2JzLXBvbGwtY2xpbnRvbnMtbGVhZC1vdmVyLXRydW1wLXdpZGVucy13aXRoLXRocmVlLXdlZWtzLXRvLWdvLwphbmQgaHR0cHM6Ly93d3cuc2NyaWJkLmNvbS9kb2N1bWVudC8zMjc5Mzg3ODkvQ0JTLU5ld3MtUG9sbC0xMC0xNy10b3BsaW5lcwoKRWZmZWN0aXZlIHNhbXBsZSBzaXplIG9mIGxpa2VseSB2b3RlcnMKYGBgCjI1NCBSZXB1YmxpY2FuLCAyODIgRGVtb2NyYXQsIDI0MiBJbmRlcGVuZGVudApgYGAKCkNvbXBhcmUgdG86CmBgYApleGl0IHBvbGxzIDIwMTIgIDMyIDM4IDI5CmV4aXQgcG9sbHMgMjAxNiAgMzMgMzYgMzEKYGBgCgpgYGAKUmVwdWJsaWNhbnM6ICA3NyUgVHJ1bXAsICA4JSBDbGludG9uIChtdXN0IG5vcm1hbGl6ZSB0byAxMDAlKQpEZW1vY3JhdHM6ICAgICA1JSBUcnVtcCwgODklIENsaW50b24gKG11c3Qgbm9ybWFsaXplIHRvIDEwMCUpCkluZGVwZW5kZW50czogMzYlIFRydW1wLCAzOCUgQ2xpbnRvbiAobXVzdCBub3JtYWxpemUgdG8gMTAwJSkKYGBgCi0tLS0tLS0tLS0tLS0KCgpgYGBge3Igc2V0dXAsIGluY2x1ZGU9RkFMU0V9CmtuaXRyOjpvcHRzX2NodW5rJHNldChtZXNzYWdlPUZBTFNFLCBlcnJvcj1GQUxTRSwgd2FybmluZz1GQUxTRSwgY29tbWVudD1OQSkKYGBgYAoKIyMjIyBMb2FkIHBhY2thZ2VzCgpgYGBge3IgfQpsaWJyYXJ5KCJycHJvanJvb3QiKQpyb290PC1oYXNfZmlsZSgiLlJPUy1FeGFtcGxlcy1yb290IikkbWFrZV9maXhfZmlsZSgpCmxpYnJhcnkoInJzdGFuYXJtIikKYGBgYAoKIyMgU2ltdWxhdGUgZmFrZSBkYXRhCgpgYGBge3IgfQpuX3BpZCA8LSBjKDI1NCwgMjgyLCAyNDIpCm4gPC0gc3VtKG5fcGlkKQpwaWRfbmFtZXMgPC0gYygiUmVwdWJsaWNhbiIsICJEZW1vY3JhdCIsICJJbmRlcGVuZGVudCIpCnBpZCA8LSByZXAocGlkX25hbWVzLCBuX3BpZCkKbl92b3RlIDwtIGFzLmxpc3QocmVwKE5BLCAzKSkKbl92b3RlW1sxXV0gPC0gcm91bmQoYygwLjc3LCAwLjA4KSpuX3BpZFsxXSkKbl92b3RlW1syXV0gPC0gcm91bmQoYygwLjA1LCAwLjg5KSpuX3BpZFsyXSkKbl92b3RlW1szXV0gPC0gcm91bmQoYygwLjM2LCAwLjM4KSpuX3BpZFszXSkKdm90ZSA8LSBOVUxMCnlfYmFyX2NlbGxzIDwtIHJlcChOQSwgMykKZm9yIChqIGluIDE6Myl7CiAgbl92b3RlW1tqXV08LSBjKG5fdm90ZVtbal1dLCBuX3BpZFtqXSAtIHN1bShuX3ZvdGVbW2pdXSkpCiAgdm90ZSA8LSBjKHZvdGUsIHJlcChjKDEsIDAsIE5BKSwgbl92b3RlW1tqXV0pKQogIHlfYmFyX2NlbGxzW2pdIDwtIG1lYW4odm90ZVtwaWQ9PXBpZF9uYW1lc1tqXV0sIG5hLnJtPVRSVUUpCiAgcm91bmQoeV9iYXJfY2VsbHNbal0sIDMpCn0KcG9sbCA8LSBkYXRhLmZyYW1lKHZvdGUsIHBpZCkKIyB3cml0ZS5jc3YocG9sbCwgcm9vdCgiUG9zdHN0cmF0L2RhdGEiLCJwb2xsLmNzdiIpLCByb3cubmFtZXM9RkFMU0UpCiMgcG9sbCA8LSByZWFkLmNzdihyb290KCJQb3N0c3RyYXQvZGF0YSIsInBvbGwuY3N2IikpCmhlYWQocG9sbCkKc3VtbWFyeShwb2xsKQpgYGBgCgojIyBTaW1wbGUgcG9zdHN0cmF0CgpgYGBge3IgfQpwb3N0c3RyYXRfZGF0YSA8LSBkYXRhLmZyYW1lKHBpZD1jKCJSZXB1YmxpY2FuIiwgIkRlbW9jcmF0IiwgIkluZGVwZW5kZW50IiksCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgTj1jKDAuMzMsIDAuMzYsIDAuMzEpKQpyb3VuZChzdW0ocG9zdHN0cmF0X2RhdGEkTiAqIHlfYmFyX2NlbGxzKSwgMykKYGBgYAoKIyMgTGluZWFyIG1vZGVsCiMjIyMgUmF3IGVzdGltYXRlCgpgYGBge3IgfQpyb3VuZChtZWFuKHBvbGwkdm90ZSwgbmEucm09VFJVRSksIDMpCmBgYGAKCiMjIyMgc3Rhbl9nbG0KCmBgYGB7ciB9CmZpdF8xIDwtIHN0YW5fZ2xtKHZvdGUgfiBmYWN0b3IocGlkKSwgZGF0YSA9IHBvbGwsIHJlZnJlc2ggPSAwKQpwcmludChmaXRfMSwgZGlnaXRzPTIpCmBgYGAKCiMjIyMgUG9zdHN0cmF0IHVzaW5nIHBvc3Rlcmlvcl9saW5wcmVkKCkKCmBgYGB7ciB9CmVwcmVkXzEgPC0gcG9zdGVyaW9yX2VwcmVkKGZpdF8xLCBuZXdkYXRhPXBvc3RzdHJhdF9kYXRhKQpwb3N0c3RyYXRfZXN0XzEgPC0gZXByZWRfMSAlKiUgcG9zdHN0cmF0X2RhdGEkTi9zdW0ocG9zdHN0cmF0X2RhdGEkTikKcHJpbnQoYyhtZWFuKHBvc3RzdHJhdF9lc3RfMSksIG1hZChwb3N0c3RyYXRfZXN0XzEpKSwgZGlnaXRzPTIpCmBgYGAKCiMjIyMgQWRkIGV4dHJhIHVuY2VydGFpbnR5CgpgYGBge3IgfQpuX3NpbSA8LSBucm93KGVwcmVkXzEpCnBvc3RzdHJhdF9lc3RfMiA8LSBwb3N0c3RyYXRfZXN0XzEgKyBybm9ybShuX3NpbSwgMCwgMC4wMikKcHJpbnQoYyhtZWFuKHBvc3RzdHJhdF9lc3RfMiksIG1hZChwb3N0c3RyYXRfZXN0XzIpKSwgZGlnaXRzPTIpCmBgYGAKCiMjIExvZ2lzdGljIG1vZGVsCiMjIyMgRml0IHRoZSByZWdyZXNzaW9uCgpgYGBge3IgfQpmaXQgPC0gc3Rhbl9nbG0odm90ZSB+IGZhY3RvcihwaWQpLCBmYW1pbHk9Ymlub21pYWwobGluaz0ibG9naXQiKSwgZGF0YSA9IHBvbGwsIHJlZnJlc2ggPSAwKQpwcmludChmaXQsIGRpZ2l0cz0yKQpgYGBgCgojIyMjIFJhdyBlc3RpbWF0ZQoKYGBgYHtyIH0Kcm91bmQobWVhbihwb2xsJHZvdGUsIG5hLnJtPVRSVUUpLCAzKQpgYGBgCgojIyMjIFBvc3RzdHJhdCB1c2luZyB0aGUgcHJlZGljdCBmdW5jdGlvbgoKYGBgYHtyIH0KWF9wb3B1bGF0aW9uIDwtIGRhdGEuZnJhbWUocGlkPWMoIlJlcHVibGljYW4iLCAiRGVtb2NyYXQiLCAiSW5kZXBlbmRlbnQiKSkKTl9wb3B1bGF0aW9uIDwtIGMoMC4zMywgMC4zNiwgMC4zMSkKcHJlZGljdF9wb3N0c3RyYXQgPC0gY29sTWVhbnMocG9zdGVyaW9yX2VwcmVkKGZpdCwgbmV3ZGF0YT1YX3BvcHVsYXRpb24pKQpwb3N0c3RyYXRfZXN0XzIgPC0gc3VtKE5fcG9wdWxhdGlvbipwcmVkaWN0X3Bvc3RzdHJhdCkvc3VtKE5fcG9wdWxhdGlvbikKcm91bmQocG9zdHN0cmF0X2VzdF8yLCBkaWdpdHM9MykKYGBgYAoKIyMjIyBKdXN0IHRvIGNvbXBhcmUsIHBvc3RzdHJhdCB1c2luZyB0aGUgb3JpZ2luYWwgZGF0YQoKYGBgYHtyIH0KcHJlZGljdF9hIDwtIHByZWRpY3QoZml0LCB0eXBlPSJyZXNwb25zZSIpCnJvdW5kKG1lYW4ocHJlZGljdF9hKSwgMykKYGBgYAoKVGhpcyBkb2Vzbid0IHdvcmstLWl0IGp1c3Qgc3BpdHMgYmFjayB0aGUgcmF3IGVzdGltYXRlLS1iZWNhdXNlIGl0J3Mgbm90IHVzaW5nIHRoZSBleHRlcm5hbCBwb3B1bGF0aW9uIGluZm8gd2hpY2ggaXMgd2hhdCBtYWtlcyBwb3N0c3RyYXQgd29yay4KCg==