--- title: "Plotting nestedLogit models with ggplot2" author: "Michael Friendly and John Fox" date: "`r Sys.Date()`" package: nestedLogit output: rmarkdown::html_vignette: fig_caption: yes bibliography: ["references.bib", "packages.bib"] csl: apa.csl vignette: > %\VignetteIndexEntry{Plotting nestedLogit models with ggplot2} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r, include = FALSE} knitr::opts_chunk$set( collapse = TRUE, message = FALSE, warning = FALSE, fig.align = "center", fig.height = 6, fig.width = 7, fig.path = "fig/", dev = "png", comment = "#>" ) # save some typing knitr::set_alias(w = "fig.width", h = "fig.height", cap = "fig.cap") # colorize text colorize <- function(x, color) { if (knitr::is_latex_output()) { sprintf("\\textcolor{%s}{%s}", color, x) } else if (knitr::is_html_output()) { sprintf("%s", color, x) } else x } set.seed(47) .opts <- options(digits = 4) # packages to be cited here. Code at the end automatically updates packages.bib #to.cite <- c("ggplot2", "geomtextpath", "equatiomatic") ``` Load the packages we'll use here: ```{r setup} library(nestedLogit) # Nested Dichotomy Logistic Regression Models library(knitr) # A General-Purpose Package for Dynamic Report Generation in R library(dplyr) # A Grammar of Data Manipulation library(tidyr) # Tidy Messy Data library(ggplot2) # Create Elegant Data Visualisations Using the Grammar of Graphics library(geomtextpath) # Curved Text in 'ggplot2' ``` The main vignette illustrated the basic plot method, `plot.nestedLogit()` in the package. However, for better control of the details and possibly more pleasing graphs, it is useful to describe how graphs can be constructed using `ggplot2` [@R-ggplot2]. We'll use the same example of women's labor force participation, using the original dichotomies: ```{r wlf-model} data(Womenlf, package = "carData") comparisons <- logits(work=dichotomy("not.work", c("parttime", "fulltime")), full=dichotomy("parttime", "fulltime")) wlf.nested <- nestedLogit(partic ~ hincome + children, dichotomies = comparisons, data=Womenlf) ``` The advantages of this approach are that * you can handle more complicated designs with more predictors by faceting, * it allows you to plot either the fitted probabilities or their transformed logits, * you can also obtain plots for log odds corresponding to each of the dichotomies which comprise the nested logit model. As we will illustrate, this provides a nice visual interpretation of the alternative specification of dichotomies for the `Womenlf` data discussed in the section "Alternative models for the `Womenlf` data" of the main vignette. ## Fitted probabilities To draw a plot, it is sufficient to calculate predicted probabilities over a grid of values of the predictor variables. Here, we select a range of 0 - 45 in steps of 5, combined with the two values of `children`. ```{r pred.nested} new <- expand.grid(hincome=seq(0, 45, by = 5), children=c("absent", "present")) pred.nested <- predict(wlf.nested, newdata = new) names(pred.nested) ``` As explained in `help(predict.nestedLogit)`, the predict method returns a complicated structure -- a list of four data frames corresponding to the predicted probabilities for the response categories, the corresponding logits, and each of their standard errors. ```{r} head(pred.nested[["p"]]) ``` However, `ggplot` wants the data in long format. This is easily done using the `as.data.frame()` method, which also includes the values of the predictors in the `newdata` data set: ```{r} plotdata <- as.data.frame(pred.nested, newdata=new) head(plotdata) ``` ## Plotting with `ggplot2` Then we can plot probability against one predictor, use `color` to distinguish the levels of the response (`partic`) and facet the plot by `children`. The point-wise standard errors are drawn in a 68% confidence envelope using `geom_ribbon()`. We've also plotted the predicted values as points to show where the predictions are obtained. ```{r wlf-ggplot-p1} #| out.width = "100%", #| fig.height = 4, #| fig.cap = "**ggplot**: Predicted probabilities of working at all or working part time or full time versus husband's income, by `children`" theme_set(theme_bw(base_size = 14)) gg1 <- ggplot(plotdata, aes(x=hincome, y=p, color=response)) + geom_line(linewidth = 2) + geom_point(size = 1.5, shape = 16, color = "black") + labs(x="Husband's Income", y= "Probability") + facet_wrap(~ children, labeller = label_both) + geom_ribbon(aes(ymin=p - se.p, ymax=p + se.p, fill = response), alpha = 0.3) gg1 ``` It is noteworthy that the confidence envelopes are wider for not-working women at higher levels of husband's income, where there are fewer observations. ### Direct labels Plot legends are somewhat hard to read and take up unnecessary space in the plot, so it is often better to label the curves directly. The `geomtextpath` package [@R-geomtextpath] produces a nicer plot. ```{r wlf-ggplot-p2} #| out.width = "100%", #| fig.height = 4, #| fig.cap = "The same plot using direct labels on the curves rather than a legend." gg1 + geom_textline(aes(label = response), hjust = -0.01, vjust=-0.5, size=5) + theme(legend.position = "none") ``` ### Plotting log-odds The advantage of the data structure returned by `as.data.frame()` is that you can just as easily plot the predicted probabilities on the scale of log-odds ($\text{logit}(p) = \log(p / (1-p))$), using the `logit` and `logit.se` components. ```{r wlf-ggplot-logit} #| out.width = "100%", #| fig.height = 4, #| fig.cap = "Predicted log odds of working at all or working part time or full time versus husband's income, by `children`" ggplot(plotdata, aes(x=hincome, y=logit, color=response)) + geom_line(linewidth = 2) + geom_point(size = 1.5, shape = 16, color = "black") + labs(x="Husband's Income", y= "Log Odds") + facet_wrap(~ children, labeller = label_both) + geom_ribbon(aes(ymin=logit - se.logit, ymax=logit + se.logit, fill = response), alpha = 0.3) + geom_textline(aes(label = response), hjust = -0.01, vjust=-0.5, size=5) + theme(legend.position = "none") ``` ## Predicted logit values for the dichotomies The nested logit model `wlf.nested` comprises the two binary logistic regression models for the `work` and `full` dichotomies. We can plot these as follows. ```{r} names(models(wlf.nested)) ``` The `predict()` method can also generate predicted values and their standard errors for the logits of these dichotomies, using the `model = "dichotomies"` argument: ```{r} pred.dichot <- predict(wlf.nested, newdata = new, model = "dichotomies") str(pred.dichot) ``` Transforming this to a data frame, we get an analogous result for plotting: ```{r} plotlogit <- as.data.frame(pred.dichot, newdata = new) head(plotlogit) ``` Then, plot the `logit` vs. husband's income, with separate curves for the two sub-models: ```{r wlf-ggplot-dichot1} #| out.width = "100%", #| fig.height = 4, #| fig.cap = "Predicted logits for the two dichotomies, `work` and `full` versus `hincome`, by `children`" ggplot(plotlogit, aes(x=hincome, y=logit, color=response)) + geom_line(linewidth = 2) + geom_point(size = 1.5, shape = 16, color = "black") + labs(x="Husband's Income", y= "Log Odds") + facet_wrap(~ children, labeller = label_both) + geom_ribbon(aes(ymin=logit - se.logit, ymax=logit + se.logit, fill = response), alpha = 0.3) + geom_textline(aes(label = response), hjust = -0.01, vjust=-0.5, size=5) + theme(legend.position = "none") ``` Or, interchanging the roles of `children` and `response`, we can plot these the other way, faceting by `response`. ```{r wlf-ggplot-dichot2} #| out.width = "100%", #| fig.height = 4, #| fig.cap = "Predicted logits for the two dichotomies, `work` and `full` versus `hincome`, by `response`" ggplot(plotlogit, aes(x=hincome, y=logit, color=children)) + geom_line(linewidth = 2) + geom_point(size = 1.5, shape = 16, color = "black") + labs(x="Husband's Income", y= "Log Odds") + facet_wrap(~ response, labeller = label_both) + geom_ribbon(aes(ymin=logit - se.logit, ymax=logit + se.logit, fill = children), alpha = 0.3) + geom_textline(aes(label = children), hjust = -0.01, vjust=-0.5, size=5) + theme(legend.position = "none") ``` This nicely illustrates the nature of the fitted logit models: The lines in each panel have the same slopes for the two levels of `children`, differing only in their intercepts. The `full` distinction between working full-time vs. part-time decreases faster with husband's income than for the `work` dichotomy between not working at all and working either part-time or full-time. ## Alternative model In the main vignette we mentioned that an alternative set of nested dichotomies first contrasts full-time work with the other categories, {full-time} vs. {not working, part-time}, and then {not working} vs. {part-time}. ```{r alt-model} wlf.nested.alt <- nestedLogit(partic ~ hincome + children, logits(full=dichotomy(nonfulltime=c("not.work", "parttime"), "fulltime"), part=dichotomy("not.work", "parttime")), data=Womenlf) ``` Proceeding in the same way as above, we get predicted logits and standard errors for each of the dichotomies. ```{r} pred.dichot.alt <- predict(wlf.nested.alt, newdata = new, model = "dichotomies") plotlogit.alt <- as.data.frame(pred.dichot.alt, newdata = new) head(plotlogit.alt) ``` Plotting these as before: ```{r wlf-ggplot-alt1} #| out.width = "100%", #| fig.height = 4, #| fig.cap = "Predicted logits for the two dichotomies, `work` and `full` versus `hincome`, by `response`" ggplot(plotlogit.alt, aes(x=hincome, y=logit, color=children)) + geom_line(linewidth = 2) + geom_point(size = 1.5, shape = 16, color = "black") + labs(x="Husband's Income", y= "Log Odds") + facet_wrap(~ response, labeller = label_both) + geom_ribbon(aes(ymin=logit - se.logit, ymax=logit + se.logit, fill = children), alpha = 0.3) + geom_textline(aes(label = children), hjust = -0.01, vjust=-0.5, size=5) + theme(legend.position = "none") ``` It's apparent that the alternative model produces a simpler description of the data: The predictors husband's income and presence of children affect the decision to work full-time, but not the decision to work part-time among those who aren't engaged in full-time work. In particular it is clear that neither husband's income nor having young children has any effect on the decision to work part-time. ```{r, include = FALSE} options(.opts) ``` ## References