Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
701ed11
make it so
DominiqueMakowski Oct 4, 2025
df602f9
Update R/estimate_grouplevel.R
DominiqueMakowski Oct 4, 2025
f52b8db
Update R/estimate_grouplevel.R
DominiqueMakowski Oct 4, 2025
b66390f
Update R/estimate_grouplevel.R
DominiqueMakowski Oct 4, 2025
396dc30
Update R/estimate_grouplevel.R
DominiqueMakowski Oct 4, 2025
06bbe4e
Update R/estimate_grouplevel.R
DominiqueMakowski Oct 4, 2025
ad50d2f
Update man/estimate_grouplevel.Rd
DominiqueMakowski Oct 4, 2025
be9c50b
Apply suggestion from @github-actions[bot]
DominiqueMakowski Oct 4, 2025
2b734bc
Apply suggestion from @github-actions[bot]
DominiqueMakowski Oct 4, 2025
761ebda
Apply suggestion from @github-actions[bot]
DominiqueMakowski Oct 4, 2025
7139b4d
Apply suggestion from @github-actions[bot]
DominiqueMakowski Oct 4, 2025
bb3a018
Apply suggestion from @github-actions[bot]
DominiqueMakowski Oct 4, 2025
14a06b2
Update man/estimate_grouplevel.Rd
DominiqueMakowski Oct 4, 2025
28435c4
spelling, styling
strengejacke Oct 4, 2025
c390c1f
add tests
strengejacke Oct 4, 2025
8849680
air styler
strengejacke Oct 4, 2025
d71e42e
stringsAsFactors = FALSE
strengejacke Oct 4, 2025
8237fe6
desc, news
strengejacke Oct 4, 2025
1bd8cb8
air styler
strengejacke Oct 4, 2025
dd96d96
fix tests
DominiqueMakowski Oct 4, 2025
d99823e
Update tests/testthat/test-estimate_grouplevel.R
DominiqueMakowski Oct 4, 2025
4e6acf9
Update tests/testthat/test-estimate_grouplevel.R
DominiqueMakowski Oct 4, 2025
3d041d5
air styler
strengejacke Oct 5, 2025
b81aae6
use `estimate = "average"`
strengejacke Oct 5, 2025
ed32638
make it work when factors
strengejacke Oct 5, 2025
5216fc7
Revert "make it work when factors"
strengejacke Oct 6, 2025
55c8a00
allow nested design
strengejacke Oct 6, 2025
efd9e1b
remove comment
strengejacke Oct 6, 2025
e98de26
add comment
strengejacke Oct 6, 2025
f9b3cc4
air
strengejacke Oct 6, 2025
1827d70
add test when random intercept is suppressed
strengejacke Oct 6, 2025
af3009e
sort output
strengejacke Oct 6, 2025
98f668d
fix test
strengejacke Oct 6, 2025
a56a767
Merge branch 'main' into grouplevel_marginal
strengejacke Oct 6, 2025
53b9ef7
news
strengejacke Oct 6, 2025
e9a5a64
Update R/estimate_grouplevel.R
strengejacke Oct 6, 2025
c78adad
rd
strengejacke Oct 6, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion DESCRIPTION
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
Type: Package
Package: modelbased
Title: Estimation of Model-Based Predictions, Contrasts and Means
Version: 0.13.0.9
Version: 0.13.0.10
Authors@R:
c(person(given = "Dominique",
family = "Makowski",
Expand Down
8 changes: 7 additions & 1 deletion NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,19 @@

## Changes

* The `type` argument in `estimate_grouplevel()` gains a `"marginal"` option,
to return marginal group-levels estimates.

* Added an `as.data.frame()` method for *modelbased* objects.

* Better formatting of the output for equivalence-tests, when the `equivalence`
argument was used. Related docs were added. It is also possible to use
`parameters::equivalence_test()` on *modelbased* objects.

* Functions calls are now saved as `call` attribute in *modelbased* objects.
* Function calls are now saved as `call` attribute in *modelbased* objects.

* More informative warnings and error messages were added to `estimate_contrasts()`
when computing effect sizes.

# modelbased 0.13.0

Expand Down
206 changes: 149 additions & 57 deletions R/estimate_grouplevel.R
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,32 @@
#' which can be useful to add the random effects to the original data.
#'
#' @param model A mixed model with random effects.
#' @param type `"random"` or `"total"`. If `"random"` (default), the
#' coefficients correspond to the conditional estimates of the random effects
#' (as they are returned by `lme4::ranef()`). They typically correspond to the
#' deviation of each individual group from their fixed effect (assuming the
#' random effect is also included as a fixed effect). As such, a coefficient
#' close to 0 means that the participants' effect is the same as the
#' population-level effect (in other words, it is "in the norm"). If
#' `"total"`, it will return the sum of the random effect and its
#' @param type String, describing the type of estimates that should be returned.
#' Can be `"random"`, `"total"`, or `"marginal"` (experimental).
#' - If `"random"` (default), the coefficients correspond to the conditional
#' estimates of the random effects (as they are returned by `lme4::ranef()`).
#' They typically correspond to the deviation of each individual group from
#' their fixed effect (assuming the random effect is also included as a fixed
#' effect). As such, a coefficient close to 0 means that the participants'
#' effect is the same as the population-level effect (in other words, it is
#' "in the norm").
#' - If `"total"`, it will return the sum of the random effect and its
#' corresponding fixed effects, which internally relies on the `coef()` method
#' (see `?coef.merMod`). Note that `type = "total"` yet does not return
#' uncertainty indices (such as SE and CI) for models from *lme4* or
#' *glmmTMB*, as the necessary information to compute them is not yet
#' available. However, for Bayesian models, it is possible to compute them.
#' - If `"marginal"` (experimental), it returns marginal group-levels
#' estimates. The random intercepts are computed using marginal means (see
#' [estimate_means()]), and the random slopes using marginal effects (see
#' [estimate_slopes()]). This method does not directly extract the parameters
#' estimated by the model, but recomputes them using model predictions. While
#' this is more computationally intensive, one of the benefits include
#' interpretability: the random intercepts correspond to the "mean" value of
#' the outcome for each group, and the random slopes correspond to the direct
#' average "effect" of the predictor for each random group. Note that in this
#' case, the group-level estimates are not technically "intercepts" or model
#' parameters, but marginal average levels and effects.
#' @param dispersion,test,diagnostic Arguments passed to
#' [parameters::model_parameters()] for Bayesian models. By default, it won't
#' return significance or diagnostic indices (as it is not typically very
Expand All @@ -31,7 +44,7 @@
#' and prevents overfitting.
#'
#'
#' @examplesIf all(insight::check_if_installed(c("see", "lme4"), quietly = TRUE)) && packageVersion("insight") > "1.1.0" && packageVersion("parameters") > "0.24.1"

Check warning on line 47 in R/estimate_grouplevel.R

View workflow job for this annotation

GitHub Actions / lint / lint

file=R/estimate_grouplevel.R,line=47,col=121,[line_length_linter] Lines should not be more than 120 characters. This line is 163 characters.

Check warning on line 47 in R/estimate_grouplevel.R

View workflow job for this annotation

GitHub Actions / lint-changed-files / lint-changed-files

file=R/estimate_grouplevel.R,line=47,col=121,[line_length_linter] Lines should not be more than 120 characters. This line is 163 characters.
#' # lme4 model
#' data(mtcars)
#' model <- lme4::lmer(mpg ~ hp + (1 | carb), data = mtcars)
Expand Down Expand Up @@ -59,11 +72,9 @@

#' @rdname estimate_grouplevel
#' @export
estimate_grouplevel.default <- function(model,
type = "random",
...) {
estimate_grouplevel.default <- function(model, type = "random", ...) {
# validate argument
type <- insight::validate_argument(type, c("random", "total"))
type <- insight::validate_argument(type, c("random", "total", "marginal"))

# sanity check
if (is.null(insight::find_random(model))) {
Expand All @@ -77,23 +88,28 @@
...
)

# get cleaned parameter names with additional information
clean_parameters <- attributes(params)$clean_parameters

# Re-add info
if (!"Group" %in% names(params) && !is.null(clean_parameters)) {
params$Group <- clean_parameters$Group
}
if (!"Level" %in% names(params) && !is.null(clean_parameters)) {
params$Level <- clean_parameters$Cleaned_Parameter
if (type %in% c("random", "total")) {
# get cleaned parameter names with additional information
clean_parameters <- attributes(params)$clean_parameters

# Re-add info
if (!"Group" %in% names(params) && !is.null(clean_parameters)) {
params$Group <- clean_parameters$Group
}
if (!"Level" %in% names(params) && !is.null(clean_parameters)) {
params$Level <- clean_parameters$Cleaned_Parameter
}

# TODO: improve / add new printing that groups by group/level?
random <- as.data.frame(params)

# Remove columns with only NaNs (as these are probably those of fixed effects)
random[vapply(random, function(x) all(is.na(x)), TRUE)] <- NULL
} else if (type == "marginal") {
# EXPERIMENTAL
random <- .grouplevel_marginal(model)
}

# TODO: improve / add new printing that groups by group/level?
random <- as.data.frame(params)

# Remove columns with only NaNs (as these are probably those of fixed effects)
random[vapply(random, function(x) all(is.na(x)), TRUE)] <- NULL

# Clean and Reorganize columns
random <- .clean_grouplevel(random)

Expand All @@ -107,12 +123,14 @@

#' @rdname estimate_grouplevel
#' @export
estimate_grouplevel.brmsfit <- function(model,
type = "random",
dispersion = TRUE,
test = NULL,
diagnostic = NULL,
...) {
estimate_grouplevel.brmsfit <- function(
model,
type = "random",
dispersion = TRUE,
test = NULL,
diagnostic = NULL,
...
) {
# validate argument
type <- insight::validate_argument(type, c("random", "total"))

Expand All @@ -136,7 +154,8 @@

# match parameters
if (!is.null(clean_parameters)) {
clean_parameters <- clean_parameters[clean_parameters$Parameter %in% params$Parameter, , drop = FALSE]
# fmt: skip
clean_parameters <- clean_parameters[clean_parameters$Parameter %in% params$Parameter, , drop = FALSE] # nolint
}

# Re-add info
Expand All @@ -163,19 +182,17 @@
# Filter out non-random effects
random <- random[startsWith(random$Parameter, "r_"), ]
# Remove Group from Level
random$Level <- sapply(
1:nrow(random),
function(i) gsub(paste0("^", random$Group[i], "\\."), "", random$Level[i])
)
random$Level <- sapply(seq_len(nrow(random)), function(i) {
gsub(paste0("^", random$Group[i], "\\."), "", random$Level[i])
})
# Find the group name (what follows "r_" and before the first "[" or "__")
random$Group <- gsub("^r_(.*?)(\\[.*|__.*)", "\\1", random$Name)
# Keep Parameter what's in between [ and ]
random$Parameter <- gsub("^r_.*?\\[(.*?)\\].*", "\\1", random$Name)
# Remove Level from it
random$Parameter <- sapply(
1:nrow(random),
function(i) gsub(paste0("^", random$Level[i], "\\,"), "", random$Parameter[i])
)
random$Parameter <- sapply(seq_len(nrow(random)), function(i) {
gsub(paste0("^", random$Level[i], "\\,"), "", random$Parameter[i])
})
# remove temporary name column
random$Name <- NULL
}
Expand All @@ -189,12 +206,14 @@


#' @export
estimate_grouplevel.stanreg <- function(model,
type = "random",
dispersion = TRUE,
test = NULL,
diagnostic = NULL,
...) {
estimate_grouplevel.stanreg <- function(
model,
type = "random",
dispersion = TRUE,
test = NULL,
diagnostic = NULL,
...
) {
# validate argument
type <- insight::validate_argument(type, c("random", "total"))

Expand Down Expand Up @@ -222,14 +241,10 @@
## insight::clean_parameter())
if (!is.null(clean_parameters)) {
# fix for rstanarm, which contains a sigma columns
clean_parameters <- clean_parameters[
clean_parameters$Component != "sigma" & !startsWith(clean_parameters$Parameter, "Sigma["), ,
drop = FALSE # nolint
]
clean_parameters <- clean_parameters[
clean_parameters$Parameter %in% params$Parameter, ,
drop = FALSE
]
# fmt: skip
clean_parameters <- clean_parameters[clean_parameters$Component != "sigma" & !startsWith(clean_parameters$Parameter, "Sigma["), , drop = FALSE] # nolint
# fmt: skip
clean_parameters <- clean_parameters[clean_parameters$Parameter %in% params$Parameter, , drop = FALSE] # nolint

params$Parameter <- insight::trim_ws(sub(":.*", "", clean_parameters$Group))
params$Group <- insight::trim_ws(sub("^[^:]*:", "", clean_parameters$Group))
Expand Down Expand Up @@ -302,7 +317,84 @@
),
]
} else {
random <- random[order(random$Group, datawizard::coerce_to_numeric(random$Level), random$Parameter), ] # nolint
random <- random[
order(random$Group, datawizard::coerce_to_numeric(random$Level), random$Parameter),
]
}
random
}


# Experimental ------------------------------------------------------------

# Quick tests:
# model <- lme4::lmer(mpg ~ hp + (1 | carb), data = mtcars)
# model <- lme4::lmer(mpg ~ hp + (1 + hp | carb), data = mtcars)
# model <- lme4::lmer(mpg ~ hp + (1 + hp | carb) + (1 | gear), data = mtcars)

# m1 <- estimate_grouplevel(model, type = "random")
# m2 <- estimate_grouplevel(model, type = "total")
# m3 <- estimate_grouplevel(model, type = "marginal")
# cor.test(m1$Coefficient, m3$Coefficient) # r = 1
# cor.test(m2$Coefficient, m3$Coefficient) # r = 1
.grouplevel_marginal <- function(model) {
insight::check_if_installed("lme4")

out <- list()

# TODO: currently only takes random effects for main component into account
# we could also look for random effects in zero-inflated, dispersion, etc.

# Analyze random effect structure
randomgroups <- insight::find_random(model, split_nested = TRUE)$random
# extract random slopes and their related grouping variable
randomslopes <- list()
p <- lme4::ranef(model)
for (g in names(p)) {
s <- names(p[[g]])[names(p[[g]]) != "(Intercept)"]

# Only pick non-factor random slopes for now
# TODO: correctly deal with the case when the random slope is a factor
pred <- insight::find_predictors(model, effects = "all", flatten = TRUE)
s <- s[s %in% pred]

if (length(s) > 0) {
randomslopes[[g]] <- s
}
}

# Extract coefs
for (g in randomgroups) {
intercepts <- estimate_means(model, by = g, include_random = TRUE, estimate = "average")
out[[g]] <- data.frame(
Group = g,
Level = intercepts[[g]],
Parameter = "(Intercept)",
Coefficient = intercepts$Mean,
CI_low = intercepts$CI_low,
CI_high = intercepts$CI_high,
stringsAsFactors = FALSE
)

if (g %in% names(randomslopes)) {
for (s in randomslopes[[g]]) {
slopes <- estimate_slopes(model, by = g, trend = s, include_random = TRUE)
out[[paste(g, s, sep = "_")]] <- data.frame(
Group = g,
Level = slopes[[g]],
Parameter = s,
Coefficient = slopes$Slope,
CI_low = slopes$CI_low,
CI_high = slopes$CI_high,
stringsAsFactors = FALSE
)
}
}
}
params <- do.call(rbind, out)
row.names(params) <- NULL

# TODO: robustify this function and integrate into the above

params
}
1 change: 1 addition & 0 deletions inst/WORDLIST
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ https
individuals’
intersectional
intersectionality
interpretability
intra
jmr
joss
Expand Down
33 changes: 24 additions & 9 deletions man/estimate_grouplevel.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading