\documentclass[12pt]{article} %% vignette index specifications need to be *after* \documentclass{} %\VignetteEngine{knitr::knitr} %\VignetteIndexEntry{Model evaluation} %\VignettePackage{glmmTMB} %\VignetteDepends{ggplot2} %\VignetteDepends{car} %\VignetteDepends{emmeans} %\VignetteDepends{effects} %\VignetteDepends{multcomp} %\VignetteDepends{MuMIn} %\VignetteDepends{DHARMa} %\VignetteDepends{broom} %\VignetteDepends{broom.mixed} %\VignetteDepends{dotwhisker} %\VignetteDepends{texreg} %\VignetteDepends{xtable} %\VignetteDepends{huxtable} \usepackage{lineno} \usepackage[utf8]{inputenc} \usepackage{graphicx} \usepackage[american]{babel} %% for huxtable \usepackage{array} \usepackage{caption} \usepackage{graphicx} %% siunitx is needed for *some* huxtable functions, %% but messes up Solaris tests %% \usepackage{siunitx} \usepackage{colortbl} \usepackage{multirow} \usepackage{hhline} \usepackage{calc} \usepackage{tabularx} \usepackage{threeparttable} % maybe not available elsewhere? \usepackage{wrapfig} \usepackage{adjustbox} \newcommand{\R}{{\sf R}} \newcommand{\fixme}[1]{\textbf{\color{red} fixme: #1}} \newcommand{\notimpl}[1]{\emph{\color{magenta} #1}} \usepackage{url} \usepackage{hyperref} \usepackage{fancyvrb} \usepackage{natbib} %% \code{} below is not safe with \section{} etc. \newcommand{\tcode}[1]{{\tt #1}} \VerbatimFootnotes \bibliographystyle{chicago} %% need this for output of citation() below ... \newcommand{\bold}[1]{\textbf{#1}} %% code formatting %% https://tex.stackexchange.com/questions/273843/inline-verbatim-with-line-breaks-colored-font-and-highlighting/280212 % \usepackage{xcolor} %% see knit_hooks$set(...) below \newcommand\code[1]{\mytokenshelp#1 \relax\relax} \def\mytokenshelp#1 #2\relax{\allowbreak\grayspace\tokenscolor{#1}\ifx\relax#2\else \mytokenshelp#2\relax\fi} %\newcommand\tokenscolor[1]{\colorbox{gray!20}{\textcolor{blue}{% % \ttfamily\mystrut\smash{\detokenize{#1}}}}} \newcommand\tokenscolor[1]{\colorbox{gray!0}{\textcolor{black}{% \ttfamily\mystrut\smash{\detokenize{#1}}}}} \def\mystrut{\rule[\dimexpr-\dp\strutbox+\fboxsep]{0pt}{% \dimexpr\normalbaselineskip-2\fboxsep}} \def\grayspace{\hspace{0pt minus \fboxsep}} \title{Post-model-fitting procedures with \tcode{glmmTMB} models: diagnostics, inference, and model output} \date{\today} \author{} \begin{document} \maketitle %\linenumbers %% FIXME: pipeline for re-running stored objects %% FIXME: improve owls fit so DHARMa looks OK? %% FIXME: fix broom.mixed:tidy method %% FIXME: huxtable issues <>= library("knitr") opts_chunk$set(fig.width=5,fig.height=5, error=FALSE, out.width="0.8\\textwidth",echo=TRUE) ## https://tex.stackexchange.com/questions/148188/knitr-xcolor-incompatible-color-definition/254482 knit_hooks$set(document = function(x) {sub('\\usepackage[]{color}', '\\usepackage{xcolor}', x, fixed = TRUE)}) Rver <- paste(R.version$major,R.version$minor,sep=".") used.pkgs <- c("glmmTMB","bbmle") ## packages to report below @ <>= ## https://stackoverflow.com/questions/23840523/check-if-os-is-solaris is.solaris <- function() { grepl('SunOS', Sys.info()['sysname']) } is.windows <- function() { .Platform$OS.type == "windows" } is.cran <- function() { !identical(Sys.getenv("NOT_CRAN"), "true") } huxtable_OK <- (!is.solaris()) && !(is.windows() && is.cran()) @ The purpose of this vignette is to describe (and test) the functions in various downstream packages that are available for summarizing and otherwise interpreting glmmTMB fits. Some of the packages/functions discussed below may not be suitable for inference on parameters of the zero-inflation or dispersion models, but will be restricted to the conditional-mean model. <>= library(glmmTMB) library(car) library(emmeans) library(effects) library(multcomp) library(MuMIn) require(DHARMa, quietly = TRUE) ## may be missing ... library(broom) library(broom.mixed) require(dotwhisker, quietly = TRUE) library(ggplot2); theme_set(theme_bw()) library(texreg) library(xtable) if (huxtable_OK) library(huxtable) ## retrieve slow stuff L <- gt_load("vignette_data/model_evaluation.rda") @ A couple of example models: % don't need to evaluate this since we have loaded owls_nb1 from model_evaluation.rda <>= owls_nb1 <- glmmTMB(SiblingNegotiation ~ FoodTreatment*SexParent + (1|Nest)+offset(log(BroodSize)), contrasts=list(FoodTreatment="contr.sum", SexParent="contr.sum"), family = nbinom1, zi = ~1, data=Owls) @ <>= data("cbpp",package="lme4") cbpp_b1 <- glmmTMB(incidence/size~period+(1|herd), weights=size,family=binomial, data=cbpp) ## simulated three-term Beta example set.seed(1001) dd <- data.frame(z=rbeta(1000,shape1=2,shape2=3), a=rnorm(1000),b=rnorm(1000),c=rnorm(1000)) simex_b1 <- glmmTMB(z~a*b*c,family=beta_family,data=dd) @ \section{model checking and diagnostics} \subsection{\tcode{DHARMa}} The \code{DHARMa} package provides diagnostics for hierarchical models. After running % set to eval=FALSE since we have this stored in model_evaluation.rda <>= owls_nb1_simres <- simulateResiduals(owls_nb1) @ you can plot the results: <>= plot(owls_nb1_simres) @ <>= if (require(DHARMa, quietly = TRUE)) plot(owls_nb1_simres) @ \code{DHARMa} provides lots of other methods based on the simulated residuals: see \tcode{vignette("DHARMa", package="DHARMa")} \subsubsection{issues} \begin{itemize} \item \code{DHARMa} will only work for models using families for which a simulate method has been implemented (in \code{TMB}, and appropriately reflected in \code{glmmTMB}) \end{itemize} \section{Inference} \subsection{\tcode{car::Anova}} We can use \code{car::Anova()} to get traditional ANOVA-style tables from \code{glmmTMB} fits. A few limitations/reminders: \begin{itemize} \item these tables use Wald $\chi^2$ statistics for comparisons (neither likelihood ratio tests nor $F$ tests) \item they apply to the fixed effects of the conditional component of the model only (other components \emph{might} work, but haven't been tested at all) \item as always, if you want to do type 3 tests, you should probably set sum-to-zero contrasts on factors and center numerical covariates (see \code{contrasts} argument above) \end{itemize} <>= if (requireNamespace("car") && getRversion() >= "3.6.0") { Anova(owls_nb1) ## default type II Anova(owls_nb1,type="III") } @ \subsection{effects} <>= effects_ok <- (requireNamespace("effects") && getRversion() >= "3.6.0") if (effects_ok) { (ae <- allEffects(owls_nb1)) plot(ae) } @ (the error can probably be ignored) <>= if (effects_ok) { plot(allEffects(simex_b1)) } @ \subsection{\tcode{emmeans}} <>= emmeans(owls_nb1, poly ~ FoodTreatment | SexParent) @ Let us also consider a corresponding hurdle model: <>= owls_hnb1 <- update(owls_nb1, family = truncated_nbinom1, ziformula = ~.) @ On the response scale, this model estimates the means of the component distribution as follows: <>= emmeans(owls_hnb1, ~ FoodTreatment * SexParent, component = "cond", type = "response") # --- or --- emmeans(owls_hnb1, ~ FoodTreatment * SexParent, component = "cmean") @ These estimates differ because the first ones are back-transformed from the linear predictor, which is based on the \emph{un-truncated} component distribution, while the second ones are estimates of the means of the \emph{truncated} distribution (with zero omitted). This discrepancy occurs only with hurdle models. The response means combine both the conditional and the zero-inflation model: <>= emmeans(owls_hnb1, ~ FoodTreatment * SexParent, component = "response") @ \subsection{\tcode{drop1}} \code{stats::drop1} is a built-in R function that refits the model with various terms dropped. In its default mode it respects marginality (i.e., it will only drop the top-level interactions, not the main effects): <>= system.time(owls_nb1_d1 <- drop1(owls_nb1,test="Chisq")) @ <>= print(owls_nb1_d1) @ In principle, using \code{scope = . ~ . - (1|Nest)} should work to execute a ``type-3-like'' series of tests, dropping the main effects one at a time while leaving the interaction in (we have to use \code{- (1|Nest)} to exclude the random effects because \code{drop1} can't handle them). However, due to the way that R handles formulas, dropping main effects from an interaction of *factors* has no effect on the overall model. (It would work if we were testing the interaction of continuous variables.) \subsubsection{issues} The \code{mixed} package implements a true ``type-3-like'' parameter-dropping mechanism for \code{[g]lmer} models. Something like that could in principle be applied here. \subsection{Model selection and averaging with \tcode{MuMIn}} We can run \code{MuMIn::dredge(owls_nb1)} on the model to fit all possible submodels. Since this takes a little while (45 seconds or so), we've instead loaded some previously computed results: % stored in vignette_data/model_evaluation.rda ... <>= print(owls_nb1_dredge) @ <>= op <- par(mar=c(2,5,14,3)) plot(owls_nb1_dredge) par(op) ## restore graphics parameters @ Model averaging: <>= model.avg(owls_nb1_dredge) @ \subsubsection{issues} \begin{itemize} \item may not work for Beta models because the \code{family} component (``beta") is not identical to the name of the family function (\code{beta_family()})? (Kamil BartoĊ„, pers. comm.) \end{itemize} \subsection{\tcode{multcomp} for multiple comparisons and \emph{post hoc} tests} <>= g1 <- glht(cbpp_b1, linfct = mcp(period = "Tukey")) summary(g1) @ \section{Extracting coefficients, coefficient plots and tables} \subsection{\tcode{broom} and friends} The \code{broom} and \code{broom.mixed} packages are designed to extract information from a broad range of models in a convenient (tidy) format; the dotwhisker package builds on this platform to draw elegant coefficient plots. <>= if (requireNamespace("broom.mixed") && requireNamespace("dotwhisker")) { t1 <- broom.mixed::tidy(owls_nb1, conf.int = TRUE) t1 <- transform(t1, term=sprintf("%s.%s", component, term)) if (packageVersion("dotwhisker")>"0.4.1") { dw <- dwplot(t1) } else { owls_nb1$coefficients <- TRUE ## hack! dw <- dwplot(owls_nb1,by_2sd=FALSE) } print(dw+geom_vline(xintercept=0,lty=2)) } @ \subsubsection{issues} (these are more general \code{dwplot} issues) \begin{itemize} \item use black rather than color(1) when there's only a single model, i.e. only add aes(colour=model) conditionally? - draw points even if std err / confint are NA (draw \code{geom_point()} as well as \code{geom_pointrange()}? need to apply all aesthetics, dodging, etc. to both ...) \item for glmmTMB models, allow labeling by component? or should this be done by manipulating the tidied frame first? (i.e.: \verb+tidy(.) \%>\% tidyr::unite(term,c(component,term))+) \end{itemize} \subsection{coefficient tables with \tcode{xtable}} The \code{xtable} package can output data frames as \LaTeX\ tables; this isn't quite as elegant as \code{stargazer} etc., but is not a bad start. I've sprinkled lots of hard line-breaks, spaces, and newlines in below: someone who was better at \TeX\ could certainly do a better job. (\code{xtable} can also produce HTML output.) <>= ss <- summary(owls_nb1) ## print table; add space, pxt <- function(x,title) { cat(sprintf("{\n\n\\textbf{%s}\n\\ \\\\\\vspace{2pt}\\ \\\\\n",title)) print(xtable(x), floating=FALSE); cat("\n\n") cat("\\ \\\\\\vspace{5pt}\\ \\\\\n") } <>= pxt(lme4::formatVC(ss$varcor$cond),"random effects variances") pxt(coef(ss)$cond,"conditional fixed effects") pxt(coef(ss)$zi,"conditional zero-inflation effects") @ <>= if (requireNamespace("xtable")) { pxt(lme4::formatVC(ss$varcor$cond),"random effects variances") pxt(coef(ss)$cond,"conditional fixed effects") pxt(coef(ss)$zi,"conditional zero-inflation effects") } @ \subsection{coefficient tables with \tcode{texreg}} For now, to avoid needing to import the \tcode{texreg} package, we are providing the required \tcode{extract.glmmTMB} in a separate R file that you can import with \tcode{source()}, as follows: <>= source(system.file("other_methods","extract.R",package="glmmTMB")) texreg(owls_nb1,caption="Owls model", label="tab:owls") @ See output in Table~\ref{tab:owls}. \subsection{coefficient tables with \tcode{huxtable}} The \code{huxtable} package allows output in either \LaTeX\ or HTML: this example is tuned for \LaTeX. <>= if (!huxtable_OK) { cat("Sorry, huxtable+LaTeX is unreliable on this platform; skipping\n") } else { cc <- c("intercept (mean)"="(Intercept)", "food treatment (starvation)"="FoodTreatment1", "parental sex (M)"="SexParent1", "food $\\times$ sex"="FoodTreatment1:SexParent1") h0 <- huxreg(" " = owls_nb1, # give model blank name so we don't get '(1)' tidy_args = list(effects="fixed"), coefs = cc, error_pos = "right", statistics = "nobs" # don't include logLik and AIC ) names(h0)[2:3] <- c("estimate", "std. err.") ## allow use of math notation in name h1 <- set_cell_properties(h0,row=5,col=1,escape_contents=FALSE) cat(to_latex(h1,tabular_only=TRUE)) } @ \subsubsection{issues} \begin{itemize} \item \code{huxtable} needs quite a few additional \LaTeX\ packages: use \code{report_latex_dependencies()} to see what they are. \end{itemize} \section{influence measures} \emph{Influence measures} quantify the effects of particular observations, or groups of observations, on the results of a statistical model; \emph{leverage} and \emph{Cook's distance} are the two most common formats for influence measures. If a \href{https://en.wikipedia.org/wiki/Projection_matrix}{projection matrix} (or ``hat matrix'') is available, influence measures can be computed efficiently; otherwise, the same quantities can be estimated by brute-force methods, refitting the model with each group or observation successively left out. We've adapted the \tcode{car::influence.merMod} function to handle \tcode{glmmTMB} models; because it uses brute force, it can be slow, especially if evaluating the influence of individual observations. For now, it is included as a separate source file rather than exported as a method (see below), although it may be included in the package (or incorporated in the \tcode{car} package) in the future. <>= source(system.file("other_methods","influence_mixed.R", package="glmmTMB")) @ <>= owls_nb1_influence_time <- system.time( owls_nb1_influence <- influence_mixed(owls_nb1, groups="Nest") ) @ Re-fitting the model with each of the \Sexpr{length(unique(Owls$Nest))} nests excluded takes \Sexpr{round(owls_nb1_influence_time[["elapsed"]])} seconds (on an old Macbook Pro). The \tcode{car::infIndexPlot()} function is one way of displaying the results: <>= car::infIndexPlot(owls_nb1_influence) @ Or, you can transform the results and plot them however you like: <>= inf <- as.data.frame(owls_nb1_influence[["fixed.effects[-Nest]"]]) inf <- transform(inf, nest=rownames(inf), cooks=cooks.distance(owls_nb1_influence)) inf$ord <- rank(inf$cooks) if (require(reshape2)) { inf_long <- melt(inf, id.vars=c("ord","nest")) gg_infl <- (ggplot(inf_long,aes(ord,value)) + geom_point() + facet_wrap(~variable, scale="free_y") ## n.b. may need expand_scale() in older ggplot versions ? + scale_x_reverse(expand=expansion(mult=0.15)) + scale_y_continuous(expand=expansion(mult=0.15)) + geom_text(data=subset(inf_long,ord>24), aes(label=nest),vjust=-1.05) ) print(gg_infl) } @ \section{to do} \begin{itemize} \item more plotting methods (\code{sjplot}) \item output with \code{memisc} \item AUC etc. with \code{ModelMetrics} \end{itemize} <>= ## store time-consuming stuff save("owls_nb1", "owls_nb1_simres", "owls_nb1_dredge", "owls_nb1_influence", "owls_nb1_influence_time", file="../inst/vignette_data/model_evaluation.rda", version=2 ## for compatibility with R < 3.6.0 ) @ \end{document}