Based on Example: modeling political preference given income Logistic regression, identifiability, and separation. See Chapters 13 and 14 in Regression and Other Stories.

Load packages

# library("arm")
# library("foreign")
library("rstanarm")
library(ggplot2)
library(car)
library(effects)
library(vcd)

Set root for data files. I run this in the directory for ROS-Examples.

library(here)
root <- here

Load data

nes <- read.table(root("NES/data","nes.txt"), header=TRUE)
head(nes, 4)
    year resid weight1 weight2 weight3 age gender race educ1 urban region
536 1952     1       1       1       1  25      2    1     2     2      1
537 1952     2       1       1       1  33      2    1     1     2      1
538 1952     3       1       1       1  26      2    1     2     2      1
539 1952     4       1       1       1  63      1    1     2     2      1
    income occup1 union religion educ2 educ3 martial_status occup2 icpsr_cty
536      4      2     1        1     3     3              1      2        NA
537      4      6     1        1     1     1              1      6        NA
538      3      6     2        2     3     3              1      6        NA
539      3      3     1        1     2     2              1      3        NA
    fips_cty partyid7 partyid3 partyid3_b str_partyid father_party mother_party
536       NA        6        3          3           3            3            3
537       NA        5        3          3           2            2            2
538       NA        4        2          2           1            1            1
539       NA        7        3          3           4            1           NA
    dlikes rlikes dem_therm rep_therm regis vote regisvote presvote
536      0      1        NA        NA     2    2         3        2
537     -1      3        NA        NA     2    2         3        1
538      0      5        NA        NA     2    2         3        2
539     -1      3        NA        NA     1    2         3        2
    presvote_2party presvote_intent ideo_feel ideo7 ideo cd state inter_pre
536               2               2        NA    NA   NA NA    13        50
537               1               2        NA    NA   NA NA    13        50
538               2               2        NA    NA   NA NA    13        50
539               2               2        NA    NA   NA NA    13        50
    inter_post black female age_sq rep_presvote rep_pres_intent south real_ideo
536         NA     0      1    625            1               1     0        NA
537         NA     0      1   1089            0               1     0        NA
538         NA     0      1    676            1               1     0        NA
539         NA     0      0   3969            1               1     0        NA
    presapprov perfin1 perfin2 perfin presadm age_10 age_sq_10 newfathe newmoth
536         NA      NA      NA     NA      -1    2.5  6.250000        1       1
537         NA      NA      NA     NA      -1    3.3 10.889999        0       0
538         NA      NA      NA     NA      -1    2.6  6.759999       -1      -1
539         NA      NA      NA     NA      -1    6.3 39.690002       -1      NA
    parent_party white year_new income_new   age_new vote.1 age_discrete
536            2     1        1          1 -2.052455      1            1
537            0     1        1          1 -1.252455      1            2
538           -2     1        1          0 -1.952455      1            1
539           NA     1        1          0  1.747545      1            3
    race_adj dvote rvote
536        1     0     1
537        1     1     0
538        1     0     1
539        1     0     1

Use only data from 1992 and remove missing data

The two binary variables of interest are rvote and dvote

ok <- nes$year==1992 & 
      !is.na(nes$rvote) & !is.na(nes$dvote) & 
      (nes$rvote==1 | nes$dvote==1)

nes92 <- nes[ok,]

Stupid plot

plot(rvote ~ income, data = nes92)

Somewhat better

Jitter both x & y. Should really be more careful to keep the result in [0,1]

plot(jitter(rvote) ~ jitter(income), data = nes92,
     xlab = "Income groups (jittered)",
     ylab = "Republican votes (jittered)")
with (nes92, 
  lines(loess.smooth(income, rvote), lwd=3, col="red")
)

Wait – isn’t this just a contingency table?

tab <- with(nes92, table(rvote, income))
tab
     income
rvote   1   2   3   4   5
    0  98 138 222 208  36
    1  30  69 144 196  38
mosaicplot(t(tab), shade=TRUE,
           xlab="Income group", ylab="Republican vote", main="nes92 table")

Fit using logistic regression

treat as a binomial glm

fit_glm <- glm(rvote ~ income, family=binomial(link="logit"), data=nes92)
brief(fit_glm)
              (Intercept) income
Estimate           -1.402 0.3260
Std. Error          0.189 0.0569
exp(Estimate)       0.246 1.3854

 Residual deviance = 1557 on 1177 df

Effect plot

plot(allEffects(fit_glm), 
     xlab= "Income group",
     ylab= "Republican vote")

Plot using ggplot2

ggplot2::ggplot(nes92, aes(x=income, y=rvote)) +
  geom_point(position=position_jitter(height=0.1, width = 0.1)) +
  stat_smooth(method = "glm", method.args=list(family="binomial")) +
  labs(x = "Income group",
       y = "Republican vote")

Could also use vcd::binreg_plot

#binreg_plot(fit_glm, type="link")

Logistic regression of vote preference on income

fit_1 <- stan_glm(rvote ~ income, family=binomial(link="logit"), data=nes92,
                  refresh=0)
print(fit_1)
stan_glm
 family:       binomial [logit]
 formula:      rvote ~ income
 observations: 1179
 predictors:   2
------
            Median MAD_SD
(Intercept) -1.4    0.2  
income       0.3    0.1  

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

Plot logistic regression with jittered points (Fig 13.2)

n <- nrow(nes92)
income_jitt <- nes92$income + runif(n, -.2, .2)
vote_jitt <- nes92$rvote + 
  ifelse(nes92$rvote==0, 
         runif(n, .005, .05), 
         runif(n, -.05, -.005))
par(mar=c(3,3,1,.1), tck=-.01, mgp=c(1.7, .3, 0))

curve(invlogit(fit_1$coef[1] + fit_1$coef[2]*x), 1, 5, ylim=c(0,1),
      xlim=c(-2,8), xaxt="n", xaxs="i", 
      ylab="Pr (Republican vote)", xlab="Income", 
      lwd=5, col="red", yaxs="i")
curve(invlogit(fit_1$coef[1] + fit_1$coef[2]*x), -2, 8, lwd=.5, add=TRUE)
axis(1, 1:5)
mtext("(poor)", 1, 1.2, at=1, adj=.5)
mtext("(rich)", 1, 1.2, at=5, adj=.5)
points(income_jitt, vote_jitt, pch=20, cex=.1)

Display uncertainty from n=20 simulations

par(mar=c(3,3,1,.1), tck=-.01, mgp=c(1.7, .3, 0))
curve (invlogit(fit_1$coef[1] + fit_1$coef[2]*x), .5, 5.5, ylim=c(0,1),
       xlim=c(.5, 5.5), xaxt="n", xaxs="i", 
       ylab="Pr (Republican vote)", xlab="Income", yaxs="i",
       )
axis(1, 1:5)
mtext("(poor)", 1, 1.2, at=1, adj=.5)
mtext("(rich)", 1, 1.2, at=5, adj=.5)

sims_1 <- as.matrix(fit_1)
n_sims <- nrow(sims_1)
for (j in sample(n_sims, 20)){
  curve(invlogit(sims_1[j,1] +sims_1[j,2]*x), .5, 5.5, lwd=.5, col="gray", add=TRUE)
}
curve(invlogit(fit_1$coef[1] + fit_1$coef[2]*x), .5, 5.5, add=TRUE,
      lwd=3, col="red")
points(income_jitt, vote_jitt, pch=20, cex=.1)

IycgLS0tDQojJyB0aXRsZTogIkxvZ2lzdGljIHJlZ3Jlc3Npb246IE5FUyA5MiAiDQojJyBhdXRob3I6ICJNaWNoYWVsIEZyaWVuZGx5Ig0KIycgZGF0ZTogImByIGZvcm1hdChTeXMuRGF0ZSgpKWAiDQojJyBvdXRwdXQ6DQojJyAgIGh0bWxfZG9jdW1lbnQ6DQojJyAgICAgdGhlbWU6IHJlYWRhYmxlDQojJyAgICAgY29kZV9kb3dubG9hZDogdHJ1ZQ0KIycgLS0tDQoNCiMnIEJhc2VkIG9uIEV4YW1wbGU6IG1vZGVsaW5nIHBvbGl0aWNhbCBwcmVmZXJlbmNlIGdpdmVuIGluY29tZQ0KIycgTG9naXN0aWMgcmVncmVzc2lvbiwgaWRlbnRpZmlhYmlsaXR5LCBhbmQgc2VwYXJhdGlvbi4gU2VlIENoYXB0ZXJzDQojJyAxMyBhbmQgMTQgaW4gUmVncmVzc2lvbiBhbmQgT3RoZXIgU3Rvcmllcy4NCiMnIA0KIycgDQoNCiMrIHNldHVwLCBpbmNsdWRlPUZBTFNFDQprbml0cjo6b3B0c19jaHVuayRzZXQobWVzc2FnZT1GQUxTRSwgZXJyb3I9RkFMU0UsIHdhcm5pbmc9RkFMU0UsIGNvbW1lbnQ9TkEpDQoNCiMnICMjIyMgTG9hZCBwYWNrYWdlcw0KIyBsaWJyYXJ5KCJhcm0iKQ0KIyBsaWJyYXJ5KCJmb3JlaWduIikNCmxpYnJhcnkoInJzdGFuYXJtIikNCmxpYnJhcnkoZ2dwbG90MikNCmxpYnJhcnkoY2FyKQ0KbGlicmFyeShlZmZlY3RzKQ0KbGlicmFyeSh2Y2QpDQoNCg0KIycgU2V0IHJvb3QgZm9yIGRhdGEgZmlsZXMuIEkgcnVuIHRoaXMgaW4gdGhlIGRpcmVjdG9yeSBmb3IgUk9TLUV4YW1wbGVzLg0KbGlicmFyeShoZXJlKQ0Kcm9vdCA8LSBoZXJlDQoNCiMnICMjIyBMb2FkIGRhdGENCm5lcyA8LSByZWFkLnRhYmxlKHJvb3QoIk5FUy9kYXRhIiwibmVzLnR4dCIpLCBoZWFkZXI9VFJVRSkNCmhlYWQobmVzLCA0KQ0KDQojJyAjIyMgVXNlIG9ubHkgZGF0YSBmcm9tIDE5OTIgYW5kIHJlbW92ZSBtaXNzaW5nIGRhdGENCiMnIFRoZSB0d28gYmluYXJ5IHZhcmlhYmxlcyBvZiBpbnRlcmVzdCBhcmUgYHJ2b3RlYCBhbmQgYGR2b3RlYA0KIycgDQpvayA8LSBuZXMkeWVhcj09MTk5MiAmIA0KICAgICAgIWlzLm5hKG5lcyRydm90ZSkgJiAhaXMubmEobmVzJGR2b3RlKSAmIA0KICAgICAgKG5lcyRydm90ZT09MSB8IG5lcyRkdm90ZT09MSkNCg0KbmVzOTIgPC0gbmVzW29rLF0NCg0KIycgIyMjIFN0dXBpZCBwbG90DQpwbG90KHJ2b3RlIH4gaW5jb21lLCBkYXRhID0gbmVzOTIpDQoNCiMnICMjIFNvbWV3aGF0IGJldHRlcg0KIycgSml0dGVyIGJvdGggeCAmIHkuICBTaG91bGQgcmVhbGx5IGJlIG1vcmUgY2FyZWZ1bCB0byBrZWVwIHRoZSByZXN1bHQgaW4gWzAsMV0NCnBsb3Qoaml0dGVyKHJ2b3RlKSB+IGppdHRlcihpbmNvbWUpLCBkYXRhID0gbmVzOTIsDQogICAgIHhsYWIgPSAiSW5jb21lIGdyb3VwcyAoaml0dGVyZWQpIiwNCiAgICAgeWxhYiA9ICJSZXB1YmxpY2FuIHZvdGVzIChqaXR0ZXJlZCkiKQ0Kd2l0aCAobmVzOTIsIA0KICBsaW5lcyhsb2Vzcy5zbW9vdGgoaW5jb21lLCBydm90ZSksIGx3ZD0zLCBjb2w9InJlZCIpDQopDQoNCiMnICMjIFdhaXQgLS0gaXNuJ3QgdGhpcyBqdXN0IGEgY29udGluZ2VuY3kgdGFibGU/DQoNCnRhYiA8LSB3aXRoKG5lczkyLCB0YWJsZShydm90ZSwgaW5jb21lKSkNCnRhYg0KDQptb3NhaWNwbG90KHQodGFiKSwgc2hhZGU9VFJVRSwNCiAgICAgICAgICAgeGxhYj0iSW5jb21lIGdyb3VwIiwgeWxhYj0iUmVwdWJsaWNhbiB2b3RlIiwgbWFpbj0ibmVzOTIgdGFibGUiKQ0KDQojJyAjIyMgRml0IHVzaW5nIGxvZ2lzdGljIHJlZ3Jlc3Npb24NCiMnICAgdHJlYXQgYXMgYSBiaW5vbWlhbCBnbG0NCmZpdF9nbG0gPC0gZ2xtKHJ2b3RlIH4gaW5jb21lLCBmYW1pbHk9Ymlub21pYWwobGluaz0ibG9naXQiKSwgZGF0YT1uZXM5MikNCmJyaWVmKGZpdF9nbG0pDQoNCiMnICMjIyBFZmZlY3QgcGxvdA0KcGxvdChhbGxFZmZlY3RzKGZpdF9nbG0pLCANCiAgICAgeGxhYj0gIkluY29tZSBncm91cCIsDQogICAgIHlsYWI9ICJSZXB1YmxpY2FuIHZvdGUiKQ0KDQojJyAjIyBQbG90IHVzaW5nIGdncGxvdDINCmdncGxvdDI6OmdncGxvdChuZXM5MiwgYWVzKHg9aW5jb21lLCB5PXJ2b3RlKSkgKw0KICBnZW9tX3BvaW50KHBvc2l0aW9uPXBvc2l0aW9uX2ppdHRlcihoZWlnaHQ9MC4xLCB3aWR0aCA9IDAuMSkpICsNCiAgc3RhdF9zbW9vdGgobWV0aG9kID0gImdsbSIsIG1ldGhvZC5hcmdzPWxpc3QoZmFtaWx5PSJiaW5vbWlhbCIpKSArDQogIGxhYnMoeCA9ICJJbmNvbWUgZ3JvdXAiLA0KICAgICAgIHkgPSAiUmVwdWJsaWNhbiB2b3RlIikNCg0KIycgQ291bGQgYWxzbyB1c2UgYHZjZDo6YmlucmVnX3Bsb3RgDQojYmlucmVnX3Bsb3QoZml0X2dsbSwgdHlwZT0ibGluayIpDQoNCg0KDQojJyAjIyMjIExvZ2lzdGljIHJlZ3Jlc3Npb24gb2Ygdm90ZSBwcmVmZXJlbmNlIG9uIGluY29tZQ0KZml0XzEgPC0gc3Rhbl9nbG0ocnZvdGUgfiBpbmNvbWUsIGZhbWlseT1iaW5vbWlhbChsaW5rPSJsb2dpdCIpLCBkYXRhPW5lczkyLA0KICAgICAgICAgICAgICAgICAgcmVmcmVzaD0wKQ0KcHJpbnQoZml0XzEpDQoNCiMnICMjIyBQbG90IGxvZ2lzdGljIHJlZ3Jlc3Npb24gd2l0aCBqaXR0ZXJlZCBwb2ludHMgKEZpZyAxMy4yKQ0KbiA8LSBucm93KG5lczkyKQ0KaW5jb21lX2ppdHQgPC0gbmVzOTIkaW5jb21lICsgcnVuaWYobiwgLS4yLCAuMikNCnZvdGVfaml0dCA8LSBuZXM5MiRydm90ZSArIA0KICBpZmVsc2UobmVzOTIkcnZvdGU9PTAsIA0KICAgICAgICAgcnVuaWYobiwgLjAwNSwgLjA1KSwgDQogICAgICAgICBydW5pZihuLCAtLjA1LCAtLjAwNSkpDQpwYXIobWFyPWMoMywzLDEsLjEpLCB0Y2s9LS4wMSwgbWdwPWMoMS43LCAuMywgMCkpDQoNCmN1cnZlKGludmxvZ2l0KGZpdF8xJGNvZWZbMV0gKyBmaXRfMSRjb2VmWzJdKngpLCAxLCA1LCB5bGltPWMoMCwxKSwNCiAgICAgIHhsaW09YygtMiw4KSwgeGF4dD0ibiIsIHhheHM9ImkiLCANCiAgICAgIHlsYWI9IlByIChSZXB1YmxpY2FuIHZvdGUpIiwgeGxhYj0iSW5jb21lIiwgDQogICAgICBsd2Q9NSwgY29sPSJyZWQiLCB5YXhzPSJpIikNCmN1cnZlKGludmxvZ2l0KGZpdF8xJGNvZWZbMV0gKyBmaXRfMSRjb2VmWzJdKngpLCAtMiwgOCwgbHdkPS41LCBhZGQ9VFJVRSkNCmF4aXMoMSwgMTo1KQ0KbXRleHQoIihwb29yKSIsIDEsIDEuMiwgYXQ9MSwgYWRqPS41KQ0KbXRleHQoIihyaWNoKSIsIDEsIDEuMiwgYXQ9NSwgYWRqPS41KQ0KcG9pbnRzKGluY29tZV9qaXR0LCB2b3RlX2ppdHQsIHBjaD0yMCwgY2V4PS4xKQ0KDQojJyAjIyMgRGlzcGxheSB1bmNlcnRhaW50eSBmcm9tIG49MjAgc2ltdWxhdGlvbnMNCiMnICANCnBhcihtYXI9YygzLDMsMSwuMSksIHRjaz0tLjAxLCBtZ3A9YygxLjcsIC4zLCAwKSkNCmN1cnZlIChpbnZsb2dpdChmaXRfMSRjb2VmWzFdICsgZml0XzEkY29lZlsyXSp4KSwgLjUsIDUuNSwgeWxpbT1jKDAsMSksDQogICAgICAgeGxpbT1jKC41LCA1LjUpLCB4YXh0PSJuIiwgeGF4cz0iaSIsIA0KICAgICAgIHlsYWI9IlByIChSZXB1YmxpY2FuIHZvdGUpIiwgeGxhYj0iSW5jb21lIiwgeWF4cz0iaSIsDQogICAgICAgKQ0KYXhpcygxLCAxOjUpDQptdGV4dCgiKHBvb3IpIiwgMSwgMS4yLCBhdD0xLCBhZGo9LjUpDQptdGV4dCgiKHJpY2gpIiwgMSwgMS4yLCBhdD01LCBhZGo9LjUpDQoNCnNpbXNfMSA8LSBhcy5tYXRyaXgoZml0XzEpDQpuX3NpbXMgPC0gbnJvdyhzaW1zXzEpDQpmb3IgKGogaW4gc2FtcGxlKG5fc2ltcywgMjApKXsNCiAgY3VydmUoaW52bG9naXQoc2ltc18xW2osMV0gK3NpbXNfMVtqLDJdKngpLCAuNSwgNS41LCBsd2Q9LjUsIGNvbD0iZ3JheSIsIGFkZD1UUlVFKQ0KfQ0KY3VydmUoaW52bG9naXQoZml0XzEkY29lZlsxXSArIGZpdF8xJGNvZWZbMl0qeCksIC41LCA1LjUsIGFkZD1UUlVFLA0KICAgICAgbHdkPTMsIGNvbD0icmVkIikNCnBvaW50cyhpbmNvbWVfaml0dCwgdm90ZV9qaXR0LCBwY2g9MjAsIGNleD0uMSkNCg0KDQo=