Graphs have long been known to be a more compact and effective means of conveying the results of regression models than tables (Gelman, Pasarica, and Dodhia 2002; Kastellec and Leoni 2007), but many researchers continue to list these results in tables. The reason, Kastellec and Leoni (2007) surmised, is “simply put, it takes much greater effort to produce a quality graph than a table.” The dotwhisker
package provides a convenient way to create highly customizable dot-and-whisker plots for presenting and comparing the output of regression models. It can be used to plot estimates of coefficients or other quantities of interest (e.g., predicted probabilities) within a single model or across different models: the estimates are presented as dots and their confidence intervals as whiskers (see Kastellec and Leoni 2007, 765–67).
Users can easily customize the number of models, the exact variables presented, and the inverval between the compared estimates based on their interests and presenting demands. Moreover, by outputting ggplot
objects, dotwhisker
allows users to easily further modify their graphs.
This vignette illustrates how to prepare regression results for input; basic use of dwplot
, the package’s function for creating dot-and-whisker plots; and a few examples of more advanced uses.
Although other packages (e.g., coefplot
) have the ability to draw dot-and-whisker plots, their functions take as inputs model objects, with the consequence that many models are not supported and plotting more than one model at a time is rarely possible. The dotwhisker
package avoids this limitation by taking as its input a data frame of estimates drawn from a model object rather than the model object itself. Moreover, in the process of preparing a data frame of estimates, users have maximum flexibility in determining what results are (and are not) included in the plot.
Extracting estimates from most model objects is straightforward, since many functions generate results that can be extracted by coef()
. Further, the broom
package (Robinson 2015) automates the process of extracting estimates for many common model objects.
The valid input object for dotwhisker
is a data frame including three columns: term
, that is, the variable name; estimate
, the regression coefficients or other quantity of interest; and std.error
, the standard errors associated with these estimates. Again, for many common models, one can use broom::tidy
to produce such a data frame of estimates.
#Package preload
library(dplyr)
library(broom)
library(dotwhisker)
data(mtcars)
# regression compatible with tidy
m1 <- lm(mpg ~ wt + cyl + disp + gear, data = mtcars)
m1_df <- tidy(m1) # create data.frame of regression results
m1_df # available for dwplot
## term estimate std.error statistic p.value
## 1 (Intercept) 43.539846900 4.86005893 8.958708 1.421951e-09
## 2 wt -3.792867171 1.08181932 -3.506008 1.608443e-03
## 3 cyl -1.784296065 0.61388910 -2.906545 7.215756e-03
## 4 disp 0.006944418 0.01200719 0.578355 5.678177e-01
## 5 gear -0.490445373 0.79028503 -0.620593 5.400711e-01
For model objects that are not compatible with tidy
, one can simply extract the results and build the needed data frame.
# the ordinal regression model is not supported by tidy
m2 <- ordinal::clm(factor(gear) ~ wt + cyl + disp, data = mtcars)
m2_df <- coef(summary(m2)) %>%
data.frame() %>%
add_rownames("term") %>%
rename(estimate = Estimate, std.error = Std..Error)
m2_df
## Source: local data frame [5 x 5]
##
## term estimate std.error z.value Pr...z..
## 1 3|4 -4.03517968 2.45869171 -1.6411898 0.1007580
## 2 4|5 -1.37662018 2.28622404 -0.6021370 0.5470829
## 3 wt -1.13452561 0.98498075 -1.1518252 0.2493929
## 4 cyl 0.41701081 0.60620009 0.6879095 0.4915098
## 5 disp -0.01343896 0.01188167 -1.1310664 0.2580271
During the process creating the data frame format, users are free to select or delete variables or to change their order. In many cases, for example, users will wish to consider omitting model intercepts as they are rarely theoretically interesting (see Kastellec and Leoni 2007, 765). Another often desireable manipulation is to standardize the scales of variables. Gelman (2008), for example, suggests rescaling ordinal and continuous predictors by two standard deviations to facilitate comparison with dichotomous predictors. Although this of course can be done before model estimation, it can be convenient to simply rescale the coefficients afterwards; the by_2sd
function, which takes as arguments a data frame of estimates along with the original data frame upon which the model was based, automates this calculation.
# Customize the input data frame
m1_df # the original tidy data.frame
## term estimate std.error statistic p.value
## 1 (Intercept) 43.539846900 4.86005893 8.958708 1.421951e-09
## 2 wt -3.792867171 1.08181932 -3.506008 1.608443e-03
## 3 cyl -1.784296065 0.61388910 -2.906545 7.215756e-03
## 4 disp 0.006944418 0.01200719 0.578355 5.678177e-01
## 5 gear -0.490445373 0.79028503 -0.620593 5.400711e-01
m1_df_sd <- tidy(m1) %>% by_2sd(mtcars) # rescale the coefficients
m1_df_sel <- filter(m1_df_sd, term != "(Intercept)") # omit intercept
m1_df_sel2 <- arrange(m1_df_sel, term) # reorder the variables
m1_df_sd # rescaled coefficients
## term estimate std.error statistic p.value
## 1 (Intercept) 43.5398469 4.860059 8.958708 1.421951e-09
## 2 wt -7.4223182 2.117028 -3.506008 1.608443e-03
## 3 cyl -6.3732259 2.192716 -2.906545 7.215756e-03
## 4 disp 1.7213643 2.976311 0.578355 5.678177e-01
## 5 gear -0.7237052 1.166151 -0.620593 5.400711e-01
m1_df_sel # rescaled and intercept omitted
## term estimate std.error statistic p.value
## 1 wt -7.4223182 2.117028 -3.506008 0.001608443
## 2 cyl -6.3732259 2.192716 -2.906545 0.007215756
## 3 disp 1.7213643 2.976311 0.578355 0.567817711
## 4 gear -0.7237052 1.166151 -0.620593 0.540071073
m1_df_sel2 # rescaled, intercept omitted, and variables reordered alphabetically
## term estimate std.error statistic p.value
## 1 cyl -6.3732259 2.192716 -2.906545 0.007215756
## 2 disp 1.7213643 2.976311 0.578355 0.567817711
## 3 gear -0.7237052 1.166151 -0.620593 0.540071073
## 4 wt -7.4223182 2.117028 -3.506008 0.001608443
An input data frame can also be constructed from estimates of other quantities of interest, such as margins and predicted probabilities, rather than coefficients.
# Create a data.frame of marginal effects
library(mfx)
m3 <- logitmfx(formula = am ~ wt + cyl + disp, data = mtcars)
m3_margin <- data.frame(m3$mfxest) %>%
add_rownames("term") %>%
rename(estimate = dF.dx, std.error = Std..Err.)
m3_margin
## Source: local data frame [3 x 5]
##
## term estimate std.error z P..z.
## 1 wt -1.121208436 0.591169104 -1.896595 0.05788140
## 2 cyl 0.367534149 0.209932491 1.750725 0.07999322
## 3 disp -0.003335217 0.003148954 -1.059151 0.28953111
dwplot
The dotwhisker::dwplot
function is very easy to use. It takes just three arguments: df
, alpha
, and dodge_size
. The df
argument is the data frame of estimates to be plotted (as described above). The alpha
argument is the significance level of the confidence intervals to be spanned by the whiskers: its default value is .05, implying a 95% confidence interval. The dodge_size
argument is used to adjust the space between the estimates of one variable when multiple models are presented in a single plot. Its default value will work fine, but more pleasing results can sometimes by achieved by it to lower values when the plotted results include a smaller number of predictors.
dwplot(m1_df)
dwplot(m1_df, alpha = .01) # using 99% CI
To omit intercepts, rescale coefficients, etc., no change in the use of dwplot
is required: one simply modifies the data frame, as described above, that is passed to the function.
dwplot(m1_df_sel2)
Moreover, the output of dwplot
is a ggplot
object. Therefore, users are able to add or change any ggplot
layers after calling dwplot
to achieve the desired presentation.
dwplot(m1_df_sel2) +
scale_y_discrete(breaks = 4:1,
labels=c("Cylinders", "Displacement", "Gears", "Weight")) +
theme_bw() + xlab("Standardized Coefficient") + ylab("") +
geom_vline(xintercept = 0, colour = "grey60", linetype = 2) +
ggtitle("Predicting Gas Mileage") +
theme(plot.title = element_text(face="bold"), legend.position="none")
An additional advantage of dwplot
over alternative solutions is that it can be used to create plots that present multiple models—across different samples or specifications—in little space while also facilitating cross-model comparisons. To do this, one first needs to append the data frame of estimates from the new model to that of the old model and create an additional column model
that identifies the two models.
# Run model on subsets of data, save results as tidy df, drop intercept, and make model variable
by_trans <- mtcars %>% group_by(am) %>%
do(tidy(lm(mpg ~ wt + cyl + disp + gear, data = .))) %>%
filter(term != "(Intercept)") %>% rename(model=am)
by_trans
## Source: local data frame [8 x 6]
## Groups: model
##
## model term estimate std.error statistic p.value
## 1 0 wt -2.812158473 1.26617822 -2.2209816 0.04335883
## 2 0 cyl -1.302359700 0.59904454 -2.1740616 0.04734352
## 3 0 disp 0.006919124 0.01294868 0.5343496 0.60148286
## 4 0 gear 1.258997239 1.80937061 0.6958205 0.49793031
## 5 1 wt -7.528674283 2.77396968 -2.7140435 0.02649178
## 6 1 cyl 0.198085116 1.70231059 0.1163625 0.91023331
## 7 1 disp -0.014608455 0.03147434 -0.4641386 0.65491703
## 8 1 gear -1.081671599 2.13913305 -0.5056589 0.62673136
dwplot(by_trans, dodge_size = .05) +
scale_y_discrete(breaks = 4:1, labels=c("Weight", "Cylinders", "Displacement", "Gears")) +
theme_bw() + xlab("Coefficient Estimate") + ylab("") +
geom_vline(xintercept = 0, colour = "grey60", linetype = 2) +
ggtitle("Predicting Gas Mileage by Transmission Type") +
theme(plot.title = element_text(face="bold"),
legend.justification=c(0, 0), legend.position=c(0, 0),
legend.background = element_rect(colour="grey80"),
legend.title.align = .5) +
scale_colour_grey(start = .4, end = .8,
name = "Transmission",
breaks = c(0, 1),
labels = c("Automatic", "Manual"))
To include models with different predictors, one must ensure the term
s are identical across all of the models. That is, the data frame for each model must include a row for every predictor included in any of the models, in the same order, regardless of whether the predictor is actually included in a particular model. Quantities not estimated in a particular model should be assigned a value of NA
.
# Estimate three models
m4 <- lm(mpg ~ wt + cyl + disp + gear, data = mtcars) # same as m1
m5 <- update(m4, . ~ . + hp) # add another predictor
m6 <- update(m5, . ~ . + am) # and another
# Tidy estimates, rescale, and omit intercepts
prep <- . %>% tidy() %>% by_2sd(mtcars) %>% filter(term != "(Intercept)")
m4_df <- prep(m4)
m5_df <- prep(m5)
m6_df <- prep(m6)
# Ensure all data.frames include rows for all of the predictors, in the same order
# Include NAs for any quantities not estimated in a particular model
m4_df <- rbind(m4_df, c("hp", rep(NA, times = ncol(m4_df) - 1)),
c("am", rep(NA, times = ncol(m4_df) - 1)))
m5_df <- rbind(m5_df, c("am", rep(NA, times = ncol(m5_df) - 1)))
# Add model variable to all data frames
m4_df <- mutate(m4_df, model = "Model 4")
m5_df <- mutate(m5_df, model = "Model 5")
m6_df <- mutate(m6_df, model = "Model 6")
m456_df <- rbind(m4_df, m5_df, m6_df)
dwplot(m456_df, dodge_size = .08) +
scale_y_discrete(breaks = 6:1,
labels=c("Weight", "Cylinders", "Displacement",
"Gears", "Horsepower", "Manual")) +
theme_bw() + xlab("Coefficient Estimate") + ylab("") +
geom_vline(xintercept = 0, colour = "grey60", linetype = 2) +
ggtitle("Predicting Gas Mileage") +
theme(plot.title = element_text(face="bold"),
legend.justification=c(1, 1), legend.position=c(1, 1),
legend.background = element_rect(colour="grey80"),
legend.title = element_blank())
It is frequently desirable to convey to an audience that the predictors in a model depicted in a dot-and-whisker plot form groups of some sort. This can be achieved by passing the finalized plot to the add_brackets
function. Note that add_brackets
outputs a gtable
rather than a ggplot
object. Therefore, the output is displayed using grid.draw
and saved, at present, by calling a device.
# Reorder predictors into groups
ordered_vars <- c("wt", "cyl", "disp", "hp", "gear", "am")
m456_df <- m456_df %>%
mutate(term = factor(term, levels = ordered_vars)) %>%
group_by(model) %>% arrange(term)
# Save finalized plot to an object (note reordered labels to match reordered predictors)
p456 <- dwplot(m456_df, dodge_size = .08) +
scale_y_discrete(breaks = 6:1,
labels=c("Weight", "Cylinders", "Displacement",
"Horsepower", "Gears", "Manual")) +
theme_bw() + xlab("Coefficient Estimate") + ylab("") +
geom_vline(xintercept = 0, colour = "grey60", linetype = 2) +
ggtitle("Predicting Gas Mileage") +
theme(plot.title = element_text(face="bold"),
legend.justification=c(1, 1), legend.position=c(1, 1),
legend.background = element_rect(colour="grey80"),
legend.title = element_blank())
# Create list of brackets (label, topmost included predictor, bottommost included predictor)
three_brackets <- list(c("Overall", "wt", "wt"), c("Engine", "cyl", "hp"),
c("Transmission", "gear", "am"))
g456 <- p456 %>% add_brackets(three_brackets)
grid.draw(g456) # to display
# pdf("plot.pdf") # to save to file (not run)
# grid.draw(g456)
# dev.off()
An alternate use of dot-and-whisker plots is to compare the estimated coefficients for a single predictor across many models or datasets: Andrew Gelman calls such plots “the secret weapon.” They are easy to make with dwplot
. Starting with a tidy data frame that includes results from all of the models, one subsets only the results for the predictor of interest, drops the old term
variable, and renames the existing model
variable to term
. Then one proceeds as usual.
data(diamonds)
# Estimate models for many subsets of data
by_clarity <- diamonds %>% group_by(clarity) %>%
do(tidy(lm(price ~ carat + cut + color, data = .))) %>% ungroup %>% rename(model=clarity)
# Extract the results for one variable
carat_results <- by_clarity %>% filter(term=="carat") %>% dplyr::select(-term) %>%
rename(term = model)
# Deploy the secret weapon
dwplot(carat_results) +
xlab("Estimated Coefficient (Dollars)") + ylab("Diamond Clarity") +
ggtitle("Estimated Coefficients for Diamond Size Across Clarity Grades") +
theme(plot.title = element_text(face="bold"))
A final means of presenting many models’ results at once, in a particularly compact format, is the “small multiple” plot. Small multiple plots present estimates in multiple panels, one for each variable: they are similar to a stack of secret weapon plots (see Kastellec and Leoni 2007, 766). Again starting with a tidy data frame that includes results from all of the models, to prepare to create a small multiple plot with dwplot
, one must first rename the term
variable (we will use the new name predictor
), then rename the model
variable term
, create a new model
variable equal to 1, and finally sort the data by predictor
and, in descending order, term
(one may also wish the convert the variable names and model names to factors for labeling purposes). Passing this data frame to dwplot
and then adding facet_grid(predictor~.) + coord_flip()
will generate a small multiple plot.
# Estimate six models, putting the results in a list, and
# transfer the list to a tidy data frame with NA rows for excluded variables
all_vars <- c("wt", "cyl", "disp", "gear", "hp", "am")
add_NAs <- function(m, all_vars) {
not_in <- setdiff(all_vars, m$term)
for (i in seq(not_in))
m <- rbind(m, c(not_in[i], rep(NA, times = ncol(m) - 1)))
m
}
m <- list()
m[[1]] <- lm(mpg ~ wt, data = mtcars)
m123456_df <- m[[1]] %>% prep %>% add_NAs(all_vars) %>%
mutate(model = "Model 1")
for (i in 2:6) {
m[[i]] <- update(m[[i-1]], paste(". ~ . +", ordered_vars[i]))
m123456_df <- rbind(m123456_df, m[[i]] %>% prep %>% add_NAs(all_vars) %>%
mutate(model = paste("Model", i)))
}
# Format the tidy data frame for a small multiple plot
m123456_df <- m123456_df %>%
mutate(term = factor(term, levels = ordered_vars),
model = factor(model, levels = paste("Model", 1:6))) %>%
rename(predictor = term, term = model) %>%
mutate(model = 1) %>% arrange(predictor, desc(term))
levels(m123456_df$predictor) <- c("Weight", "Cylinders", "Displacement",
"Gears", "Horsepower", "Manual") # For facet labels
# Plot using small multiples
dwplot(m123456_df) + facet_grid(predictor~.) + coord_flip() +
theme_bw() + xlab("Coefficient Estimate") +
geom_vline(xintercept = 0, colour = "grey60", linetype = 2) +
ggtitle("Predicting Gas Mileage") +
theme(plot.title = element_text(face = "bold"), legend.position = "none",
axis.text.x = element_text(angle = 60, hjust = 1))
The dotwhisker
package provides a flexible and convenient way to visualize and compare estimates across various models. This vignette offers an overview of its use and features. We encourage users to consult the help files for more details.
The development of the package is ongoing (among other things, we are working to enable small multiples plots to have separate y-axes for each predictor). Please contact us with any questions, bug reports, and comments.
Frederick Solt
Department of Political Science,
University of Iowa,
324 Schaeffer Hall,
20 E Washington St, Iowa City, IA, 52242
Email: frederick-solt@uiowa.edu
Website: http://myweb.uiowa.edu/fsolt
Yue Hu
Department of Political Science,
University of Iowa,
313 Schaeffer Hall,
20 E Washington St, Iowa City, IA, 52242
Email: yue-hu-1@uiowa.edu
Website: http://clas.uiowa.edu/polisci/people/yue-hu
Gelman, Andrew. 2008. “Scaling Regression Inputs by Dividing by Two Standard Deviations.” Statistics in Medicine 27 (15). Citeseer: 2865–73.
Gelman, Andrew, Cristian Pasarica, and Rahul Dodhia. 2002. “Let’s Practice What We Preach: Turning Tables into Graphs.” American Statistician 56 (2). Taylor & Francis: 121–30.
Kastellec, Jonathan P, and Eduardo L Leoni. 2007. “Using Graphs Instead of Tables in Political Science.” Perspectives on Politics 5 (04). Cambridge Univ Press: 755–71.
Robinson, David. 2015. Broom: Convert Statistical Analysis Objects into Tidy Data Frames. http://CRAN.R-project.org/package=broom.