Skip to content

Commit 6b5ef97

Browse files
authored
Add options to calculate context effects (#608)
* Add options to calculate context effects * add test * fix for glm * fix test * fix test * fix tests * fix test * allow flexible handling of dot arguments * mark as internal * fix * fix * test * docs * fixes * revert * fix test * fix test * typo * news
1 parent 02803da commit 6b5ef97

15 files changed

Lines changed: 362 additions & 76 deletions

DESCRIPTION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
Type: Package
22
Package: modelbased
33
Title: Estimation of Model-Based Predictions, Contrasts and Means
4-
Version: 0.14.0.2
4+
Version: 0.14.0.3
55
Authors@R:
66
c(person(given = "Dominique",
77
family = "Makowski",

NEWS.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@
99

1010
* Support for models of class `nestedLogit`.
1111

12+
* Added option `comparison = "slope"` to `estimate_contrast()`, to allow
13+
calculating contrasts of average slopes. This can also be useful when
14+
estimating the context effects of within- and between-effects in a model.
15+
1216
# modelbased 0.14.0
1317

1418
## Changes

R/clean_names.R

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
# Clean names -------------------------------------------------------------
22

3-
43
#' @keywords internal
54
.clean_names_frequentist <- function(means, predict = NULL, info = NULL) {
6-
names(means)[names(means) == "emmean"] <- .guess_estimate_name(predict, info)
7-
names(means)[names(means) == "response"] <- .guess_estimate_name(predict, info)
5+
names(means)[names(means) == "emmean"] <- .guess_estimate_name(predict, info = info)
6+
names(means)[names(means) == "response"] <- .guess_estimate_name(predict, info = info)
87
names(means)[names(means) == "prob"] <- "Probability"
98
names(means)[names(means) == "estimate"] <- "Difference"
109
names(means)[names(means) == "odds.ratio"] <- "Odds_ratio"

R/estimate_contrasts.R

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,12 @@
3333
#' [this website](https://marginaleffects.com/bonus/hypothesis.html) and
3434
#' section _Comparison options_ below.
3535
#' * String: One of `"pairwise"`, `"reference"`, `"sequential"`, `"meandev"`
36-
#' `"meanotherdev"`, `"poly"`, `"helmert"`, or `"trt_vs_ctrl"`. To test
37-
#' multiple hypotheses jointly (usually used for factorial designs),
36+
#' `"meanotherdev"`, `"poly"`, `"helmert"`, `"slope"` or `"trt_vs_ctrl"`.
37+
#' The `"slope"` option calculates contrasts between average slopes and can
38+
#' also be used to calculate "context" effects, which is the difference of
39+
#' within- and between-effects (see
40+
#' https://statisticalhorizons.com/between-within-contextual-effects/). To
41+
#' test multiple hypotheses jointly (usually used for factorial designs),
3842
#' `comparison` can also be `"joint"`. In this case, use the `test` argument
3943
#' to specify which test should be conducted: `"F"` (default) or `"Chi2"`.
4044
#' * String: Special string options are `"inequality"`, `"inequality_ratio"`,

R/format.R

Lines changed: 106 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,16 @@ format.estimate_contrasts <- function(
1414
select <- NULL
1515
include_grid <- FALSE
1616
}
17+
# change parameter name for context effects
18+
if (isTRUE(attributes(x)$context_effects)) {
19+
x$Parameter <- "Average slope"
20+
}
21+
1722
# don't print columns of adjusted_for variables
1823
adjusted_for <- attr(x, "adjusted_for", exact = TRUE)
19-
if (!is.null(adjusted_for) && all(adjusted_for %in% colnames(x)) && !isTRUE(include_grid)) {
24+
if (
25+
!is.null(adjusted_for) && all(adjusted_for %in% colnames(x)) && !isTRUE(include_grid)
26+
) {
2027
# remove non-focal terms from data frame
2128
x[adjusted_for] <- NULL
2229
} else if (isTRUE(include_grid)) {
@@ -84,7 +91,13 @@ format.estimate_contrasts <- function(
8491
}
8592

8693
if (!is.null(format) && format %in% c("md", "markdown", "html")) {
87-
insight::format_table(x, ci_brackets = c("(", ")"), select = select, format = format, ...)
94+
insight::format_table(
95+
x,
96+
ci_brackets = c("(", ")"),
97+
select = select,
98+
format = format,
99+
...
100+
)
88101
} else {
89102
insight::format_table(x, select = select, ...)
90103
}
@@ -106,8 +119,12 @@ format.estimate_grouplevel <- format.estimate_contrasts
106119
#' @export
107120
format.estimate_smooth <- function(x, ...) {
108121
# Colnames
109-
if ("Size" %in% names(x)) x$Size <- ifelse(x$Size < 1, paste0(insight::format_value(x$Size * 100), "%"), "100%")
110-
if ("Part" %in% names(x)) x$Part <- insight::format_value(x$Part, protect_integers = TRUE)
122+
if ("Size" %in% names(x)) {
123+
x$Size <- ifelse(x$Size < 1, paste0(insight::format_value(x$Size * 100), "%"), "100%")
124+
}
125+
if ("Part" %in% names(x)) {
126+
x$Part <- insight::format_value(x$Part, protect_integers = TRUE)
127+
}
111128

112129
insight::format_table(x, ...)
113130
}
@@ -137,6 +154,7 @@ format.marginaleffects_means <- function(x, model, ci = 0.95, ...) {
137154
}
138155
non_focal <- setdiff(colnames(model_data), attr(x, "focal_terms"))
139156
predict_type <- attributes(x)$predict
157+
transform <- attributes(x)$transform
140158

141159
# special attributes we get from "get_marginalcontrasts()"
142160
comparison <- list(...)$hypothesis
@@ -161,7 +179,7 @@ format.marginaleffects_means <- function(x, model, ci = 0.95, ...) {
161179
# for simple means, we don't want p-values
162180
remove_columns <- c(remove_columns, "p")
163181
# estimate name
164-
estimate_name <- .guess_estimate_name(predict_type, info)
182+
estimate_name <- .guess_estimate_name(predict_type, transform, info)
165183
}
166184

167185
# reshape and format columns
@@ -190,7 +208,14 @@ format.marginaleffects_slopes <- function(x, model, ci = 0.95, ...) {
190208
}
191209
model_data <- insight::get_data(model, verbose = FALSE)
192210
# define all columns that should be removed
193-
remove_columns <- c("Predicted", "s.value", "S", "CI", "rowid_dedup", equivalence_columns)
211+
remove_columns <- c(
212+
"Predicted",
213+
"s.value",
214+
"S",
215+
"CI",
216+
"rowid_dedup",
217+
equivalence_columns
218+
)
194219
# for contrasting slope, we need to keep the "Parameter" column
195220
# however, for estimating trends/slope, the "Parameter" column is usually
196221
# redundant. Since we cannot check for class-attributes, we simply check if
@@ -377,7 +402,11 @@ format.marginaleffects_contrasts <- function(
377402
# replace all comparison levels with tokens
378403
params[] <- lapply(params, function(comparison_pair) {
379404
for (j in seq_along(all_num_levels)) {
380-
comparison_pair <- sub(all_num_levels[j], replace_num_levels[j], comparison_pair)
405+
comparison_pair <- sub(
406+
all_num_levels[j],
407+
replace_num_levels[j],
408+
comparison_pair
409+
)
381410
}
382411
for (j in seq_along(all_levels)) {
383412
comparison_pair <- sub(
@@ -489,7 +518,10 @@ format.marginaleffects_contrasts <- function(
489518
if (!is.null(contrast_filter)) {
490519
# make sure we also have all levels for non-filtered variables
491520
contrast_filter <- insight::compact_list(c(
492-
lapply(dgrid[setdiff(focal_terms, unique(c(by, names(contrast_filter))))], unique),
521+
lapply(
522+
dgrid[setdiff(focal_terms, unique(c(by, names(contrast_filter))))],
523+
unique
524+
),
493525
contrast_filter
494526
))
495527
# now create combinations of all filter variables
@@ -516,7 +548,6 @@ format.marginaleffects_contrasts <- function(
516548
# Helper ----------------------------------------------------------------------
517549
# -----------------------------------------------------------------------------
518550

519-
520551
# since we combine levels from different factors, we have to make
521552
# sure levels are unique across different terms. If not, paste
522553
# variable names to levels. We first find the intersection of all
@@ -553,15 +584,17 @@ equivalence_columns <- c(
553584
# outputs from {marginaleffects}
554585

555586
#' @keywords internal
556-
.standardize_marginaleffects_columns <- function(x,
557-
remove_columns,
558-
model,
559-
model_data,
560-
info,
561-
ci = 0.95,
562-
estimate_name = NULL,
563-
is_contrast_analysis = FALSE,
564-
...) {
587+
.standardize_marginaleffects_columns <- function(
588+
x,
589+
remove_columns,
590+
model,
591+
model_data,
592+
info,
593+
ci = 0.95,
594+
estimate_name = NULL,
595+
is_contrast_analysis = FALSE,
596+
...
597+
) {
565598
# tidy output - we want to tidy the output, using `model_parameters()` or
566599
# `describe_posterior()` for Bayesian models. We also need to know how the
567600
# coefficient column is named, because we replace that column name with an
@@ -578,7 +611,12 @@ equivalence_columns <- c(
578611
# column names for their "coefficient". We now extract the relevant one.
579612
possible_colnames <- c(
580613
attributes(params)$coefficient_name,
581-
"Coefficient", "Slope", "Predicted", "Median", "Mean", "MAP"
614+
"Coefficient",
615+
"Slope",
616+
"Predicted",
617+
"Median",
618+
"Mean",
619+
"MAP"
582620
)
583621
coefficient_name <- intersect(possible_colnames, colnames(params))[1]
584622
# we need to remove some more columns
@@ -665,9 +703,18 @@ equivalence_columns <- c(
665703
if (.is_inequality_comparison(comparison_hypothesis)) {
666704
# fix for pairwise inequality labels - these are named like "(b1) - (b2)" etc.
667705
# but we want the original labels instead of b1, b2 etc.
668-
if(comparison_hypothesis %in% c("inequality_pairwise", "inequality_ratio_pairwise") && !is.null(by_terms)) {
706+
if (
707+
comparison_hypothesis %in%
708+
c("inequality_pairwise", "inequality_ratio_pairwise") &&
709+
!is.null(by_terms)
710+
) {
669711
# clean parameter names
670-
parameter_names <- gsub(")", "", gsub("(", "", params$Parameter, fixed = TRUE), fixed = TRUE)
712+
parameter_names <- gsub(
713+
")",
714+
"",
715+
gsub("(", "", params$Parameter, fixed = TRUE),
716+
fixed = TRUE
717+
)
671718
# extract data for by-variable
672719
by_var <- model_data[[by_terms]]
673720
# make sure we have a factor
@@ -693,7 +740,11 @@ equivalence_columns <- c(
693740
}
694741

695742
# fix labels for inequality analysis for slopes
696-
if (comparison_hypothesis %in% c("inequality", "inequality_ratio") && isTRUE(attributes(x)$compute_slopes)) {
743+
if (
744+
comparison_hypothesis %in%
745+
c("inequality", "inequality_ratio") &&
746+
isTRUE(attributes(x)$compute_slopes)
747+
) {
697748
# for slopes, we either have the trend variable, or only the grouping,
698749
# but not the "inequality" variabe (the first in "by"). Update labels,
699750
# so users know by which variables slopes are averaged and grouped
@@ -752,7 +803,9 @@ equivalence_columns <- c(
752803

753804
#' @keywords internal
754805
.add_contrasts_ci <- function(is_contrast_analysis, params) {
755-
if (is_contrast_analysis && !"CI_low" %in% colnames(params) && "SE" %in% colnames(params)) {
806+
if (
807+
is_contrast_analysis && !"CI_low" %in% colnames(params) && "SE" %in% colnames(params)
808+
) {
756809
# extract ci-level
757810
if ("CI" %in% colnames(params)) {
758811
ci <- params[["CI"]][1]
@@ -782,25 +835,45 @@ equivalence_columns <- c(
782835
# based on on which scale predictions were requested
783836

784837
#' @keywords internal
785-
.guess_estimate_name <- function(predict_type, info) {
838+
.guess_estimate_name <- function(predict_type, transform = NULL, info) {
786839
# estimate name
787840
if (is.null(predict_type)) {
788841
estimate_name <- "Mean"
789-
} else if (!is.null(predict_type) && tolower(predict_type) %in% .brms_aux_elements()) {
842+
} else if (tolower(predict_type) %in% .brms_aux_elements()) {
790843
# for Bayesian models with distributional parameter
791844
estimate_name <- tools::toTitleCase(predict_type)
792-
} else if (!predict_type %in% c("none", "link") && (info$is_binomial || info$is_bernoulli || info$is_multinomial)) {
845+
} else if (
846+
!predict_type %in% c("none", "link") &&
847+
(info$is_binomial || info$is_bernoulli || info$is_multinomial)
848+
) {
793849
# here we add all models that model the probability of an outcome, such as
794850
# binomial, multinomial, or Bernoulli models
795851
estimate_name <- "Probability"
852+
} else if (
853+
predict_type %in%
854+
c("none", "link") &&
855+
identical(transform, "exp") &&
856+
(info$is_binomial || info$is_bernoulli || info$is_multinomial)
857+
) {
858+
# here we add all models that have odds ratios as exponentiated coefficients
859+
estimate_name <- "Odds_Ratio"
860+
} else if (
861+
predict_type %in% c("none", "link") && identical(transform, "exp") && (info$is_count)
862+
) {
863+
# here we add all models that have IRRs as exponentiated coefficients
864+
estimate_name <- "IRR"
796865
} else if (predict_type == "survival" && info$is_survival) {
797866
# this is for survival models, where we want to predict the survival probability
798867
estimate_name <- "Probability"
799868
} else if (predict_type %in% c("zprob", "zero")) {
800869
# this is for zero-inflated models, where we want to predict the probability
801870
# of a zero-inflated outcome
802871
estimate_name <- "Probability"
803-
} else if (predict_type %in% c("response", "invlink(link)") && (info$is_beta || info$is_orderedbeta)) {
872+
} else if (
873+
predict_type %in%
874+
c("response", "invlink(link)") &&
875+
(info$is_beta || info$is_orderedbeta)
876+
) {
804877
# this is for beta regression models, where we want to predict the mean
805878
# value of the outcome, which is a proportion
806879
estimate_name <- "Proportion"
@@ -834,7 +907,11 @@ equivalence_columns <- c(
834907
if (substring(input_string, match_positions[i], match_positions[i]) == "-") {
835908
inside_parentheses <- FALSE
836909
for (j in seq_along(match_positions)) {
837-
if (i != j && match_positions[i] > match_positions[j] && match_positions[i] < (match_positions[j] + match_lengths[j])) {
910+
if (
911+
i != j &&
912+
match_positions[i] > match_positions[j] &&
913+
match_positions[i] < (match_positions[j] + match_lengths[j])
914+
) {
838915
inside_parentheses <- TRUE
839916
break
840917
}
@@ -850,11 +927,7 @@ equivalence_columns <- c(
850927
for (i in 1:(length(split_positions) - 1)) {
851928
parts <- c(
852929
parts,
853-
substring(
854-
input_string,
855-
split_positions[i] + 1,
856-
split_positions[i + 1] - 1
857-
)
930+
substring(input_string, split_positions[i] + 1, split_positions[i + 1] - 1)
858931
)
859932
}
860933
}

R/get_contexteffects.R

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# special contrasts: context effects ----------------------------------------
2+
# ---------------------------------------------------------------------------
3+
4+
.get_contexteffects <- function(
5+
model,
6+
my_args,
7+
predict = NULL,
8+
transform = NULL,
9+
model_info,
10+
...
11+
) {
12+
if (model_info$is_linear) {
13+
out <- marginaleffects::avg_comparisons(
14+
model,
15+
variables = my_args$contrast,
16+
hypothesis = my_args$comparison,
17+
...
18+
)
19+
} else {
20+
dots <- list(...)
21+
fun_args <- list(model, variables = my_args$contrast, hypothesis = my_args$comparison)
22+
# set default for "type" argument, if not provided
23+
if (is.null(predict)) {
24+
fun_args$type <- "link"
25+
# if "type" was not provided, also change transform argument. we do
26+
# this only when user did not provide "type", else - if user provided
27+
# "type" - we keep the default NULL
28+
if (is.null(transform)) {
29+
fun_args$transform <- "exp"
30+
}
31+
} else {
32+
fun_args$type <- predict
33+
fun_args$transform <- transform
34+
}
35+
out <- do.call(marginaleffects::avg_comparisons, c(fun_args, dots))
36+
}
37+
# save some labels for printing
38+
attr(out, "by") <- my_args$by
39+
attr(out, "contrast") <- my_args$contrast
40+
attr(out, "context_effects") <- TRUE
41+
class(out) <- unique(c("marginaleffects_means", class(out)))
42+
out
43+
}

0 commit comments

Comments
 (0)