From 483ffee652b3d5c5cc38c983fe1c0692e2e09c8d Mon Sep 17 00:00:00 2001 From: Daniel Date: Wed, 29 Jan 2025 16:15:11 +0100 Subject: [PATCH 1/7] `summary.estimate_slopes()` no longer working. Fixes #345 --- R/summary.R | 63 ++++++++--------------------------------------------- 1 file changed, 9 insertions(+), 54 deletions(-) diff --git a/R/summary.R b/R/summary.R index c0c2bf99f..2b137e353 100644 --- a/R/summary.R +++ b/R/summary.R @@ -1,35 +1,15 @@ #' @export summary.estimate_slopes <- function(object, ...) { - my_data <- as.data.frame(object) - trend <- attributes(object)$trend + out <- as.data.frame(object) # Add "Confidence" col based on the sig index present in the data - my_data$Confidence <- .estimate_slopes_sig(my_data, ...) + out$Confidence <- .estimate_slopes_significance(out, ...) + out$Direction <- .estimate_slopes_direction(out, ...) - # Grouping variables - vars <- attributes(object)$at - vars <- vars[!vars %in% trend] - - # If no grouping variables, summarize all - if (length(vars) == 0) { - out <- .estimate_slopes_summarize(my_data, trend = trend) - } else { - out <- data.frame() - # Create vizmatrix of grouping variables - groups <- as.data.frame(insight::get_datagrid(my_data[vars], factors = "all", numerics = "all")) - # Summarize all of the chunks - for (i in seq_len(nrow(groups))) { - g <- datawizard::data_match(my_data, groups[i, , drop = FALSE]) - out <- rbind(out, .estimate_slopes_summarize(g, trend = trend)) - } - out <- datawizard::data_relocate(out, vars) - } - - # Clean and sanitize - out$Confidence <- NULL # Drop significance col attributes(out) <- utils::modifyList(attributes(object), attributes(out)) - class(out) <- c("estimate_slopes", class(out)) + class(out) <- c("estimate_slopes", "data.frame") attr(out, "table_title") <- c("Average Marginal Effects", "blue") + out } @@ -45,43 +25,18 @@ summary.reshape_grouplevel <- function(object, ...) { # Utilities =============================================================== -.estimate_slopes_summarize <- function(data, trend, ...) { - # Find beginnings and ends ----------------------- - # First row - starting point +.estimate_slopes_direction <- function(data, ...) { centrality_columns <- datawizard::extract_column_names( data, c("Coefficient", "Slope", "Median", "Mean", "MAP_Estimate"), verbose = FALSE ) - centrality_signs <- sign(data[[centrality_columns]]) - centrality_sign <- centrality_signs[1] - sig <- data$Confidence[1] - starts <- 1 - ends <- nrow(data) - # Iterate through all rows to find blocks - for (i in 2:nrow(data)) { - if ((data$Confidence[i] != sig) || ((centrality_signs[i] != centrality_sign) && data$Confidence[i] == "Uncertain")) { - centrality_sign <- centrality_signs[i] - sig <- data$Confidence[i] - starts <- c(starts, i) - ends <- c(ends, i - 1) - } - } - ends <- sort(ends) - - # Summarize these groups ----------------------- - out <- data.frame() - for (g in seq_len(length(starts))) { - dat <- data[starts[g]:ends[g], ] - dat <- as.data.frame(insight::get_datagrid(dat, by = NULL, factors = "mode")) - dat <- cbind(data.frame(Start = data[starts[g], trend], End = data[ends[g], trend]), dat) - out <- rbind(out, dat) - } - out + centrality_signs <- as.character(sign(data[[centrality_columns]])) + datawizard::recode_into(centrality_signs == -1 ~ "negative", default = "positive") } -.estimate_slopes_sig <- function(x, confidence = "auto", ...) { +.estimate_slopes_significance <- function(x, confidence = "auto", ...) { insight::check_if_installed("effectsize") if (confidence == "auto") { From 9832acc354ac833cb0272cb80606d359f981418f Mon Sep 17 00:00:00 2001 From: Daniel Date: Wed, 29 Jan 2025 16:43:18 +0100 Subject: [PATCH 2/7] print --- DESCRIPTION | 2 +- NAMESPACE | 1 + R/print.R | 60 +++++++++++++++++++++++++++++++++++++++++++++++++++++ R/summary.R | 5 ++--- 4 files changed, 64 insertions(+), 4 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index 7f10e8ab7..25efa26af 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,7 +1,7 @@ Type: Package Package: modelbased Title: Estimation of Model-Based Predictions, Contrasts and Means -Version: 0.8.9.106 +Version: 0.8.9.107 Authors@R: c(person(given = "Dominique", family = "Makowski", diff --git a/NAMESPACE b/NAMESPACE index b37e4714a..631b0b095 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -24,6 +24,7 @@ S3method(print,estimate_means) S3method(print,estimate_predicted) S3method(print,estimate_slopes) S3method(print,estimate_smooth) +S3method(print,summary_estimate_slopes) S3method(print,visualisation_matrix) S3method(print_html,estimate_contrasts) S3method(print_html,estimate_grouplevel) diff --git a/R/print.R b/R/print.R index 73f379cd4..945237c09 100644 --- a/R/print.R +++ b/R/print.R @@ -31,6 +31,66 @@ print.visualisation_matrix <- print.estimate_contrasts #' @export print.estimate_grouplevel <- print.estimate_contrasts +#' @export +print.summary_estimate_slopes <- function(x, verbose = TRUE, ...) { + by <- attributes(x)$by + trend <- attributes(x)$trend + response <- attributes(x)$response + + if (verbose && nrow(x) < 50) { + insight::format_alert("There might be too few data to accurately determine intervals. Consider setting `length = 100` (or larger) in your call to `estimate_slopes()`.") # nolint + } + + # init messages + msg_neg <- msg_pos <- msg_unclear <- NULL + + # associations + negative_association <- which(x$Confidence == "Significant" & x$Direction == "negative") + positive_association <- which(x$Confidence == "Significant" & x$Direction == "positive") + unclear_association <- which(x$Confidence == "Not Significant") + + # sentence negative association + if (length(negative_association)) { + negative_bound <- insight::format_value(max(x[[by]][negative_association]), ...) + msg_neg <- paste0( + "The association between `", response, "` and `", trend, "` is negative for values of ", + "`", by, "` lower than ", negative_bound, "." + ) + } else { + msg_neg <- paste0( + "There were no negative associations between `", response, "` and `", trend, "`." + ) + } + # sentence positive association + if (length(positive_association)) { + positive_bound <- insight::format_value(min(x[[by]][positive_association]), ...) + msg_pos <- paste0( + "The association between `", response, "` and `", trend, "` is positive for values of ", + "`", by, "` larger than ", positive_bound, "." + ) + } else { + msg_pos <- paste0( + "There were no positive associations between `", response, "` and `", trend, "`." + ) + } + # sentence unclear association + if (length(unclear_association)) { + unclear_interval <- insight::format_ci( + x[[by]][unclear_association[1]], + x[[by]][unclear_association[length(unclear_association)]], + ci = NULL + ) + msg_unclear <- paste0( + "Inside the interval of ", unclear_interval, + ", there were no clear associations between `", + response, "` and `", trend, "`." + ) + } + + cat(insight::format_message(msg_neg, msg_pos, msg_unclear)) + cat("\n") +} + # Helper -------------------------------- diff --git a/R/summary.R b/R/summary.R index 2b137e353..82d0aa94b 100644 --- a/R/summary.R +++ b/R/summary.R @@ -7,7 +7,7 @@ summary.estimate_slopes <- function(object, ...) { out$Direction <- .estimate_slopes_direction(out, ...) attributes(out) <- utils::modifyList(attributes(object), attributes(out)) - class(out) <- c("estimate_slopes", "data.frame") + class(out) <- c("summary_estimate_slopes", "data.frame") attr(out, "table_title") <- c("Average Marginal Effects", "blue") out @@ -31,8 +31,7 @@ summary.reshape_grouplevel <- function(object, ...) { c("Coefficient", "Slope", "Median", "Mean", "MAP_Estimate"), verbose = FALSE ) - centrality_signs <- as.character(sign(data[[centrality_columns]])) - datawizard::recode_into(centrality_signs == -1 ~ "negative", default = "positive") + ifelse(data[[centrality_columns]] < 0, "negative", "positive") } From 7546a68faba0a520e3d408c2f60dcaed3017a4b2 Mon Sep 17 00:00:00 2001 From: Daniel Date: Wed, 29 Jan 2025 16:59:31 +0100 Subject: [PATCH 3/7] fix example --- R/estimate_slopes.R | 1 - R/print.R | 5 +++++ man/estimate_slopes.Rd | 1 - 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/R/estimate_slopes.R b/R/estimate_slopes.R index e3cf08b50..f5b72bd5f 100644 --- a/R/estimate_slopes.R +++ b/R/estimate_slopes.R @@ -107,7 +107,6 @@ #' trend = "Petal.Length", #' by = c("Petal.Length", "Species"), length = 20 #' ) -#' summary(slopes) #' plot(slopes) #' } #' @export diff --git a/R/print.R b/R/print.R index 945237c09..383c95479 100644 --- a/R/print.R +++ b/R/print.R @@ -37,6 +37,11 @@ print.summary_estimate_slopes <- function(x, verbose = TRUE, ...) { trend <- attributes(x)$trend response <- attributes(x)$response + ## TODO: make by > 1 work + if (length(by) > 1) { + insight::format_error("`summary()` not implemented for more than one stratification variable in `by` yet.") + } + if (verbose && nrow(x) < 50) { insight::format_alert("There might be too few data to accurately determine intervals. Consider setting `length = 100` (or larger) in your call to `estimate_slopes()`.") # nolint } diff --git a/man/estimate_slopes.Rd b/man/estimate_slopes.Rd index cc29d468c..a0d24176c 100644 --- a/man/estimate_slopes.Rd +++ b/man/estimate_slopes.Rd @@ -163,7 +163,6 @@ slopes <- estimate_slopes(model, trend = "Petal.Length", by = c("Petal.Length", "Species"), length = 20 ) -summary(slopes) plot(slopes) } \dontshow{\}) # examplesIf} From 85f9cfb8e0a262919d785bdde89cf5fe4254942c Mon Sep 17 00:00:00 2001 From: Daniel Date: Wed, 29 Jan 2025 17:33:26 +0100 Subject: [PATCH 4/7] revise --- NAMESPACE | 1 + R/format.R | 6 +++++ R/print.R | 68 +++-------------------------------------------------- R/summary.R | 40 +++++++++++++++++++++++++++++++ 4 files changed, 50 insertions(+), 65 deletions(-) diff --git a/NAMESPACE b/NAMESPACE index 631b0b095..622cb423f 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -12,6 +12,7 @@ S3method(format,estimate_smooth) S3method(format,marginaleffects_contrasts) S3method(format,marginaleffects_means) S3method(format,marginaleffects_slopes) +S3method(format,summary_estimate_slopes) S3method(format,visualisation_matrix) S3method(plot,estimate_contrasts) S3method(plot,estimate_grouplevel) diff --git a/R/format.R b/R/format.R index 8f1a5ddce..1d5f19b6b 100644 --- a/R/format.R +++ b/R/format.R @@ -70,6 +70,12 @@ format.visualisation_matrix <- function(x, ...) { } +#' @export +format.summary_estimate_slopes <- function(x, ...) { + insight::format_table(x, ...) +} + + #' @export format.marginaleffects_means <- function(x, model, ci = 0.95, ...) { # model information diff --git a/R/print.R b/R/print.R index 383c95479..276282aa5 100644 --- a/R/print.R +++ b/R/print.R @@ -19,6 +19,9 @@ print.estimate_means <- print.estimate_contrasts #' @export print.estimate_slopes <- print.estimate_contrasts +#' @export +print.summary_estimate_slopes <- print.estimate_contrasts + #' @export print.estimate_smooth <- print.estimate_contrasts @@ -31,71 +34,6 @@ print.visualisation_matrix <- print.estimate_contrasts #' @export print.estimate_grouplevel <- print.estimate_contrasts -#' @export -print.summary_estimate_slopes <- function(x, verbose = TRUE, ...) { - by <- attributes(x)$by - trend <- attributes(x)$trend - response <- attributes(x)$response - - ## TODO: make by > 1 work - if (length(by) > 1) { - insight::format_error("`summary()` not implemented for more than one stratification variable in `by` yet.") - } - - if (verbose && nrow(x) < 50) { - insight::format_alert("There might be too few data to accurately determine intervals. Consider setting `length = 100` (or larger) in your call to `estimate_slopes()`.") # nolint - } - - # init messages - msg_neg <- msg_pos <- msg_unclear <- NULL - - # associations - negative_association <- which(x$Confidence == "Significant" & x$Direction == "negative") - positive_association <- which(x$Confidence == "Significant" & x$Direction == "positive") - unclear_association <- which(x$Confidence == "Not Significant") - - # sentence negative association - if (length(negative_association)) { - negative_bound <- insight::format_value(max(x[[by]][negative_association]), ...) - msg_neg <- paste0( - "The association between `", response, "` and `", trend, "` is negative for values of ", - "`", by, "` lower than ", negative_bound, "." - ) - } else { - msg_neg <- paste0( - "There were no negative associations between `", response, "` and `", trend, "`." - ) - } - # sentence positive association - if (length(positive_association)) { - positive_bound <- insight::format_value(min(x[[by]][positive_association]), ...) - msg_pos <- paste0( - "The association between `", response, "` and `", trend, "` is positive for values of ", - "`", by, "` larger than ", positive_bound, "." - ) - } else { - msg_pos <- paste0( - "There were no positive associations between `", response, "` and `", trend, "`." - ) - } - # sentence unclear association - if (length(unclear_association)) { - unclear_interval <- insight::format_ci( - x[[by]][unclear_association[1]], - x[[by]][unclear_association[length(unclear_association)]], - ci = NULL - ) - msg_unclear <- paste0( - "Inside the interval of ", unclear_interval, - ", there were no clear associations between `", - response, "` and `", trend, "`." - ) - } - - cat(insight::format_message(msg_neg, msg_pos, msg_unclear)) - cat("\n") -} - # Helper -------------------------------- diff --git a/R/summary.R b/R/summary.R index 82d0aa94b..6180147e7 100644 --- a/R/summary.R +++ b/R/summary.R @@ -1,11 +1,21 @@ #' @export summary.estimate_slopes <- function(object, ...) { out <- as.data.frame(object) + by <- attributes(object)$by # Add "Confidence" col based on the sig index present in the data out$Confidence <- .estimate_slopes_significance(out, ...) out$Direction <- .estimate_slopes_direction(out, ...) + if (length(by) > 1) { + parts <- split(out, out[[by[2]]]) + out <- do.call(rbind, lapply(parts, .estimate_slope_parts, by = by[1])) + out <- datawizard::rownames_as_column(out, "Group") + out$Group <- gsub("\\.\\d+$", "", out$Group) + } else { + out <- .estimate_slope_parts(out, by) + } + attributes(out) <- utils::modifyList(attributes(object), attributes(out)) class(out) <- c("summary_estimate_slopes", "data.frame") attr(out, "table_title") <- c("Average Marginal Effects", "blue") @@ -25,6 +35,36 @@ summary.reshape_grouplevel <- function(object, ...) { # Utilities =============================================================== +.estimate_slope_parts <- function(out, by) { + # filter significant values only +# out <- out[out$Confidence == "Significant", ] + + # mark all "changes" from negative to positive and vice versa + index <- 1 + out$switch <- index + index <- index + 1 + + for (i in 2:nrow(out)) { + if (out$Direction[i] != out$Direction[i - 1] || out$Confidence[i] != out$Confidence[i - 1]) { + out$switch[i:nrow(out)] <- index + index <- index + 1 + } + } + + # split into "switches" + parts <- split(out, out$switch) + + do.call(rbind, lapply(parts, function(i) { + data.frame( + Start = i[[by]][1], + End = i[[by]][nrow(i)], + Direction = i$Direction[1], + Confidence = i$Confidence[1] + ) + })) +} + + .estimate_slopes_direction <- function(data, ...) { centrality_columns <- datawizard::extract_column_names( data, From 12d7f89f8df4fdc631b84d72f80d7e1951e5b16f Mon Sep 17 00:00:00 2001 From: Daniel Date: Wed, 29 Jan 2025 17:35:45 +0100 Subject: [PATCH 5/7] still show msg --- R/summary.R | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/R/summary.R b/R/summary.R index 6180147e7..45846e65b 100644 --- a/R/summary.R +++ b/R/summary.R @@ -1,8 +1,12 @@ #' @export -summary.estimate_slopes <- function(object, ...) { +summary.estimate_slopes <- function(object, verbose = TRUE, ...) { out <- as.data.frame(object) by <- attributes(object)$by + if (verbose && nrow(out) < 50) { + insight::format_alert("There might be too few data to accurately determine intervals. Consider setting `length = 100` (or larger) in your call to `estimate_slopes()`.") # nolint + } + # Add "Confidence" col based on the sig index present in the data out$Confidence <- .estimate_slopes_significance(out, ...) out$Direction <- .estimate_slopes_direction(out, ...) From a266d861b8a2a64e541acb55fba62213cccb4655 Mon Sep 17 00:00:00 2001 From: Daniel Date: Wed, 29 Jan 2025 17:36:41 +0100 Subject: [PATCH 6/7] comments --- R/summary.R | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/R/summary.R b/R/summary.R index 45846e65b..1e2e2515d 100644 --- a/R/summary.R +++ b/R/summary.R @@ -11,6 +11,8 @@ summary.estimate_slopes <- function(object, verbose = TRUE, ...) { out$Confidence <- .estimate_slopes_significance(out, ...) out$Direction <- .estimate_slopes_direction(out, ...) + # if we have more than one variable in `by`, group result table and + # add group name as separate column if (length(by) > 1) { parts <- split(out, out[[by[2]]]) out <- do.call(rbind, lapply(parts, .estimate_slope_parts, by = by[1])) @@ -40,9 +42,6 @@ summary.reshape_grouplevel <- function(object, ...) { .estimate_slope_parts <- function(out, by) { - # filter significant values only -# out <- out[out$Confidence == "Significant", ] - # mark all "changes" from negative to positive and vice versa index <- 1 out$switch <- index From 6509fee01110d1a8495fdb9402fe05c02f87b014 Mon Sep 17 00:00:00 2001 From: Daniel Date: Wed, 29 Jan 2025 17:38:12 +0100 Subject: [PATCH 7/7] ad back to examples --- R/estimate_slopes.R | 1 + man/estimate_slopes.Rd | 1 + 2 files changed, 2 insertions(+) diff --git a/R/estimate_slopes.R b/R/estimate_slopes.R index f5b72bd5f..e3cf08b50 100644 --- a/R/estimate_slopes.R +++ b/R/estimate_slopes.R @@ -107,6 +107,7 @@ #' trend = "Petal.Length", #' by = c("Petal.Length", "Species"), length = 20 #' ) +#' summary(slopes) #' plot(slopes) #' } #' @export diff --git a/man/estimate_slopes.Rd b/man/estimate_slopes.Rd index a0d24176c..cc29d468c 100644 --- a/man/estimate_slopes.Rd +++ b/man/estimate_slopes.Rd @@ -163,6 +163,7 @@ slopes <- estimate_slopes(model, trend = "Petal.Length", by = c("Petal.Length", "Species"), length = 20 ) +summary(slopes) plot(slopes) } \dontshow{\}) # examplesIf}