Skip to content

Commit 577fc24

Browse files
authored
Move check-functions into own R file, refactor longer checks into separate functions (#662)
1 parent 46f8661 commit 577fc24

8 files changed

Lines changed: 403 additions & 371 deletions

‎R/checks.R‎

Lines changed: 382 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,382 @@
1+
#' @keywords internal
2+
#' @noRd
3+
.check_custom_contrasts_and_filter <- function(
4+
estimate,
5+
by,
6+
original_contrast,
7+
comparison
8+
) {
9+
# setup message to tell user that results must be cross-checked
10+
if (estimate == "average" && !all(.grep_cleaned_by_vars(by) == by)) {
11+
# first, extract contrast with filtering, which doesn't work
12+
wrong_contrast <- setdiff(original_contrast, .grep_cleaned_by_vars(original_contrast))
13+
# clean contrast and by, used to show correct example
14+
original_contrast <- .grep_cleaned_by_vars(original_contrast)
15+
original_by <- setdiff(.grep_cleaned_by_vars(by), original_contrast)
16+
msg1 <- paste0(
17+
"Selecting specific levels or values in the `contrast` or `by` arguments ",
18+
if (length(wrong_contrast)) {
19+
paste0(
20+
"(e.g., ",
21+
paste0(
22+
"`contrast = c(",
23+
paste0("\"", wrong_contrast, "\"", collapse = ", "),
24+
")`) "
25+
)
26+
)
27+
},
28+
"is error-prone for custom contrasts like ",
29+
paste0("`comparison = \"", comparison, "\"`"),
30+
" in combination with `estimate = \"average\"`. This can yield incorrect results,",
31+
" even if the output suggests the correct comparisons. It is strongly recommended",
32+
" to use only bare variable names in `contrast` and `by`, e.g.\n\n"
33+
)
34+
msg2 <- insight::color_text(
35+
paste0(
36+
" estimate_contrasts(\n",
37+
" contrast = c(",
38+
paste0("\"", original_contrast, "\"", collapse = ", "),
39+
"),\n",
40+
if (length(original_by)) {
41+
paste0(" by = c(", paste0("\"", original_by, "\"", collapse = ", "), "),\n")
42+
},
43+
" estimate = \"average\",\n comparison = ...\n )"
44+
),
45+
color = "green"
46+
)
47+
msg3 <- "\n\n and update your `comparison` argument accordingly.\n Run\n\n"
48+
msg4 <- insight::color_text(
49+
paste0(
50+
" estimate_means(\n",
51+
" c(",
52+
paste0("\"", c(original_contrast, original_by), "\"", collapse = ", "),
53+
"),\n estimate = \"average\"\n )"
54+
),
55+
color = "green"
56+
)
57+
msg5 <- "\n\n first to find out the correct rows to specify the `b`-coefficients for the `comparison` argument.\n"
58+
warning(
59+
insight::format_message(msg1),
60+
msg2,
61+
msg3,
62+
msg4,
63+
insight::format_message(msg5),
64+
call. = FALSE
65+
)
66+
}
67+
}
68+
69+
70+
#' @keywords internal
71+
#' @noRd
72+
.check_standard_errors <- function(
73+
out,
74+
by = NULL,
75+
contrast = NULL,
76+
model = NULL,
77+
model_name = "model",
78+
verbose = TRUE,
79+
...
80+
) {
81+
if (!verbose || is.null(out$SE)) {
82+
return(NULL)
83+
}
84+
85+
if (all(is.na(out$SE))) {
86+
# we show an example code how to resolve the problem. this example
87+
# code only works when we have at least `by` or `contrast`. If both
88+
# are NULL, we ignore the example code (see below)
89+
code_snippet <- paste0("\n\nestim <- estimate_relation(\n ", model_name)
90+
by_vars <- c(by, contrast)
91+
if (!is.null(by_vars)) {
92+
code_snippet <- paste0(
93+
code_snippet,
94+
",\n by = ",
95+
ifelse(length(by_vars) > 1, "c(", ""),
96+
paste0("\"", by_vars, "\"", collapse = ", "),
97+
ifelse(length(by_vars) > 1, ")", "")
98+
)
99+
}
100+
code_snippet <- paste0(code_snippet, "\n)\nestimate_contrasts(\n estim")
101+
if (!is.null(contrast)) {
102+
code_snippet <- paste0(
103+
code_snippet,
104+
",\n contrast = ",
105+
ifelse(length(contrast) > 1, "c(", ""),
106+
paste0("\"", contrast, "\"", collapse = ", "),
107+
ifelse(length(contrast) > 1, ")", "")
108+
)
109+
}
110+
code_snippet <- paste0(code_snippet, "\n)")
111+
# setup message
112+
msg <- insight::format_message(
113+
"Could not calculate standard errors for contrasts. This can happen when random effects are involved."
114+
)
115+
# add example code, if valid
116+
if (!is.null(by_vars)) {
117+
msg <- c(
118+
paste(msg, "You may try following:"),
119+
insight::color_text(code_snippet, "green"),
120+
"\n"
121+
)
122+
}
123+
message(msg)
124+
125+
# disable message for now, see
126+
# https://github.com/easystats/modelbased/issues/526
127+
# } else if (length(out$SE) > 1 && isTRUE(all(out$SE == out$SE[1])) && insight::is_mixed_model(model)) {
128+
# msg <- "Standard errors are probably not reliable. This can happen when random effects are involved. You may try `estimate_relation()` instead."
129+
# if (!inherits(model, "glmmTMB")) {
130+
# msg <- paste(msg, "You may also try package {.pkg glmmTMB} to produce valid standard errors.")
131+
# }
132+
# insight::format_alert(msg)
133+
}
134+
}
135+
136+
137+
#' @keywords internal
138+
#' @noRd
139+
.check_offset <- function(
140+
model,
141+
estimate,
142+
offset = NULL,
143+
my_args = NULL,
144+
verbose = TRUE
145+
) {
146+
model_offset <- insight::find_offset(model)
147+
# check if model has an offset at all
148+
if (!is.null(model_offset) && !any(startsWith(my_args$by, model_offset)) && verbose) {
149+
msg <- NULL
150+
if (is.null(offset)) {
151+
# if no offset argument was specified, tell user what this means
152+
msg <- switch(
153+
estimate,
154+
specific = ,
155+
typical = paste(
156+
"Model contains an offset-term, which is set to its mean value.",
157+
"If you want to average predictions over the distribution of the offset",
158+
"(if appropriate), use `estimate = \"average\"` or `estimate = \"population\"`.",
159+
"If you want to fix the offset to a specific value, for instance `1`,",
160+
"use `offset = 1`."
161+
),
162+
average = ,
163+
population = paste(
164+
"Model contains an offset-term and you average predictions over the",
165+
"distribution of that offset. If you want to fix the offset to a",
166+
"specific value, for instance `1`, use `offset = 1`."
167+
)
168+
)
169+
# if offset term is log-transformed, tell user. offset should be fixed then
170+
log_offset <- insight::find_transformation(insight::find_offset(
171+
model,
172+
as_term = TRUE
173+
))
174+
if (!is.null(log_offset) && startsWith(log_offset, "log")) {
175+
msg <- c(
176+
msg,
177+
paste(
178+
"We also found that the model has a log-transformed offset term.",
179+
"If you use the `offset` argument, the log-transformation will",
180+
"automatically be applied to the provided offset-value. I.e., consider",
181+
"using, for instance, `offset = 10` and not `offset = log(10)`."
182+
)
183+
)
184+
}
185+
}
186+
if (!is.null(msg)) {
187+
insight::format_alert(msg)
188+
}
189+
}
190+
}
191+
192+
193+
#' @keywords internal
194+
#' @noRd
195+
.check_dots_data <- function(dots, verbose) {
196+
if (!is.null(dots$data)) {
197+
if (!is.null(dots$newdata) && verbose) {
198+
insight::format_alert(
199+
"Both 'data' and 'newdata' were provided. Please specify only one. Ignoring 'newdata' and using 'data' instead."
200+
)
201+
}
202+
dots$newdata <- dots$data
203+
dots$data <- NULL
204+
}
205+
dots
206+
}
207+
208+
209+
#' @keywords internal
210+
#' @noRd
211+
.check_for_inequality_comparison <- function(comparison) {
212+
# check whether we have a formula definition of inequality comparisons,
213+
# and convert it to a string
214+
#
215+
# the default formulas are converted to a string:
216+
# ~inequality -> "inequality"
217+
# inequality ~ pairwise -> "inequality_pairwise"
218+
# ratio ~ inequality -> "inequality_ratio"
219+
# ratio ~ inequality + pairwise` -> "inequality_ratio_pairwise"
220+
#
221+
# we may have other formulas that control grouping and averaging, like
222+
# `~ inequality | grp1 + grp2`. In this case, the formula is returned as is
223+
# and processed later in ".process_inequality_formula()"
224+
if (inherits(comparison, "formula")) {
225+
# parse variables into a string
226+
out <- paste(all.vars(comparison), collapse = "_")
227+
# handle special cases
228+
out <- switch(
229+
out,
230+
ratio_inequality = "inequality_ratio",
231+
ratio_inequality_pairwise = "inequality_ratio_pairwise",
232+
out
233+
)
234+
if (.is_inequality_comparison(out)) {
235+
return(out)
236+
}
237+
}
238+
comparison
239+
}
240+
241+
242+
#' @keywords internal
243+
#' @noRd
244+
.check_format_backend <- function(...) {
245+
# we allow exporting HTML format based on "gt" or "tinytable"
246+
dots <- list(...)
247+
if (identical(dots$backend, "tt")) {
248+
"tt"
249+
} else {
250+
"html"
251+
}
252+
}
253+
254+
255+
#' @keywords internal
256+
#' @noRd
257+
.check_predict_arg <- function(predict, valid_types, error_arg) {
258+
if (isTRUE(is.na(predict))) {
259+
# add modelbased-options to valid types
260+
valid_types <- unique(c("response", "link", valid_types))
261+
insight::format_error(paste0(
262+
"The option provided in the `",
263+
error_arg,
264+
"` argument is not recognized.",
265+
" Valid options are: ",
266+
datawizard::text_concatenate(valid_types, enclose = "`"),
267+
"."
268+
))
269+
}
270+
}
271+
272+
273+
# handle errors from marginaleffects -----------------------------------------
274+
#
275+
# This helper function processes errors that occur during calls to the
276+
# {marginaleffects} package. It creates more informative and user-friendly
277+
# error messages by inspecting the original error and suggesting potential
278+
# solutions for common problems, such as using a different `estimate` option
279+
# or switching to the `emmeans` backend.
280+
#
281+
# Arguments:
282+
# - out: The error object returned from the `tryCatch` block.
283+
# - fun_args: A list of arguments that were passed to the failing
284+
# {marginaleffects} function.
285+
#
286+
# returns: A character vector containing the formatted, user-friendly error
287+
# message, which is then passed to `insight::format_error()`.
288+
#
289+
#' @keywords internal
290+
#' @noRd
291+
.check_marginaleffects_errors <- function(out, fun_args) {
292+
# what was requested?
293+
if (is.null(fun_args$hypothesis)) {
294+
fun <- "marginal means"
295+
} else {
296+
fun <- "marginal contrasts"
297+
}
298+
# clean original error message
299+
out$message <- gsub("\\s+", " ", gsub("\n", "", out$message, fixed = TRUE))
300+
# setup clear error message
301+
msg <- c(
302+
paste0("Sorry, calculating ", fun, " failed with following error:"),
303+
insight::color_text(gsub("\n", "", out$message, fixed = TRUE), "red")
304+
)
305+
# handle exceptions ------------------------------------------------------
306+
# we get this error when we should use counterfactuals
307+
if (grepl("not found in column names", out$message, fixed = TRUE)) {
308+
msg <- c(
309+
msg,
310+
"\nIt seems that not all required levels of the focal terms are available in the provided data. If you want predictions extrapolated to a hypothetical target population, try setting `estimate=\"population\"."
311+
)
312+
}
313+
# we get this error for models with complex random effects structures in glmmTMB,
314+
# or when the data grid is too large
315+
if (
316+
grepl("map factor length must equal", out$message, fixed = TRUE) ||
317+
grepl("cannot allocate", out$message, fixed = TRUE)
318+
) {
319+
msg <- c(
320+
msg,
321+
paste0(
322+
"\nYou may try using the `emmeans` backend, e.g. `estimate_means(model, by = c(",
323+
toString(paste0("\"", fun_args$by, "\"")),
324+
"), backend = \"emmeans\")`, or use `estimate_relation(model, by = c(",
325+
toString(paste0("\"", fun_args$by, "\"")),
326+
"))` instead. For contrasts or pairwise comparisons, save the output of `estimate_relation()` and pass it to `estimate_contrasts()`, e.g.\n"
327+
),
328+
paste0(
329+
"out <- estimate_relation(model, by = c(",
330+
toString(paste0("\"", fun_args$by, "\"")),
331+
"))"
332+
),
333+
paste0(
334+
"estimate_contrasts(out, contrast = c(",
335+
toString(paste0("\"", fun_args$by, "\"")),
336+
"))"
337+
)
338+
)
339+
}
340+
msg
341+
}
342+
343+
344+
#' @keywords internal
345+
#' @noRd
346+
.check_filter_args <- function(result, prefix = "") {
347+
if (nrow(result) == 0) {
348+
# small helper, because we have the same error message in several places
349+
insight::format_error(
350+
prefix,
351+
"Please check your `by` and `contrast` arguments, or try one of the following options:",
352+
"1. Use a different option for the `estimate` argument, e.g. `estimate = \"typical\"`.",
353+
"2. Use the `newdata` argument to provide a data grid of predictor values at which to evaluate predictions."
354+
)
355+
}
356+
}
357+
358+
359+
#' @param trend The `trend` argument
360+
#' @return updated `trend` variable
361+
#' @keywords internal
362+
#' @noRd
363+
.check_trend_arg <- function(trend, verbose = TRUE) {
364+
if (length(trend) > 1) {
365+
if (verbose) {
366+
insight::format_alert(paste0(
367+
"More than one numeric variable was selected for slope estimation. Keeping only `",
368+
trend[1],
369+
"`. ",
370+
"If you want to estimate the slope of `",
371+
trend[1],
372+
"` at different values of `",
373+
trend[2],
374+
"`, use `by=\"",
375+
trend[2],
376+
"\"` instead."
377+
))
378+
}
379+
trend <- trend[1]
380+
}
381+
trend
382+
}

0 commit comments

Comments
 (0)