diff --git a/DESCRIPTION b/DESCRIPTION index 09b13e44b..c4107bfc7 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,7 +1,7 @@ Type: Package Package: modelbased Title: Estimation of Model-Based Predictions, Contrasts and Means -Version: 0.15.0 +Version: 0.15.0.1 Authors@R: c(person(given = "Dominique", family = "Makowski", diff --git a/NEWS.md b/NEWS.md index 92446f7b1..5915fffe3 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,3 +1,10 @@ +# modelbased (devel) + +## Changes + +* Informative error message when the `trend` variable in `estimate_slopes()` is + not numeric and `backend = "emmeans"`. + # modelbased 0.15.0 ## Breaking Changes diff --git a/R/estimate_contrasts.R b/R/estimate_contrasts.R index 59474500d..a65d95670 100644 --- a/R/estimate_contrasts.R +++ b/R/estimate_contrasts.R @@ -218,7 +218,7 @@ #' confidence bands: Theory, implementation, and an application to SVARs. #' Journal of Applied Econometrics, 34(1), 1–17. \doi{10.1002/jae.2656} #' -#' @examplesIf all(insight::check_if_installed(c("lme4", "marginaleffects", "parameters", "datawizard", "rstanarm"), quietly = TRUE)) +#' @examplesIf all(insight::check_if_installed(c("lme4", "emmeans", "marginaleffects", "parameters", "datawizard", "rstanarm"), quietly = TRUE)) #' \dontrun{ #' # Basic usage -------------------------------- #' # -------------------------------------------- @@ -259,8 +259,15 @@ #' # "time" only has integer values and few values, so it's treated like a factor #' estimate_contrasts(model, "time", by = "education") #' -#' # we set `integer_as_continuous = TRUE` to treat integer as continuous -#' estimate_contrasts(model, "time", by = "education", integer_as_continuous = 1) +#' # Setting `integer_as_continuous = TRUE` treats "time" as a continuous +#' # variable. This allows us to compare its average slope rather than making +#' # discrete pairwise comparisons at each time point. +#' estimate_contrasts( +#' model, +#' contrast = "time", +#' by = "education", +#' integer_as_continuous = TRUE +#' ) #' #' # pairwise comparisons for multiple groups #' estimate_contrasts( @@ -290,6 +297,17 @@ #' model <- lm(happiness ~ puppy_love * dose, data = puppy_love) #' estimate_slopes(model, "puppy_love", by = "dose", comparison = cond_tx) #' +#' # Note: for the emmeans-backend, we need to use `estimate_contrasts()` for +#' # the above example: +#' cond_tx <- list(`no treatment` = c(1, 0, 0), treatment = c(0, 0.5, 0.5)) +#' estimate_contrasts( +#' model, +#' contrast = "puppy_love", +#' by = "dose", +#' comparison = cond_tx, +#' backend = "emmeans" +#' ) +#' #' # Other models (mixed, Bayesian, ...) -------- #' # -------------------------------------------- #' data <- iris diff --git a/R/estimate_means.R b/R/estimate_means.R index ec3732412..6cc1500a9 100644 --- a/R/estimate_means.R +++ b/R/estimate_means.R @@ -117,7 +117,7 @@ #' @param ... Other arguments passed, for instance, to [insight::get_datagrid()], #' to functions from the **emmeans** or **marginaleffects** package, or to process #' Bayesian models via [bayestestR::describe_posterior()]. Examples: -#' - `insight::get_datagrid()`: Argument such as `length`, `digits` or `range` +#' - `insight::get_datagrid()`: Arguments such as `length`, `digits` or `range` #' can be used to control the (number of) representative values. For integer #' variables, `protect_integers` modulates whether these should also be #' treated as numerics, i.e. values can have fractions or not. @@ -134,8 +134,8 @@ #' - **emmeans**: Internally used functions are `emmeans()` and `emtrends()`. #' Additional arguments can be passed to these functions. #' - Bayesian models: For Bayesian models, parameters are cleaned using -#' `describe_posterior()`, thus, arguments like, for example, `centrality`, -#' `rope_range`, or `test` are passed to that function. +#' `bayestestR::describe_posterior()`, thus, arguments like, for example, +#' `centrality`, `rope_range`, or `test` are passed to that function. #' - Especially for `estimate_contrasts()` with integer focal predictors, for #' which contrasts should be calculated, use argument `integer_as_continuous` #' to set the maximum number of unique values in an integer predictor to treat diff --git a/R/get_emtrends.R b/R/get_emtrends.R index 141c1d3e0..2cdca1cf8 100644 --- a/R/get_emtrends.R +++ b/R/get_emtrends.R @@ -13,13 +13,15 @@ #' get_emtrends(model) #' get_emtrends(model, by = "Sepal.Width") #' @export -get_emtrends <- function(model, - trend = NULL, - by = NULL, - predict = NULL, - keep_iterations = FALSE, - verbose = TRUE, - ...) { +get_emtrends <- function( + model, + trend = NULL, + by = NULL, + predict = NULL, + keep_iterations = FALSE, + verbose = TRUE, + ... +) { # check if available insight::check_if_installed("emmeans") @@ -38,7 +40,11 @@ get_emtrends <- function(model, )) # handle distributional parameters - if (!is.null(predict) && inherits(model, "brmsfit") && predict %in% .brms_aux_elements(model)) { + if ( + !is.null(predict) && + inherits(model, "brmsfit") && + predict %in% .brms_aux_elements(model) + ) { fun_args$dpar <- predict } else { fun_args$type <- predict @@ -70,11 +76,13 @@ get_emtrends <- function(model, # ========================================================================= #' @keywords internal -.guess_emtrends_arguments <- function(model, - trend = NULL, - by = NULL, - verbose = TRUE, - ...) { +.guess_emtrends_arguments <- function( + model, + trend = NULL, + by = NULL, + verbose = TRUE, + ... +) { # Gather info model_data <- insight::get_data(model, verbose = FALSE) predictors <- intersect( @@ -86,19 +94,37 @@ get_emtrends <- function(model, if (is.null(trend)) { trend <- predictors[sapply(model_data[predictors], is.numeric)][1] if (!length(trend) || is.na(trend)) { - insight::format_error("Model contains no numeric predictor. Please specify `trend`.") + insight::format_error( + "Model contains no numeric predictor. Please specify `trend`." + ) } if (verbose) { - insight::format_alert(paste0("No numeric variable was specified for slope estimation. Selecting `trend = \"", trend, "\"`.")) # nolint + insight::format_alert(paste0( + "No numeric variable was specified for slope estimation. Selecting `trend = \"", + trend, + "\"`." + )) } } if (length(trend) > 1) { trend <- trend[1] if (verbose) { - insight::format_alert(paste0("More than one numeric variable was selected for slope estimation. Keeping only ", trend[1], ".")) # nolint + insight::format_alert(paste0( + "More than one numeric variable was selected for slope estimation. Keeping only `", + trend[1], + "`." + )) } } + if (trend %in% colnames(model_data) && !is.numeric(model_data[[trend]])) { + insight::format_error(paste0( + "Variable `", + trend, + "` is not numeric. Slopes can only be estimated for numeric variables when `backend = \"emmeans\"`. Please use `estimate_contrasts()` instead." + )) + } + my_args <- list(trend = trend, by = by) .process_emmeans_arguments(model, args = my_args, data = model_data, ...) } diff --git a/man/estimate_contrasts.Rd b/man/estimate_contrasts.Rd index 9efd82c85..553944c65 100644 --- a/man/estimate_contrasts.Rd +++ b/man/estimate_contrasts.Rd @@ -33,7 +33,7 @@ estimate_contrasts(model, ...) to functions from the \strong{emmeans} or \strong{marginaleffects} package, or to process Bayesian models via \code{\link[bayestestR:describe_posterior]{bayestestR::describe_posterior()}}. Examples: \itemize{ -\item \code{insight::get_datagrid()}: Argument such as \code{length}, \code{digits} or \code{range} +\item \code{insight::get_datagrid()}: Arguments such as \code{length}, \code{digits} or \code{range} can be used to control the (number of) representative values. For integer variables, \code{protect_integers} modulates whether these should also be treated as numerics, i.e. values can have fractions or not. @@ -50,8 +50,8 @@ supported by that model class. \item \strong{emmeans}: Internally used functions are \code{emmeans()} and \code{emtrends()}. Additional arguments can be passed to these functions. \item Bayesian models: For Bayesian models, parameters are cleaned using -\code{describe_posterior()}, thus, arguments like, for example, \code{centrality}, -\code{rope_range}, or \code{test} are passed to that function. +\code{bayestestR::describe_posterior()}, thus, arguments like, for example, +\code{centrality}, \code{rope_range}, or \code{test} are passed to that function. \item Especially for \code{estimate_contrasts()} with integer focal predictors, for which contrasts should be calculated, use argument \code{integer_as_continuous} to set the maximum number of unique values in an integer predictor to treat @@ -536,7 +536,7 @@ averaging across random effects groups is then more accurate. } \examples{ -\dontshow{if (all(insight::check_if_installed(c("lme4", "marginaleffects", "parameters", "datawizard", "rstanarm"), quietly = TRUE))) withAutoprint(\{ # examplesIf} +\dontshow{if (all(insight::check_if_installed(c("lme4", "emmeans", "marginaleffects", "parameters", "datawizard", "rstanarm"), quietly = TRUE))) withAutoprint(\{ # examplesIf} \dontrun{ # Basic usage -------------------------------- # -------------------------------------------- @@ -577,8 +577,15 @@ model <- lm(QoL ~ time * education * grp, data = qol_cancer) # "time" only has integer values and few values, so it's treated like a factor estimate_contrasts(model, "time", by = "education") -# we set `integer_as_continuous = TRUE` to treat integer as continuous -estimate_contrasts(model, "time", by = "education", integer_as_continuous = 1) +# Setting `integer_as_continuous = TRUE` treats "time" as a continuous +# variable. This allows us to compare its average slope rather than making +# discrete pairwise comparisons at each time point. +estimate_contrasts( + model, + contrast = "time", + by = "education", + integer_as_continuous = TRUE +) # pairwise comparisons for multiple groups estimate_contrasts( @@ -608,6 +615,17 @@ cond_tx <- cbind("no treatment" = c(1, 0, 0), "treatment" = c(0, 0.5, 0.5)) model <- lm(happiness ~ puppy_love * dose, data = puppy_love) estimate_slopes(model, "puppy_love", by = "dose", comparison = cond_tx) +# Note: for the emmeans-backend, we need to use `estimate_contrasts()` for +# the above example: +cond_tx <- list(`no treatment` = c(1, 0, 0), treatment = c(0, 0.5, 0.5)) +estimate_contrasts( + model, + contrast = "puppy_love", + by = "dose", + comparison = cond_tx, + backend = "emmeans" +) + # Other models (mixed, Bayesian, ...) -------- # -------------------------------------------- data <- iris diff --git a/man/estimate_means.Rd b/man/estimate_means.Rd index d92b0f4b1..10c9a94f5 100644 --- a/man/estimate_means.Rd +++ b/man/estimate_means.Rd @@ -144,7 +144,7 @@ default backend.} to functions from the \strong{emmeans} or \strong{marginaleffects} package, or to process Bayesian models via \code{\link[bayestestR:describe_posterior]{bayestestR::describe_posterior()}}. Examples: \itemize{ -\item \code{insight::get_datagrid()}: Argument such as \code{length}, \code{digits} or \code{range} +\item \code{insight::get_datagrid()}: Arguments such as \code{length}, \code{digits} or \code{range} can be used to control the (number of) representative values. For integer variables, \code{protect_integers} modulates whether these should also be treated as numerics, i.e. values can have fractions or not. @@ -161,8 +161,8 @@ supported by that model class. \item \strong{emmeans}: Internally used functions are \code{emmeans()} and \code{emtrends()}. Additional arguments can be passed to these functions. \item Bayesian models: For Bayesian models, parameters are cleaned using -\code{describe_posterior()}, thus, arguments like, for example, \code{centrality}, -\code{rope_range}, or \code{test} are passed to that function. +\code{bayestestR::describe_posterior()}, thus, arguments like, for example, +\code{centrality}, \code{rope_range}, or \code{test} are passed to that function. \item Especially for \code{estimate_contrasts()} with integer focal predictors, for which contrasts should be calculated, use argument \code{integer_as_continuous} to set the maximum number of unique values in an integer predictor to treat diff --git a/man/estimate_slopes.Rd b/man/estimate_slopes.Rd index b998936d1..5263bbebf 100644 --- a/man/estimate_slopes.Rd +++ b/man/estimate_slopes.Rd @@ -166,7 +166,7 @@ default backend.} to functions from the \strong{emmeans} or \strong{marginaleffects} package, or to process Bayesian models via \code{\link[bayestestR:describe_posterior]{bayestestR::describe_posterior()}}. Examples: \itemize{ -\item \code{insight::get_datagrid()}: Argument such as \code{length}, \code{digits} or \code{range} +\item \code{insight::get_datagrid()}: Arguments such as \code{length}, \code{digits} or \code{range} can be used to control the (number of) representative values. For integer variables, \code{protect_integers} modulates whether these should also be treated as numerics, i.e. values can have fractions or not. @@ -183,8 +183,8 @@ supported by that model class. \item \strong{emmeans}: Internally used functions are \code{emmeans()} and \code{emtrends()}. Additional arguments can be passed to these functions. \item Bayesian models: For Bayesian models, parameters are cleaned using -\code{describe_posterior()}, thus, arguments like, for example, \code{centrality}, -\code{rope_range}, or \code{test} are passed to that function. +\code{bayestestR::describe_posterior()}, thus, arguments like, for example, +\code{centrality}, \code{rope_range}, or \code{test} are passed to that function. \item Especially for \code{estimate_contrasts()} with integer focal predictors, for which contrasts should be calculated, use argument \code{integer_as_continuous} to set the maximum number of unique values in an integer predictor to treat diff --git a/man/get_emmeans.Rd b/man/get_emmeans.Rd index dd6953d5f..2be80529a 100644 --- a/man/get_emmeans.Rd +++ b/man/get_emmeans.Rd @@ -200,7 +200,7 @@ to the output. You can reshape them to a long format by running to functions from the \strong{emmeans} or \strong{marginaleffects} package, or to process Bayesian models via \code{\link[bayestestR:describe_posterior]{bayestestR::describe_posterior()}}. Examples: \itemize{ -\item \code{insight::get_datagrid()}: Argument such as \code{length}, \code{digits} or \code{range} +\item \code{insight::get_datagrid()}: Arguments such as \code{length}, \code{digits} or \code{range} can be used to control the (number of) representative values. For integer variables, \code{protect_integers} modulates whether these should also be treated as numerics, i.e. values can have fractions or not. @@ -217,8 +217,8 @@ supported by that model class. \item \strong{emmeans}: Internally used functions are \code{emmeans()} and \code{emtrends()}. Additional arguments can be passed to these functions. \item Bayesian models: For Bayesian models, parameters are cleaned using -\code{describe_posterior()}, thus, arguments like, for example, \code{centrality}, -\code{rope_range}, or \code{test} are passed to that function. +\code{bayestestR::describe_posterior()}, thus, arguments like, for example, +\code{centrality}, \code{rope_range}, or \code{test} are passed to that function. \item Especially for \code{estimate_contrasts()} with integer focal predictors, for which contrasts should be calculated, use argument \code{integer_as_continuous} to set the maximum number of unique values in an integer predictor to treat diff --git a/tests/testthat/test-estimate_slopes.R b/tests/testthat/test-estimate_slopes.R index d90b5021c..f97b30565 100644 --- a/tests/testthat/test-estimate_slopes.R +++ b/tests/testthat/test-estimate_slopes.R @@ -131,6 +131,30 @@ test_that("estimate_slopes", { }) +test_that("estimate_slopes, errors for emmeans when trend is non-numeric", { + skip_if_not_installed("emmeans") + data(iris) + model <- lm(Sepal.Length ~ Species * Sepal.Width, data = iris) + expect_error( + estimate_slopes(model, trend = "Species", by = "Sepal.Width", backend = "emmeans"), + regex = "Variable `Species` is not numeric", + fixed = TRUE + ) + model <- lm(Sepal.Length ~ Species, data = iris) + expect_error( + estimate_slopes(model, backend = "emmeans"), + regex = "Model contains no numeric predictor", + fixed = TRUE + ) + model <- lm(Sepal.Length ~ Species * Sepal.Width, data = iris) + expect_message( + estimate_slopes(model, backend = "emmeans"), + regex = "No numeric variable was specified", + fixed = TRUE + ) +}) + + test_that("estimate_slopes, johnson-neyman p-adjust", { data(iris) model <- lm(Sepal.Width ~ Petal.Width * Petal.Length, data = iris)