Load packages and the data

library(vcd)
library(vcdExtra)
library(ggplot2)

data(HospVisits, package="vcdExtra")
HospVisits
##             stay
## visit        2-9 10-19 20+
##   Regular     43    16   3
##   Infrequent   6    11  10
##   Never        9    18  16

Calculate log odds

vcd::lodds calculates log odds for comparisons of adjacent categories, for one response variable

lodds(HospVisits, response="stay") 
## log odds for visit by stay 
## 
##            visit
## stay        Regular Infrequent  Never
##   2-9:10-19   0.989    -0.6061 -0.693
##   10-19:20+   1.674     0.0953  0.118

Make it into a data.frame, including the ASE

hosp.lodds <- as.data.frame(lodds(HospVisits, response="stay"))

Fit models

Here, I use 1/ASE^2 as the weights, in a weighted least squares

mod.null  <- lm(logodds ~ -1, weights=1/ASE^2, data=hosp.lodds)     # null
mod.const <- lm(logodds ~ 1, weights=1/ASE^2, data=hosp.lodds)      # constant 
mod.unif  <- lm(logodds ~ visit, weights=1/ASE^2, data=hosp.lodds)  # uniform

Parallel model

A parallel log odds model has a main effect for stay

mod.par   <- lm(logodds ~ visit + stay, data=hosp.lodds)

Compare model fits

anova(mod.null, mod.const, mod.unif, mod.par)
## Analysis of Variance Table
## 
## Model 1: logodds ~ -1
## Model 2: logodds ~ 1
## Model 3: logodds ~ visit
## Model 4: logodds ~ visit + stay
##   Res.Df   RSS Df Sum of Sq    F  Pr(>F)    
## 1      6 22.95                              
## 2      5 19.90  1      3.05 1308 0.00076 ***
## 3      3  4.38  2     15.51 3323 0.00030 ***
## 4      2  0.00  1      4.38 1875 0.00053 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
LRstats(mod.null, mod.const, mod.unif, mod.par)
## Likelihood summary table:
##             AIC   BIC LR Chisq Df Pr(>Chisq)    
## mod.null   16.8  16.5    22.95  6    0.00081 ***
## mod.const  17.9  17.5    19.90  5    0.00131 ** 
## mod.unif   12.8  12.0     4.38  3    0.22305    
## mod.par   -15.9 -17.0     0.00  2    0.99767    
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Fit models with visit as an ordinal factor

mod1a <- lm(logodds ~ as.numeric(visit), weights=1/ASE^2, data=hosp.lodds)
mod2a <- lm(logodds ~ as.numeric(visit) + stay, weights=1/ASE^2, data=hosp.lodds)
anova(mod.const, mod2a, mod.par)
## Analysis of Variance Table
## 
## Model 1: logodds ~ 1
## Model 2: logodds ~ as.numeric(visit) + stay
## Model 3: logodds ~ visit + stay
##   Res.Df   RSS Df Sum of Sq    F  Pr(>F)    
## 1      5 19.90                              
## 2      3  4.24  2     15.66 3353 0.00030 ***
## 3      2  0.00  1      4.24 1815 0.00055 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Plotting

## Basic plot of points

gg <- ggplot(hosp.lodds, aes(x=visit, y=logodds, 
                             group=stay, color=stay)) + 
  geom_point(size=5) +
  ylab("log odds of shorter stay\n") + 
  xlab("Visit frequency") + theme_bw() + 
  theme(axis.title = element_text(size = rel(1.5))) + 
  theme(axis.text = element_text(size = rel(1.2))) +
  theme(legend.position = c(.85, .85), 
        legend.background = element_rect(colour = "black")) +
  theme(legend.title = element_text(size=rel(1.3))) +
  theme(legend.text = element_text(size=rel(1.2))) +
  scale_colour_manual(values=c("blue", "red"), 
                      name = "Length of stay")

gg + geom_line(size=1.2, linetype="dotted") 

showing model fits

grid <- hosp.lodds[,1:2]

Make a function to plots lines from a given model

gg_lines <- function(grid, mod, size=1.2, color=NULL, ...) {
  grid$logodds <- stats::predict(mod, grid)
  if(is.null(color)) geom_line(data=grid, size=size, ...) 
  else geom_line(data=grid, size=size, color=color, ...)
}

Add fitted lines to the plot

gg2 <-  
  gg + gg_lines(grid, mod.null, color="gray", size=1, linetype="dashed") +
  gg_lines(grid, mod.const, color=gray(.5), size=1) +
  gg_lines(grid, mod.unif, color="black", size=1) +
  gg_lines(grid, mod.par)

gg2 + annotate("text", label="null",      x=3.3, y=0, color="gray") +
  annotate("text", label="constant",  x=3.3, y=predict(mod.const, grid)[6], color=gray(.5)) + 
  annotate("text", label="uniform",   x=3.3, y=predict(mod.unif, grid)[6], color="black") +
  annotate("text", label="parallel",  x=3.3, y=hosp.lodds[5:6, 3], color=c("blue", "red")) +
  annotate("text", label="Log odds\nModel",     x=3.3, y=0.6, color="black", size=6)

IycgLS0tDQojJyB0aXRsZTogSG9zcGl0YWwgdmlzaXRzLS0gTG9nLW9kZHMgYW5hbHlzaXMNCiMnIGF1dGhvcjogIk1pY2hhZWwgRnJpZW5kbHkiDQojJyBkYXRlOiAiYHIgZm9ybWF0KFN5cy5EYXRlKCkpYCINCiMnIG91dHB1dDoNCiMnICAgaHRtbF9kb2N1bWVudDoNCiMnICAgICB0aGVtZTogcmVhZGFibGUNCiMnICAgICBjb2RlX2Rvd25sb2FkOiB0cnVlDQojJyAtLS0NCg0KIysgZWNobz1GQUxTRQ0Ka25pdHI6Om9wdHNfY2h1bmskc2V0KA0KICB3YXJuaW5nID0gRkFMU0UsICAgIyBhdm9pZCB3YXJuaW5ncyBhbmQgbWVzc2FnZXMgaW4gdGhlIG91dHB1dA0KICBtZXNzYWdlID0gRkFMU0UNCikNCm9wdGlvbnMoZGlnaXRzID0gMykNCg0KIycgIyMgTG9hZCBwYWNrYWdlcyBhbmQgdGhlIGRhdGENCmxpYnJhcnkodmNkKQ0KbGlicmFyeSh2Y2RFeHRyYSkNCmxpYnJhcnkoZ2dwbG90MikNCg0KZGF0YShIb3NwVmlzaXRzLCBwYWNrYWdlPSJ2Y2RFeHRyYSIpDQpIb3NwVmlzaXRzDQoNCiMnICMjIENhbGN1bGF0ZSBsb2cgb2Rkcw0KIycgYHZjZDo6bG9kZHNgIGNhbGN1bGF0ZXMgbG9nIG9kZHMgZm9yIGNvbXBhcmlzb25zIG9mIGFkamFjZW50IGNhdGVnb3JpZXMsIGZvciBvbmUgcmVzcG9uc2UgdmFyaWFibGUNCmxvZGRzKEhvc3BWaXNpdHMsIHJlc3BvbnNlPSJzdGF5IikgDQoNCiMnIE1ha2UgaXQgaW50byBhIGRhdGEuZnJhbWUsIGluY2x1ZGluZyB0aGUgQVNFDQpob3NwLmxvZGRzIDwtIGFzLmRhdGEuZnJhbWUobG9kZHMoSG9zcFZpc2l0cywgcmVzcG9uc2U9InN0YXkiKSkNCg0KIycgIyMgRml0IG1vZGVscw0KIycgSGVyZSwgSSB1c2UgMS9BU0VeMiBhcyB0aGUgd2VpZ2h0cywgaW4gYSAqKndlaWdodGVkIGxlYXN0IHNxdWFyZXMqKg0KbW9kLm51bGwgIDwtIGxtKGxvZ29kZHMgfiAtMSwgd2VpZ2h0cz0xL0FTRV4yLCBkYXRhPWhvc3AubG9kZHMpICAgICAjIG51bGwNCm1vZC5jb25zdCA8LSBsbShsb2dvZGRzIH4gMSwgd2VpZ2h0cz0xL0FTRV4yLCBkYXRhPWhvc3AubG9kZHMpICAgICAgIyBjb25zdGFudCANCm1vZC51bmlmICA8LSBsbShsb2dvZGRzIH4gdmlzaXQsIHdlaWdodHM9MS9BU0VeMiwgZGF0YT1ob3NwLmxvZGRzKSAgIyB1bmlmb3JtDQoNCiMnICMjIFBhcmFsbGVsIG1vZGVsDQojJyBBIHBhcmFsbGVsIGxvZyBvZGRzIG1vZGVsIGhhcyBhIG1haW4gZWZmZWN0IGZvciBgc3RheWANCm1vZC5wYXIgICA8LSBsbShsb2dvZGRzIH4gdmlzaXQgKyBzdGF5LCBkYXRhPWhvc3AubG9kZHMpDQoNCiMnIENvbXBhcmUgbW9kZWwgZml0cw0KYW5vdmEobW9kLm51bGwsIG1vZC5jb25zdCwgbW9kLnVuaWYsIG1vZC5wYXIpDQpMUnN0YXRzKG1vZC5udWxsLCBtb2QuY29uc3QsIG1vZC51bmlmLCBtb2QucGFyKQ0KDQojJyAjIyBGaXQgbW9kZWxzIHdpdGggYHZpc2l0YCBhcyBhbiBvcmRpbmFsIGZhY3RvciANCm1vZDFhIDwtIGxtKGxvZ29kZHMgfiBhcy5udW1lcmljKHZpc2l0KSwgd2VpZ2h0cz0xL0FTRV4yLCBkYXRhPWhvc3AubG9kZHMpDQptb2QyYSA8LSBsbShsb2dvZGRzIH4gYXMubnVtZXJpYyh2aXNpdCkgKyBzdGF5LCB3ZWlnaHRzPTEvQVNFXjIsIGRhdGE9aG9zcC5sb2RkcykNCmFub3ZhKG1vZC5jb25zdCwgbW9kMmEsIG1vZC5wYXIpDQoNCiMnICMjIFBsb3R0aW5nDQojJyANCiMnICAjIyBCYXNpYyBwbG90IG9mIHBvaW50cw0KZ2cgPC0gZ2dwbG90KGhvc3AubG9kZHMsIGFlcyh4PXZpc2l0LCB5PWxvZ29kZHMsIA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICBncm91cD1zdGF5LCBjb2xvcj1zdGF5KSkgKyANCiAgZ2VvbV9wb2ludChzaXplPTUpICsNCiAgeWxhYigibG9nIG9kZHMgb2Ygc2hvcnRlciBzdGF5XG4iKSArIA0KICB4bGFiKCJWaXNpdCBmcmVxdWVuY3kiKSArIHRoZW1lX2J3KCkgKyANCiAgdGhlbWUoYXhpcy50aXRsZSA9IGVsZW1lbnRfdGV4dChzaXplID0gcmVsKDEuNSkpKSArIA0KICB0aGVtZShheGlzLnRleHQgPSBlbGVtZW50X3RleHQoc2l6ZSA9IHJlbCgxLjIpKSkgKw0KICB0aGVtZShsZWdlbmQucG9zaXRpb24gPSBjKC44NSwgLjg1KSwgDQogICAgICAgIGxlZ2VuZC5iYWNrZ3JvdW5kID0gZWxlbWVudF9yZWN0KGNvbG91ciA9ICJibGFjayIpKSArDQogIHRoZW1lKGxlZ2VuZC50aXRsZSA9IGVsZW1lbnRfdGV4dChzaXplPXJlbCgxLjMpKSkgKw0KICB0aGVtZShsZWdlbmQudGV4dCA9IGVsZW1lbnRfdGV4dChzaXplPXJlbCgxLjIpKSkgKw0KICBzY2FsZV9jb2xvdXJfbWFudWFsKHZhbHVlcz1jKCJibHVlIiwgInJlZCIpLCANCiAgICAgICAgICAgICAgICAgICAgICBuYW1lID0gIkxlbmd0aCBvZiBzdGF5IikNCg0KZ2cgKyBnZW9tX2xpbmUoc2l6ZT0xLjIsIGxpbmV0eXBlPSJkb3R0ZWQiKSANCg0KIycgIyMgc2hvd2luZyBtb2RlbCBmaXRzDQpncmlkIDwtIGhvc3AubG9kZHNbLDE6Ml0NCg0KIycgTWFrZSBhIGZ1bmN0aW9uIHRvIHBsb3RzIGxpbmVzIGZyb20gYSBnaXZlbiBtb2RlbA0KZ2dfbGluZXMgPC0gZnVuY3Rpb24oZ3JpZCwgbW9kLCBzaXplPTEuMiwgY29sb3I9TlVMTCwgLi4uKSB7DQogIGdyaWQkbG9nb2RkcyA8LSBzdGF0czo6cHJlZGljdChtb2QsIGdyaWQpDQogIGlmKGlzLm51bGwoY29sb3IpKSBnZW9tX2xpbmUoZGF0YT1ncmlkLCBzaXplPXNpemUsIC4uLikgDQogIGVsc2UgZ2VvbV9saW5lKGRhdGE9Z3JpZCwgc2l6ZT1zaXplLCBjb2xvcj1jb2xvciwgLi4uKQ0KfQ0KDQojJyBBZGQgZml0dGVkIGxpbmVzIHRvIHRoZSBwbG90DQpnZzIgPC0gCQ0KICBnZyArIGdnX2xpbmVzKGdyaWQsIG1vZC5udWxsLCBjb2xvcj0iZ3JheSIsIHNpemU9MSwgbGluZXR5cGU9ImRhc2hlZCIpICsNCiAgZ2dfbGluZXMoZ3JpZCwgbW9kLmNvbnN0LCBjb2xvcj1ncmF5KC41KSwgc2l6ZT0xKSArDQogIGdnX2xpbmVzKGdyaWQsIG1vZC51bmlmLCBjb2xvcj0iYmxhY2siLCBzaXplPTEpICsNCiAgZ2dfbGluZXMoZ3JpZCwgbW9kLnBhcikNCg0KZ2cyICsgYW5ub3RhdGUoInRleHQiLCBsYWJlbD0ibnVsbCIsICAgICAgeD0zLjMsIHk9MCwgY29sb3I9ImdyYXkiKSArDQogIGFubm90YXRlKCJ0ZXh0IiwgbGFiZWw9ImNvbnN0YW50IiwgIHg9My4zLCB5PXByZWRpY3QobW9kLmNvbnN0LCBncmlkKVs2XSwgY29sb3I9Z3JheSguNSkpICsgDQogIGFubm90YXRlKCJ0ZXh0IiwgbGFiZWw9InVuaWZvcm0iLCAgIHg9My4zLCB5PXByZWRpY3QobW9kLnVuaWYsIGdyaWQpWzZdLCBjb2xvcj0iYmxhY2siKSArDQogIGFubm90YXRlKCJ0ZXh0IiwgbGFiZWw9InBhcmFsbGVsIiwgIHg9My4zLCB5PWhvc3AubG9kZHNbNTo2LCAzXSwgY29sb3I9YygiYmx1ZSIsICJyZWQiKSkgKw0KICBhbm5vdGF0ZSgidGV4dCIsIGxhYmVsPSJMb2cgb2Rkc1xuTW9kZWwiLCAgICAgeD0zLjMsIHk9MC42LCBjb2xvcj0iYmxhY2siLCBzaXplPTYpDQoNCg0KDQoNCg==