From bfab7931029b4754c3c276c6183e3795e5a3ee7f Mon Sep 17 00:00:00 2001 From: DominiqueMakowski Date: Mon, 25 May 2026 10:52:32 +0100 Subject: [PATCH 01/10] Initial --- DESCRIPTION | 3 + NAMESPACE | 4 +- NEWS.md | 14 ++ R/report.check_outliers.R | 112 ++++++++++++ R/report.lm.R | 14 ++ R/report_ai.R | 18 +- R/report_assumptions.R | 218 +++++++++++++++++++++++ man/reexports.Rd | 2 +- man/report-package.Rd | 1 - man/report.Rd | 13 +- man/report.compare.loo.Rd | 2 +- man/report_ai.Rd | 45 ----- man/report_assumptions.Rd | 48 +++++ man/report_text.check_outliers.Rd | 74 ++++++++ tests/testthat/test-report_assumptions.R | 176 ++++++++++++++++++ vignettes/report_ai.Rmd | 7 +- 16 files changed, 693 insertions(+), 58 deletions(-) create mode 100644 R/report.check_outliers.R create mode 100644 R/report_assumptions.R delete mode 100644 man/report_ai.Rd create mode 100644 man/report_assumptions.Rd create mode 100644 man/report_text.check_outliers.Rd create mode 100644 tests/testthat/test-report_assumptions.R diff --git a/DESCRIPTION b/DESCRIPTION index 868ce116e..c978fd675 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -117,6 +117,7 @@ Collate: 'report.stanreg.R' 'report.brmsfit.R' 'report.character.R' + 'report.check_outliers.R' 'report.compare.loo.R' 'report.compare_performance.R' 'report.coxph.R' @@ -135,6 +136,7 @@ Collate: 'report.test_performance.R' 'report.zeroinfl.R' 'report_ai.R' + 'report_assumptions.R' 'report_effectsize.R' 'report_htest_chi2.R' 'report_htest_cor.R' @@ -162,3 +164,4 @@ Collate: 'zzz.R' Roxygen: list(markdown = TRUE) Config/roxygen2/version: 8.0.0 +RoxygenNote: 7.3.3 diff --git a/NAMESPACE b/NAMESPACE index 3447bd881..174a6ad6e 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -38,6 +38,7 @@ S3method(report,bayesfactor_inclusion) S3method(report,bayesfactor_models) S3method(report,brmsfit) S3method(report,character) +S3method(report,check_outliers) S3method(report,compare.loo) S3method(report,compare_performance) S3method(report,coxph) @@ -247,6 +248,7 @@ S3method(report_text,bayesfactor_inclusion) S3method(report_text,bayesfactor_models) S3method(report_text,brmsfit) S3method(report_text,character) +S3method(report_text,check_outliers) S3method(report_text,compare_performance) S3method(report_text,coxph) S3method(report_text,data.frame) @@ -305,7 +307,7 @@ export(is.report) export(print_html) export(print_md) export(report) -export(report_ai) +export(report_assumptions) export(report_date) export(report_effectsize) export(report_info) diff --git a/NEWS.md b/NEWS.md index 3b4df89e9..dbbd95cf3 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,9 +1,23 @@ # report (devel) +New features + +* `report_assumptions()`: new function that reports model assumption checks for a model, covering influential observations (via `performance::check_outliers()`) and homoskedasticity (via `performance::check_heteroskedasticity()`). Calling `summary()` on the result returns a compact one-sentence version integrating all checks. Supports `audience = "ai"` for a compact token-efficient structured output. + +* `report()` / `report_text()` for linear models: assumption checks (from `report_assumptions()`) are now automatically included at the end of the report. For human audience, the compact one-sentence summary is appended; for AI audience, a structured `## Assumptions` section is added to the output. + +Bug fixes + +* Fixed missing `@exportS3Method` roxygen2 tags on `report_ai` S3 methods (`default`, `lm`, `glm`, `merMod`, `glmmTMB`), which caused warnings during package loading. + +* `report_text()`: new method for `performance::check_outliers()` output, producing a brief sentence such as "2 observations (5.88%) were detected as potential outliers based on Cook's distance (threshold = 0.806)." + Bug fixes * Fixed an issue in `report()` where the reference level for logical predictors was incorrectly displayed as `[?]` instead of `FALSE` for the intercept (@M-Colley, #598). +* Fixed the *AI-Optimized Reports* vignette to no longer refer to the internal `report_ai()` function; all examples now use `report(x, audience = "ai")`. + # report 0.6.4 New features diff --git a/R/report.check_outliers.R b/R/report.check_outliers.R new file mode 100644 index 000000000..b2a860a0c --- /dev/null +++ b/R/report.check_outliers.R @@ -0,0 +1,112 @@ +#' Reporting outlier detection results +#' +#' Create a brief textual report for outlier detection results from +#' [performance::check_outliers()]. +#' +#' @param x Object of class `check_outliers` as returned by +#' [performance::check_outliers()]. +#' @inheritParams report +#' +#' @inherit report return seealso +#' +#' @examplesIf requireNamespace("performance", quietly = TRUE) +#' \donttest{ +#' library(report) +#' library(performance) +#' +#' mt2 <- rbind( +#' mtcars[, c("mpg", "disp", "hp")], +#' data.frame(mpg = c(37, 40), disp = c(300, 400), hp = c(110, 120)) +#' ) +#' model <- lm(disp ~ mpg + hp, data = mt2) +#' rez <- performance::check_outliers(model) +#' report(rez) +#' } +#' +#' @return An object of class [report_text()]. +#' @export +report_text.check_outliers <- function(x, ...) { + n_outliers <- sum(x) + n_total <- length(x) + pct <- n_outliers / n_total + + methods <- attr(x, "method") + thresholds <- attr(x, "threshold") + + method_labels <- .format_outlier_method_names(methods) + + method_parts <- vapply( + seq_along(methods), + function(i) { + m <- methods[i] + thresh <- thresholds[[m]] + if (!is.null(thresh) && length(thresh) == 1L && !is.na(thresh)) { + paste0( + method_labels[i], + " (threshold = ", + insight::format_value(thresh, digits = 3), + ")" + ) + } else { + method_labels[i] + } + }, + character(1L) + ) + + method_str <- paste(method_parts, collapse = " and ") + + if (n_outliers == 0L) { + text <- "No observations were detected as potential outliers." + } else if (n_outliers == 1L) { + text <- paste0( + "1 observation (", + insight::format_value(pct, as_percent = TRUE), + ") was detected as a potential outlier based on ", + method_str, + "." + ) + } else { + text <- paste0( + n_outliers, + " observations (", + insight::format_value(pct, as_percent = TRUE), + ") were detected as potential outliers based on ", + method_str, + "." + ) + } + + as.report_text(text) +} + + +#' @rdname report_text.check_outliers +#' @export +report.check_outliers <- function(x, ...) { + result_text <- report_text(x, ...) + as.report(text = result_text, ...) +} + + +# Helper ------------------------------------------------------------------ + +.format_outlier_method_names <- function(methods) { + method_map <- c( + cook = "Cook's distance", + mahalanobis = "Mahalanobis distance", + mahalanobis_robust = "robust Mahalanobis distance", + zscore = "z-score", + zscore_robust = "robust z-score", + iqr = "IQR", + ci = "confidence intervals", + optics = "OPTICS", + iforest = "Isolation Forest", + lof = "Local Outlier Factor", + mcd = "MCD" + ) + labels <- method_map[methods] + # Fall back to the raw method name for any unknown method + labels[is.na(labels)] <- methods[is.na(labels)] + unname(labels) +} diff --git a/R/report.lm.R b/R/report.lm.R index 872576371..b34e64c95 100644 --- a/R/report.lm.R +++ b/R/report.lm.R @@ -753,6 +753,18 @@ report_text.lm <- function(x, table = NULL, ...) { ) } + # --- Assumptions (requires performance) --------------------------------- + assumptions_summary <- "" + if (requireNamespace("performance", quietly = TRUE)) { + assumptions_obj <- tryCatch( + report_assumptions(x), + error = function(e) NULL + ) + if (!is.null(assumptions_obj)) { + assumptions_summary <- as.character(summary(assumptions_obj)) + } + } + # Helpers sep_after <- function(x) { x <- trimws(x) @@ -773,6 +785,7 @@ report_text.lm <- function(x, table = NULL, ...) { "We fitted a ", model, ". ", + if (nzchar(assumptions_summary)) paste0(assumptions_summary, " ") else "", perf, sep_after(perf), intercept, @@ -787,6 +800,7 @@ report_text.lm <- function(x, table = NULL, ...) { "We fitted a ", summary(model), ". ", + if (nzchar(assumptions_summary)) paste0(assumptions_summary, " ") else "", summary(perf), sep_after(summary(perf)), summary(intercept), diff --git a/R/report_ai.R b/R/report_ai.R index e7a123892..02707528c 100644 --- a/R/report_ai.R +++ b/R/report_ai.R @@ -3,6 +3,7 @@ report_ai <- function(x, ...) { UseMethod("report_ai") } +#' @exportS3Method report_ai.default <- function(x, ...) { insight::format_warning( paste0( @@ -14,16 +15,22 @@ report_ai.default <- function(x, ...) { report(x, ..., audience = "humans") } +#' @exportS3Method report_ai.lm <- function(x, ...) { .report_ai_models(x, ...) } -report_ai.glm <- report_ai.lm +#' @exportS3Method +report_ai.glm <- function(x, ...) { + .report_ai_models(x, ...) +} +#' @exportS3Method report_ai.merMod <- function(x, ...) { .report_ai_models(x, ...) } +#' @exportS3Method report_ai.glmmTMB <- function(x, ...) { .report_ai_models(x, ...) } @@ -79,6 +86,8 @@ report_ai.glmmTMB <- function(x, ...) { } else { desc_str <- "- No variables found." } + # Remove any leading blank lines introduced by the report() header + desc_str <- sub("^\\n+", "", desc_str) params <- parameters::model_parameters(x, ...) @@ -153,6 +162,12 @@ report_ai.glmmTMB <- function(x, ...) { perf_markdown <- insight::export_table(perf_table, format = "markdown") perf_str <- paste(perf_markdown, collapse = "\n") + # Assumptions (requires performance; silently skipped if unsupported) + assumptions_str <- tryCatch( + as.character(report_assumptions(x, audience = "ai")), + error = function(e) NULL + ) + if ("p" %in% names(fixed_params) && "Parameter" %in% names(fixed_params)) { sig_effects <- fixed_params$Parameter[ !is.na(fixed_params$p) & @@ -221,6 +236,7 @@ report_ai.glmmTMB <- function(x, ...) { "\n\n", "## Variables\n", desc_str, + if (!is.null(assumptions_str)) paste0("\n\n", assumptions_str) else "", "\n\n", param_section, "\n\n", diff --git a/R/report_assumptions.R b/R/report_assumptions.R new file mode 100644 index 000000000..5f051cade --- /dev/null +++ b/R/report_assumptions.R @@ -0,0 +1,218 @@ +#' Report model assumption checks +#' +#' Produces a formatted summary of model assumption checks, covering +#' **influential observations** (via [performance::check_outliers()]) and +#' **homoskedasticity** (via [performance::check_heteroskedasticity()]). +#' Calling [summary()] on the result returns a single integrated sentence. +#' Set `audience = "ai"` for a compact, token-efficient version suitable for +#' passing to an LLM. +#' +#' @param x A statistical model object (e.g., `lm`, `glm`). +#' @param audience The intended audience. `"humans"` (default) produces a +#' readable bulleted report; `"ai"` produces a compact structured version. +#' The default can be changed globally with `options(report_audience = "ai")`. +#' @param ... Additional arguments passed to [performance::check_outliers()]. +#' +#' @return An object of class [report_text()]. [summary()] returns a compact +#' one-sentence version integrating all checks (for `audience = "humans"`). +#' +#' @examplesIf requireNamespace("performance", quietly = TRUE) +#' \donttest{ +#' model <- lm(mpg ~ wt + hp, data = mtcars) +#' report_assumptions(model) +#' summary(report_assumptions(model)) +#' report_assumptions(model, audience = "ai") +#' +#' mt2 <- rbind( +#' mtcars[, c("mpg", "disp", "hp")], +#' data.frame(mpg = c(37, 40), disp = c(300, 400), hp = c(110, 120)) +#' ) +#' model2 <- lm(disp ~ mpg + hp, data = mt2) +#' report_assumptions(model2) +#' summary(report_assumptions(model2)) +#' report_assumptions(model2, audience = "ai") +#' } +#' +#' @export +report_assumptions <- function( + x, + ..., + audience = getOption("report_audience", "humans") +) { + insight::check_if_installed("performance") + audience <- match.arg(audience, c("humans", "ai")) + + # TODO: add the following assumption checks: + # - Linearity performance::check_predictions() / check_linearity() + # - Collinearity performance::check_collinearity() + # - Normality performance::check_normality() + # - Autocorrelation performance::check_autocorrelation() + # - Overdispersion performance::check_overdispersion() + + # --- Influential observations ------------------------------------------- + outliers <- tryCatch( + performance::check_outliers(x, ...), + error = function(e) NULL + ) + + # --- Homoskedasticity --------------------------------------------------- + heterosk <- tryCatch( + performance::check_heteroskedasticity(x), + error = function(e) NULL + ) + if (!is.null(heterosk)) { + p_val <- as.numeric(heterosk) + p_fmt <- insight::format_p(p_val) + homosked_ok <- p_val >= 0.05 + } + + # --- AI output ---------------------------------------------------------- + if (audience == "ai") { + if (!is.null(outliers)) { + n_outliers <- sum(outliers) + n_total <- length(outliers) + if (n_outliers == 0L) { + outlier_ai <- "none" + } else { + pct <- insight::format_value(n_outliers / n_total, as_percent = TRUE) + methods <- attr(outliers, "method") + thresholds <- attr(outliers, "threshold") + method_labels <- .format_outlier_method_names(methods) + thresh_parts <- vapply( + seq_along(methods), + function(i) { + thresh <- thresholds[[methods[i]]] + if (!is.null(thresh) && length(thresh) == 1L && !is.na(thresh)) { + paste0( + method_labels[i], + ", threshold = ", + insight::format_value(thresh, digits = 3) + ) + } else { + method_labels[i] + } + }, + character(1L) + ) + outlier_ai <- paste0( + n_outliers, + "/", + n_total, + " (", + pct, + ") [", + paste(thresh_parts, collapse = "; "), + "]" + ) + } + } else { + outlier_ai <- "N/A" + } + + if (!is.null(heterosk)) { + heterosk_ai <- if (homosked_ok) { + paste0("OK (Breusch-Pagan, ", p_fmt, ")") + } else { + paste0("VIOLATED (Breusch-Pagan, ", p_fmt, ")") + } + } else { + heterosk_ai <- "N/A" + } + + lines <- c( + "## Assumptions", + paste0("- Influential Observations: ", outlier_ai), + paste0("- Homoskedasticity: ", heterosk_ai) + ) + res <- paste(lines, collapse = "\n") + class(res) <- c("report_ai", "character") + return(res) + } + + # --- Full bulleted output (humans) -------------------------------------- + lines_bullets <- c( + "Model Assumptions", + paste(rep("-", 24L), collapse = "") + ) + phrases <- character(0L) + + if (!is.null(outliers)) { + outlier_bullet <- as.character(report_text(outliers)) + lines_bullets <- c( + lines_bullets, + paste0("- Influential observations: ", outlier_bullet) + ) + phrases <- c(phrases, .outlier_summary_phrase(outliers)) + } + + if (!is.null(heterosk)) { + if (homosked_ok) { + heterosk_bullet <- paste0( + "Homoskedasticity was satisfied (Breusch-Pagan test, ", + p_fmt, + ")." + ) + heterosk_phrase <- paste0( + "the error variance appeared homoskedastic (", + p_fmt, + ")" + ) + } else { + heterosk_bullet <- paste0( + "Heteroskedasticity was detected (Breusch-Pagan test, ", + p_fmt, + ")." + ) + heterosk_phrase <- paste0("heteroskedasticity was detected (", p_fmt, ")") + } + lines_bullets <- c( + lines_bullets, + paste0("- Homoskedasticity: ", heterosk_bullet) + ) + phrases <- c(phrases, heterosk_phrase) + } + + if (length(phrases) == 0L) { + lines_bullets <- c( + lines_bullets, + "- Assumption checks could not be performed for this model class." + ) + text_summary <- "Assumption checks could not be performed for this model class." + } else { + text_summary <- paste0( + "The model's assumptions were checked: ", + paste(phrases, collapse = " and "), + "." + ) + } + + as.report_text(paste(lines_bullets, collapse = "\n"), summary = text_summary) +} + + +# Helpers ----------------------------------------------------------------- + +.outlier_summary_phrase <- function(outliers) { + n_outliers <- sum(outliers) + + if (n_outliers == 0L) { + return("no influential observations were detected") + } + + pct <- n_outliers / length(outliers) + methods <- attr(outliers, "method") + method_labels <- .format_outlier_method_names(methods) + method_str <- paste(method_labels, collapse = " and ") + + paste0( + n_outliers, + " influential ", + if (n_outliers == 1L) "observation" else "observations", + " (", + insight::format_value(pct, as_percent = TRUE), + ")", + " were detected (", + method_str, + ")" + ) +} diff --git a/man/reexports.Rd b/man/reexports.Rd index eef46699c..87b8b45bd 100644 --- a/man/reexports.Rd +++ b/man/reexports.Rd @@ -13,6 +13,6 @@ These objects are imported from other packages. Follow the links below to see their documentation. \describe{ - \item{insight}{\code{\link[insight:display]{display()}}, \code{\link[insight:print_html]{print_html()}}, \code{\link[insight:print_md]{print_md()}}} + \item{insight}{\code{\link[insight]{display}}, \code{\link[insight:display]{print_html}}, \code{\link[insight:display]{print_md}}} }} diff --git a/man/report-package.Rd b/man/report-package.Rd index 86d188672..ce41d4924 100644 --- a/man/report-package.Rd +++ b/man/report-package.Rd @@ -28,7 +28,6 @@ Useful links: Authors: \itemize{ - \item Rémi Thériault \email{remi.theriault@mail.mcgill.ca} (\href{https://orcid.org/0000-0003-4315-6788}{ORCID}) \item Dominique Makowski \email{dom.makowski@gmail.com} (\href{https://orcid.org/0000-0001-5375-9967}{ORCID}) \item Daniel Lüdecke \email{d.luedecke@uke.de} (\href{https://orcid.org/0000-0002-8895-3206}{ORCID}) \item Indrajeet Patil \email{patilindrajeet.science@gmail.com} (\href{https://orcid.org/0000-0003-1995-6531}{ORCID}) diff --git a/man/report.Rd b/man/report.Rd index 181175af8..f95cc5b63 100644 --- a/man/report.Rd +++ b/man/report.Rd @@ -13,10 +13,15 @@ objects above).} \item{...}{Arguments passed to or from other methods.} \item{audience}{The intended audience for the report. \code{"humans"} (default) -produces the standard formatted text report. \code{"ai"} produces a compact, -structured output optimised for consumption by a Large Language Model (LLM) -or AI agent via \code{\link[=report_ai]{report_ai()}}. The default can be changed globally with -\code{options(report_audience = "ai")}.} +produces the standard narrative text report. \code{"ai"} produces a compact, +structured Markdown output designed for consumption by a Large Language +Model (LLM) or AI agent. It strikes a careful balance between +comprehensiveness, specificity, and compactness, giving the model the +clearest and most relevant analytical information at the lowest possible +token cost. The output is a single character vector of class \code{report_ai} +that can be pasted directly into a chat window or fed to an LLM API. +The default can be changed globally with \code{options(report_audience = "ai")}. +See \code{vignette("report_ai", package = "report")} for details and examples.} } \value{ A list-object of class \code{report}, which contains further diff --git a/man/report.compare.loo.Rd b/man/report.compare.loo.Rd index 7e715b70a..ae8395304 100644 --- a/man/report.compare.loo.Rd +++ b/man/report.compare.loo.Rd @@ -7,7 +7,7 @@ \method{report}{compare.loo}(x, include_IC = TRUE, include_ENP = FALSE, ...) } \arguments{ -\item{x}{An object of class \link[brms:loo_compare]{brms::loo_compare}.} +\item{x}{An object of class \link[brms:loo_compare.brmsfit]{brms::loo_compare}.} \item{include_IC}{Whether to include the information criteria (IC).} diff --git a/man/report_ai.Rd b/man/report_ai.Rd deleted file mode 100644 index f2488289b..000000000 --- a/man/report_ai.Rd +++ /dev/null @@ -1,45 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/report_ai.R -\name{report_ai} -\alias{report_ai} -\alias{report_ai.merMod} -\alias{report_ai.glmmTMB} -\title{Generate AI-optimized reports} -\usage{ -report_ai(x, ...) - -\method{report_ai}{merMod}(x, ...) - -\method{report_ai}{glmmTMB}(x, ...) -} -\arguments{ -\item{x}{A statistical model.} - -\item{...}{Arguments passed to other functions, like \code{parameters::model_parameters()}, -\code{performance::model_performance()} or \code{insight::format_table()}.} -} -\value{ -A character vector of class \code{report_ai} containing the formatted text. -} -\description{ -This function is designed to produce AI-optimized output for statistical models. -It strikes a careful balance between comprehensiveness, specificity, and compactness. -The primary goal is to provide a Large Language Model (LLM) or AI agent with the -clearest and most relevant analytical information at the lowest possible token cost. -} -\examples{ -m <- lm(mpg ~ wt + hp, data = mtcars) -report_ai(m) -\dontshow{if (requireNamespace("lme4", quietly = TRUE)) withAutoprint(\{ # examplesIf} -\donttest{ -m <- lme4::lmer(Reaction ~ Days + (1 | Subject), data = lme4::sleepstudy) -report_ai(m) -} -\dontshow{\}) # examplesIf} -\dontshow{if (requireNamespace("glmmTMB", quietly = TRUE)) withAutoprint(\{ # examplesIf} -\donttest{ -m <- glmmTMB::glmmTMB(count ~ mined + (1 | site), family = poisson(), data = glmmTMB::Salamanders) -report_ai(m) -} -\dontshow{\}) # examplesIf} -} diff --git a/man/report_assumptions.Rd b/man/report_assumptions.Rd new file mode 100644 index 000000000..0ceb8e2d4 --- /dev/null +++ b/man/report_assumptions.Rd @@ -0,0 +1,48 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/report_assumptions.R +\name{report_assumptions} +\alias{report_assumptions} +\title{Report model assumption checks} +\usage{ +report_assumptions(x, ..., audience = getOption("report_audience", "humans")) +} +\arguments{ +\item{x}{A statistical model object (e.g., \code{lm}, \code{glm}).} + +\item{...}{Additional arguments passed to \code{\link[performance:check_outliers]{performance::check_outliers()}}.} + +\item{audience}{The intended audience. \code{"humans"} (default) produces a +readable bulleted report; \code{"ai"} produces a compact structured version. +The default can be changed globally with \code{options(report_audience = "ai")}.} +} +\value{ +An object of class \code{\link[=report_text]{report_text()}}. \code{\link[=summary]{summary()}} returns a compact +one-sentence version integrating all checks (for \code{audience = "humans"}). +} +\description{ +Produces a formatted summary of model assumption checks, covering +\strong{influential observations} (via \code{\link[performance:check_outliers]{performance::check_outliers()}}) and +\strong{homoskedasticity} (via \code{\link[performance:check_heteroscedasticity]{performance::check_heteroskedasticity()}}). +Calling \code{\link[=summary]{summary()}} on the result returns a single integrated sentence. +Set \code{audience = "ai"} for a compact, token-efficient version suitable for +passing to an LLM. +} +\examples{ +\dontshow{if (requireNamespace("performance", quietly = TRUE)) withAutoprint(\{ # examplesIf} +\donttest{ +model <- lm(mpg ~ wt + hp, data = mtcars) +report_assumptions(model) +summary(report_assumptions(model)) +report_assumptions(model, audience = "ai") + +mt2 <- rbind( + mtcars[, c("mpg", "disp", "hp")], + data.frame(mpg = c(37, 40), disp = c(300, 400), hp = c(110, 120)) +) +model2 <- lm(disp ~ mpg + hp, data = mt2) +report_assumptions(model2) +summary(report_assumptions(model2)) +report_assumptions(model2, audience = "ai") +} +\dontshow{\}) # examplesIf} +} diff --git a/man/report_text.check_outliers.Rd b/man/report_text.check_outliers.Rd new file mode 100644 index 000000000..d49ca4b23 --- /dev/null +++ b/man/report_text.check_outliers.Rd @@ -0,0 +1,74 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/report.check_outliers.R +\name{report_text.check_outliers} +\alias{report_text.check_outliers} +\alias{report.check_outliers} +\title{Reporting outlier detection results} +\usage{ +\method{report_text}{check_outliers}(x, ...) + +\method{report}{check_outliers}(x, ...) +} +\arguments{ +\item{x}{Object of class \code{check_outliers} as returned by +\code{\link[performance:check_outliers]{performance::check_outliers()}}.} + +\item{...}{Arguments passed to or from other methods.} +} +\value{ +An object of class \code{\link[=report_text]{report_text()}}. +} +\description{ +Create a brief textual report for outlier detection results from +\code{\link[performance:check_outliers]{performance::check_outliers()}}. +} +\examples{ +\dontshow{if (requireNamespace("performance", quietly = TRUE)) withAutoprint(\{ # examplesIf} +\donttest{ +library(report) +library(performance) + +mt2 <- rbind( + mtcars[, c("mpg", "disp", "hp")], + data.frame(mpg = c(37, 40), disp = c(300, 400), hp = c(110, 120)) +) +model <- lm(disp ~ mpg + hp, data = mt2) +rez <- performance::check_outliers(model) +report(rez) +} +\dontshow{\}) # examplesIf} +} +\seealso{ +Specific components of reports (especially for stats models): +\itemize{ +\item \code{\link[=report_table]{report_table()}} +\item \code{\link[=report_parameters]{report_parameters()}} +\item \code{\link[=report_statistics]{report_statistics()}} +\item \code{\link[=report_effectsize]{report_effectsize()}} +\item \code{\link[=report_model]{report_model()}} +\item \code{\link[=report_priors]{report_priors()}} +\item \code{\link[=report_random]{report_random()}} +\item \code{\link[=report_performance]{report_performance()}} +\item \code{\link[=report_info]{report_info()}} +\item \code{\link[=report_text]{report_text()}} +} + +Other types of reports: +\itemize{ +\item \code{\link[=report_system]{report_system()}} +\item \code{\link[=report_packages]{report_packages()}} +\item \code{\link[=report_participants]{report_participants()}} +\item \code{\link[=report_sample]{report_sample()}} +\item \code{\link[=report_date]{report_date()}} +} + +Methods: +\itemize{ +\item \code{\link[=as.report]{as.report()}} +} + +Template file for supporting new models: +\itemize{ +\item \code{\link[=report.default]{report.default()}} +} +} diff --git a/tests/testthat/test-report_assumptions.R b/tests/testthat/test-report_assumptions.R new file mode 100644 index 000000000..cf6e8b118 --- /dev/null +++ b/tests/testthat/test-report_assumptions.R @@ -0,0 +1,176 @@ +skip_if_not_installed("performance") + +# --------------------------------------------------------------------------- +# Happy paths +# --------------------------------------------------------------------------- + +test_that("report_assumptions - all assumptions satisfied", { + m <- lm(mpg ~ wt + hp, data = mtcars) + result <- report_assumptions(m) + + expect_s3_class(result, "report_text") + expect_match(as.character(result), "Model Assumptions", fixed = TRUE) + expect_match(as.character(result), "Influential observations", fixed = TRUE) + expect_match(as.character(result), "Homoskedasticity", fixed = TRUE) +}) + +test_that("report_assumptions - summary sentence structure", { + m <- lm(mpg ~ wt + hp, data = mtcars) + summ <- as.character(summary(report_assumptions(m))) + + expect_match(summ, "The model's assumptions were checked", fixed = TRUE) + expect_match(summ, "no influential observations were detected", fixed = TRUE) + expect_match(summ, "homoskedastic", fixed = TRUE) +}) + +test_that("report_assumptions - outliers detected", { + dat <- mtcars + dat[1L, "mpg"] <- 200 + dat[2L, "mpg"] <- 200 + m <- lm(mpg ~ wt + hp, data = dat) + summ <- as.character(summary(report_assumptions(m))) + + expect_match(summ, "The model's assumptions were checked", fixed = TRUE) + expect_false(grepl("no influential observations", summ, fixed = TRUE)) + expect_match(summ, "influential observation", ignore.case = TRUE) +}) + +test_that("report_assumptions - AI audience structure", { + m <- lm(mpg ~ wt + hp, data = mtcars) + result <- report_assumptions(m, audience = "ai") + + expect_s3_class(result, "report_ai") + expect_match(result, "## Assumptions", fixed = TRUE) + expect_match(result, "- Influential Observations:", fixed = TRUE) + expect_match(result, "- Homoskedasticity:", fixed = TRUE) +}) + +test_that("report_assumptions - AI no outliers", { + m <- lm(mpg ~ wt + hp, data = mtcars) + result <- report_assumptions(m, audience = "ai") + expect_match(result, "Influential Observations: none", fixed = TRUE) +}) + +test_that("report_assumptions - AI outliers detected", { + dat <- mtcars + dat[1L, "mpg"] <- 200 + dat[2L, "mpg"] <- 200 + m <- lm(mpg ~ wt + hp, data = dat) + result <- report_assumptions(m, audience = "ai") + # Should show a count, not "none" + expect_false(grepl("Influential Observations: none", result, fixed = TRUE)) + expect_match(result, "Influential Observations:", fixed = TRUE) +}) + +# --------------------------------------------------------------------------- +# Graceful fallbacks when individual checks fail +# --------------------------------------------------------------------------- + +test_that("report_assumptions - partial fallback when check_outliers fails", { + m <- lm(mpg ~ wt + hp, data = mtcars) + local_mocked_bindings( + check_outliers = function(...) stop("not supported for this class"), + .package = "performance" + ) + result <- expect_no_error(report_assumptions(m)) + + # Heteroskedasticity check still ran → summary should mention it + summ <- as.character(summary(result)) + expect_match(summ, "The model's assumptions were checked", fixed = TRUE) + expect_match(summ, "homoskedastic", fixed = TRUE) + # Outlier check absent from summary + expect_false(grepl("influential", summ, ignore.case = TRUE)) +}) + +test_that("report_assumptions - partial fallback when check_heteroskedasticity fails", { + m <- lm(mpg ~ wt + hp, data = mtcars) + local_mocked_bindings( + check_heteroskedasticity = function(...) stop("not supported"), + .package = "performance" + ) + result <- expect_no_error(report_assumptions(m)) + + summ <- as.character(summary(result)) + expect_match(summ, "The model's assumptions were checked", fixed = TRUE) + expect_match(summ, "influential observations", fixed = TRUE) + expect_false(grepl("homoskedast", summ, ignore.case = TRUE)) +}) + +test_that("report_assumptions - full fallback when both checks fail", { + m <- lm(mpg ~ wt + hp, data = mtcars) + local_mocked_bindings( + check_outliers = function(...) stop("not supported"), + check_heteroskedasticity = function(...) stop("not supported"), + .package = "performance" + ) + result <- expect_no_error(report_assumptions(m)) + + # Should return an object (not NULL, not an error) + expect_false(is.null(result)) + summ <- as.character(summary(result)) + expect_match(summ, "could not be performed", fixed = TRUE) +}) + +test_that("report_assumptions - AI partial fallback shows N/A", { + m <- lm(mpg ~ wt + hp, data = mtcars) + local_mocked_bindings( + check_outliers = function(...) stop("not supported"), + .package = "performance" + ) + result <- expect_no_error(report_assumptions(m, audience = "ai")) + + expect_s3_class(result, "report_ai") + expect_match(result, "Influential Observations: N/A", fixed = TRUE) + # Homoskedasticity still worked + expect_false(grepl("Homoskedasticity: N/A", result, fixed = TRUE)) +}) + +test_that("report_assumptions - AI full fallback shows N/A for both", { + m <- lm(mpg ~ wt + hp, data = mtcars) + local_mocked_bindings( + check_outliers = function(...) stop("not supported"), + check_heteroskedasticity = function(...) stop("not supported"), + .package = "performance" + ) + result <- expect_no_error(report_assumptions(m, audience = "ai")) + + expect_s3_class(result, "report_ai") + expect_match(result, "Influential Observations: N/A", fixed = TRUE) + expect_match(result, "Homoskedasticity: N/A", fixed = TRUE) +}) + +# --------------------------------------------------------------------------- +# Integration with report() +# --------------------------------------------------------------------------- + +test_that("report() - assumptions summary appears after model description", { + m <- lm(mpg ~ wt + hp, data = mtcars) + summ <- as.character(summary(report(m))) + + # Assumptions sentence present + expect_match(summ, "The model's assumptions were checked", fixed = TRUE) + # It should come before the R2 performance sentence + assum_pos <- regexpr("assumptions were checked", summ) + r2_pos <- regexpr("R2", summ) + expect_true(assum_pos < r2_pos) +}) + +test_that("report(audience='ai') - assumptions block between Variables and Parameters", { + m <- lm(mpg ~ wt + hp, data = mtcars) + result <- report(m, audience = "ai") + + expect_match(result, "## Assumptions", fixed = TRUE) + vars_pos <- regexpr("## Variables", result) + assum_pos <- regexpr("## Assumptions", result) + params_pos <- regexpr("## Parameters", result) + expect_true(vars_pos < assum_pos) + expect_true(assum_pos < params_pos) +}) + +test_that("report(audience='ai') - no blank line after ## Variables", { + m <- lm(mpg ~ wt + hp, data = mtcars) + result <- report(m, audience = "ai") + + # After "## Variables\n" the very next character should not be "\n" + expect_false(grepl("## Variables\n\n", result, fixed = TRUE)) +}) diff --git a/vignettes/report_ai.Rmd b/vignettes/report_ai.Rmd index 7d0bc108c..54233dc0a 100644 --- a/vignettes/report_ai.Rmd +++ b/vignettes/report_ai.Rmd @@ -33,7 +33,7 @@ The **report** package was originally designed in a pre-AI era with one overarch However, the analytical landscape has fundamentally changed. Today, researchers increasingly rely on AI agents and Large Language Models (LLMs) to help interpret results, summarize findings, and draft manuscripts, often by simply **pasting raw outputs directly into a chat window**. This shift prompted us to ask: should the scope of the *report* package be expanded to **support** this new workflow, rather than pretending it doesn't exist? And it wasn't even a matter of staying relevant, it was a matter of empowering our users to get the best possible results from their new AI assistants. -While our standard narrative outputs are excellent for humans, they are remarkably suboptimal for AI: verbose prose forces an LLM to waste valuable context tokens re-parsing implicit structures, wasting tokens and context memory. The answer to this challenge is `report_ai()`, a function that can be triggered via an argument from the main `report()` function. By stripping away the narrative bloat in favour of highly structured, token-efficient formats, `report_ai()` bridges the gap between R and the context window, giving your AI assistant exactly the information it needs in the most effective way possible. +While our standard narrative outputs are excellent for humans, they are remarkably suboptimal for AI: verbose prose forces an LLM to waste valuable context tokens re-parsing implicit structures, wasting tokens and context memory. The answer to this challenge is the `audience = "ai"` argument in `report()`. By stripping away the narrative bloat in favour of highly structured, token-efficient formats, `report(x, audience = "ai")` bridges the gap between R and the context window, giving your AI assistant exactly the information it needs in the most effective way possible. ## Basic Usage @@ -51,9 +51,8 @@ report(m) report(m, audience = "ai") ``` -The AI output is a single character vector of class `report_ai` that you can -pass directly to any LLM API, embed in a system prompt, or include in a -context window. +The AI output is a character string that you can pass directly to any LLM API, +embed in a system prompt, or include in a context window. ## Setting the Option Globally From 638369b801bbd37d266befae992d3bad39e5089f Mon Sep 17 00:00:00 2001 From: DominiqueMakowski Date: Mon, 25 May 2026 11:01:40 +0100 Subject: [PATCH 02/10] add collinearity --- R/report_assumptions.R | 116 ++++++++++++++++++++++- man/report_assumptions.Rd | 5 +- tests/testthat/test-report_assumptions.R | 72 ++++++++++++++ 3 files changed, 186 insertions(+), 7 deletions(-) diff --git a/R/report_assumptions.R b/R/report_assumptions.R index 5f051cade..23b97a108 100644 --- a/R/report_assumptions.R +++ b/R/report_assumptions.R @@ -1,8 +1,9 @@ #' Report model assumption checks #' #' Produces a formatted summary of model assumption checks, covering -#' **influential observations** (via [performance::check_outliers()]) and -#' **homoskedasticity** (via [performance::check_heteroskedasticity()]). +#' **influential observations** (via [performance::check_outliers()]), +#' **homoskedasticity** (via [performance::check_heteroskedasticity()]), and +#' **collinearity** (via [performance::check_collinearity()]). #' Calling [summary()] on the result returns a single integrated sentence. #' Set `audience = "ai"` for a compact, token-efficient version suitable for #' passing to an LLM. @@ -44,7 +45,6 @@ report_assumptions <- function( # TODO: add the following assumption checks: # - Linearity performance::check_predictions() / check_linearity() - # - Collinearity performance::check_collinearity() # - Normality performance::check_normality() # - Autocorrelation performance::check_autocorrelation() # - Overdispersion performance::check_overdispersion() @@ -66,6 +66,15 @@ report_assumptions <- function( homosked_ok <- p_val >= 0.05 } + # --- Collinearity ------------------------------------------------------- + collin <- tryCatch( + performance::check_collinearity(x), + error = function(e) NULL + ) + if (!is.null(collin)) { + collin_info <- .collinearity_info(collin) + } + # --- AI output ---------------------------------------------------------- if (audience == "ai") { if (!is.null(outliers)) { @@ -119,10 +128,30 @@ report_assumptions <- function( heterosk_ai <- "N/A" } + if (!is.null(collin)) { + if (collin_info$severity == "none") { + collin_ai <- "OK (all VIF < 5)" + } else { + level_str <- if (collin_info$severity == "high") "HIGH" else "MODERATE" + flagged_str <- paste( + paste0( + collin_info$flagged_terms, + ": VIF = ", + insight::format_value(collin_info$flagged_vif) + ), + collapse = ", " + ) + collin_ai <- paste0(level_str, " (", flagged_str, ")") + } + } else { + collin_ai <- "N/A" + } + lines <- c( "## Assumptions", paste0("- Influential Observations: ", outlier_ai), - paste0("- Homoskedasticity: ", heterosk_ai) + paste0("- Homoskedasticity: ", heterosk_ai), + paste0("- Collinearity: ", collin_ai) ) res <- paste(lines, collapse = "\n") class(res) <- c("report_ai", "character") @@ -172,6 +201,38 @@ report_assumptions <- function( phrases <- c(phrases, heterosk_phrase) } + if (!is.null(collin)) { + if (collin_info$severity == "none") { + collin_bullet <- "No collinearity detected (all VIF < 5)." + collin_phrase <- "no collinearity was detected" + } else { + level_str <- if (collin_info$severity == "high") "High" else "Moderate" + flagged_str <- paste( + paste0( + collin_info$flagged_terms, + " (VIF = ", + insight::format_value(collin_info$flagged_vif), + ")" + ), + collapse = ", " + ) + collin_bullet <- paste0( + level_str, + " collinearity detected: ", + flagged_str, + "." + ) + collin_phrase <- paste0( + tolower(level_str), + " collinearity was detected (", + paste(collin_info$flagged_terms, collapse = ", "), + ")" + ) + } + lines_bullets <- c(lines_bullets, paste0("- Collinearity: ", collin_bullet)) + phrases <- c(phrases, collin_phrase) + } + if (length(phrases) == 0L) { lines_bullets <- c( lines_bullets, @@ -181,7 +242,7 @@ report_assumptions <- function( } else { text_summary <- paste0( "The model's assumptions were checked: ", - paste(phrases, collapse = " and "), + datawizard::text_concatenate(phrases), "." ) } @@ -216,3 +277,48 @@ report_assumptions <- function( ")" ) } + +#' @keywords internal +.collinearity_info <- function(collin) { + vif_col <- if ("VIF" %in% names(collin)) { + "VIF" + } else if ("GVIF" %in% names(collin)) { + "GVIF" + } else { + NULL + } + + if (is.null(vif_col) || nrow(collin) == 0L) { + return(list( + severity = "none", + flagged_terms = character(0L), + flagged_vif = numeric(0L) + )) + } + + vif_vals <- collin[[vif_col]] + terms <- collin[["Term"]] + + high_idx <- which(vif_vals >= 10) + mod_idx <- which(vif_vals >= 5 & vif_vals < 10) + + if (length(high_idx) > 0L) { + list( + severity = "high", + flagged_terms = terms[high_idx], + flagged_vif = vif_vals[high_idx] + ) + } else if (length(mod_idx) > 0L) { + list( + severity = "moderate", + flagged_terms = terms[mod_idx], + flagged_vif = vif_vals[mod_idx] + ) + } else { + list( + severity = "none", + flagged_terms = character(0L), + flagged_vif = numeric(0L) + ) + } +} diff --git a/man/report_assumptions.Rd b/man/report_assumptions.Rd index 0ceb8e2d4..9ee96324a 100644 --- a/man/report_assumptions.Rd +++ b/man/report_assumptions.Rd @@ -21,8 +21,9 @@ one-sentence version integrating all checks (for \code{audience = "humans"}). } \description{ Produces a formatted summary of model assumption checks, covering -\strong{influential observations} (via \code{\link[performance:check_outliers]{performance::check_outliers()}}) and -\strong{homoskedasticity} (via \code{\link[performance:check_heteroscedasticity]{performance::check_heteroskedasticity()}}). +\strong{influential observations} (via \code{\link[performance:check_outliers]{performance::check_outliers()}}), +\strong{homoskedasticity} (via \code{\link[performance:check_heteroscedasticity]{performance::check_heteroskedasticity()}}), and +\strong{collinearity} (via \code{\link[performance:check_collinearity]{performance::check_collinearity()}}). Calling \code{\link[=summary]{summary()}} on the result returns a single integrated sentence. Set \code{audience = "ai"} for a compact, token-efficient version suitable for passing to an LLM. diff --git a/tests/testthat/test-report_assumptions.R b/tests/testthat/test-report_assumptions.R index cf6e8b118..4c86c29eb 100644 --- a/tests/testthat/test-report_assumptions.R +++ b/tests/testthat/test-report_assumptions.R @@ -139,6 +139,78 @@ test_that("report_assumptions - AI full fallback shows N/A for both", { expect_match(result, "Homoskedasticity: N/A", fixed = TRUE) }) +# --------------------------------------------------------------------------- +# Collinearity +# --------------------------------------------------------------------------- + +test_that("report_assumptions - no collinearity (humans)", { + m <- lm(mpg ~ wt + hp, data = mtcars) + result <- report_assumptions(m) + expect_match(as.character(result), "Collinearity", fixed = TRUE) + expect_match(as.character(result), "No collinearity detected", fixed = TRUE) + summ <- as.character(summary(result)) + expect_match(summ, "no collinearity was detected", fixed = TRUE) +}) + +test_that("report_assumptions - high collinearity detected (humans)", { + set.seed(42) + dat <- mtcars + dat$wt_noise <- mtcars$wt + rnorm(32L, 0, 0.01) + m <- lm(mpg ~ wt + wt_noise + hp, data = dat) + result <- report_assumptions(m) + full <- as.character(result) + # Either high or moderate collinearity should be flagged + expect_true( + grepl("High collinearity", full, fixed = TRUE) || + grepl("Moderate collinearity", full, fixed = TRUE) + ) + summ <- as.character(summary(result)) + expect_true( + grepl("high collinearity", summ, fixed = TRUE) || + grepl("moderate collinearity", summ, fixed = TRUE) + ) +}) + +test_that("report_assumptions - no collinearity (AI)", { + m <- lm(mpg ~ wt + hp, data = mtcars) + result <- report_assumptions(m, audience = "ai") + expect_match(result, "- Collinearity:", fixed = TRUE) + expect_match(result, "OK (all VIF < 5)", fixed = TRUE) +}) + +test_that("report_assumptions - high collinearity (AI)", { + set.seed(42) + dat <- mtcars + dat$wt_noise <- mtcars$wt + rnorm(32L, 0, 0.01) + m <- lm(mpg ~ wt + wt_noise + hp, data = dat) + result <- report_assumptions(m, audience = "ai") + expect_match(result, "- Collinearity:", fixed = TRUE) + expect_false(grepl("OK (all VIF < 5)", result, fixed = TRUE)) +}) + +test_that("report_assumptions - collinearity fallback when check fails", { + m <- lm(mpg ~ wt + hp, data = mtcars) + local_mocked_bindings( + check_collinearity = function(...) stop("not supported"), + .package = "performance" + ) + result <- expect_no_error(report_assumptions(m)) + # Other checks still ran + summ <- as.character(summary(result)) + expect_match(summ, "The model's assumptions were checked", fixed = TRUE) + expect_false(grepl("collinearity", summ, fixed = TRUE)) +}) + +test_that("report_assumptions - collinearity AI fallback shows N/A", { + m <- lm(mpg ~ wt + hp, data = mtcars) + local_mocked_bindings( + check_collinearity = function(...) stop("not supported"), + .package = "performance" + ) + result <- expect_no_error(report_assumptions(m, audience = "ai")) + expect_match(result, "Collinearity: N/A", fixed = TRUE) +}) + # --------------------------------------------------------------------------- # Integration with report() # --------------------------------------------------------------------------- From f0c4158a6a672831f26232d9ca4adacc01c1d7be Mon Sep 17 00:00:00 2001 From: DominiqueMakowski Date: Mon, 25 May 2026 11:06:16 +0100 Subject: [PATCH 03/10] added argument to skip the assumptions report --- R/report.lm.R | 15 ++++++++++++--- R/report_assumptions.R | 4 ++++ 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/R/report.lm.R b/R/report.lm.R index b34e64c95..f5e171907 100644 --- a/R/report.lm.R +++ b/R/report.lm.R @@ -8,6 +8,9 @@ #' @param include_diagnostic If `FALSE`, won't include diagnostic related #' indices for Bayesian models (ESS, Rhat). #' @param include_intercept If `FALSE`, won't include the intercept. +#' @param assumptions If `FALSE`, assumption checks (see +#' [report_assumptions()]) are skipped and not included in the report. +#' Defaults to `TRUE`. #' @param effectsize_method See documentation for #' [effectsize::effectsize()]. #' @param parameters Provide the output of `report_parameters()` to avoid @@ -55,6 +58,7 @@ report.lm <- function( x, include_effectsize = TRUE, effectsize_method = "refit", + assumptions = TRUE, ... ) { result_table <- report_table( @@ -63,7 +67,12 @@ report.lm <- function( effectsize_method = effectsize_method, ... ) - result_text <- report_text(x, table = result_table, ...) + result_text <- report_text( + x, + table = result_table, + assumptions = assumptions, + ... + ) as.report(result_text, table = result_table, ...) } @@ -729,7 +738,7 @@ report_info.lm <- function( #' @rdname report.lm #' @export -report_text.lm <- function(x, table = NULL, ...) { +report_text.lm <- function(x, table = NULL, assumptions = TRUE, ...) { params <- report_parameters(x, table = table, include_intercept = FALSE, ...) report_table_data <- attributes(params)$table @@ -755,7 +764,7 @@ report_text.lm <- function(x, table = NULL, ...) { # --- Assumptions (requires performance) --------------------------------- assumptions_summary <- "" - if (requireNamespace("performance", quietly = TRUE)) { + if (isTRUE(assumptions) && requireNamespace("performance", quietly = TRUE)) { assumptions_obj <- tryCatch( report_assumptions(x), error = function(e) NULL diff --git a/R/report_assumptions.R b/R/report_assumptions.R index 23b97a108..cd17bdfb4 100644 --- a/R/report_assumptions.R +++ b/R/report_assumptions.R @@ -8,6 +8,10 @@ #' Set `audience = "ai"` for a compact, token-efficient version suitable for #' passing to an LLM. #' +#' This function is called automatically by [report()] for supported model +#' classes (e.g., `lm`, `glm`). Pass `assumptions = FALSE` to [report()] to +#' suppress assumption checks in the final report. +#' #' @param x A statistical model object (e.g., `lm`, `glm`). #' @param audience The intended audience. `"humans"` (default) produces a #' readable bulleted report; `"ai"` produces a compact structured version. From cd2530e93789ae0802e9fb7f1aa23a5d3966055c Mon Sep 17 00:00:00 2001 From: DominiqueMakowski Date: Mon, 25 May 2026 11:13:05 +0100 Subject: [PATCH 04/10] fix --- man/report.lm.Rd | 14 ++++++++++++-- man/report_assumptions.Rd | 5 +++++ pkgdown/_pkgdown.yml | 7 +------ 3 files changed, 18 insertions(+), 8 deletions(-) diff --git a/man/report.lm.Rd b/man/report.lm.Rd index 20a816c99..37a1e4d7b 100644 --- a/man/report.lm.Rd +++ b/man/report.lm.Rd @@ -14,7 +14,13 @@ \alias{report_random.merMod} \title{Reporting (General) Linear Models} \usage{ -\method{report}{lm}(x, include_effectsize = TRUE, effectsize_method = "refit", ...) +\method{report}{lm}( + x, + include_effectsize = TRUE, + effectsize_method = "refit", + assumptions = TRUE, + ... +) \method{report_effectsize}{lm}(x, effectsize_method = "refit", ...) @@ -50,7 +56,7 @@ ... ) -\method{report_text}{lm}(x, table = NULL, ...) +\method{report_text}{lm}(x, table = NULL, assumptions = TRUE, ...) \method{report_random}{merMod}(x, ...) } @@ -63,6 +69,10 @@ indices (standardized coefficients, etc.).} \item{effectsize_method}{See documentation for \code{\link[effectsize:effectsize]{effectsize::effectsize()}}.} +\item{assumptions}{If \code{FALSE}, assumption checks (see +\code{\link[=report_assumptions]{report_assumptions()}}) are skipped and not included in the report. +Defaults to \code{TRUE}.} + \item{...}{Arguments passed to or from other methods.} \item{table}{Provide the output of \code{report_table()} to avoid its re-computation.} diff --git a/man/report_assumptions.Rd b/man/report_assumptions.Rd index 9ee96324a..570523c65 100644 --- a/man/report_assumptions.Rd +++ b/man/report_assumptions.Rd @@ -28,6 +28,11 @@ Calling \code{\link[=summary]{summary()}} on the result returns a single integra Set \code{audience = "ai"} for a compact, token-efficient version suitable for passing to an LLM. } +\details{ +This function is called automatically by \code{\link[=report]{report()}} for supported model +classes (e.g., \code{lm}, \code{glm}). Pass \code{assumptions = FALSE} to \code{\link[=report]{report()}} to +suppress assumption checks in the final report. +} \examples{ \dontshow{if (requireNamespace("performance", quietly = TRUE)) withAutoprint(\{ # examplesIf} \donttest{ diff --git a/pkgdown/_pkgdown.yml b/pkgdown/_pkgdown.yml index a5094afa9..8861f6040 100644 --- a/pkgdown/_pkgdown.yml +++ b/pkgdown/_pkgdown.yml @@ -62,6 +62,7 @@ reference: - report_s - report_sample - report_statistics + - report_assumptions - title: Formatting desc: | @@ -77,12 +78,6 @@ reference: - report_table - report_text - - title: AI-Optimized Reports - desc: | - Functions for generating AI-optimized reports - contents: - - report_ai - - title: Report Statistical Objects desc: | Helper functions for reporting of statistical objects From 130c38470688627faf759e01118f1d7527ec61ad Mon Sep 17 00:00:00 2001 From: Dominique Makowski Date: Mon, 25 May 2026 11:13:57 +0100 Subject: [PATCH 05/10] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- NEWS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NEWS.md b/NEWS.md index dbbd95cf3..c0a85e37a 100644 --- a/NEWS.md +++ b/NEWS.md @@ -2,7 +2,7 @@ New features -* `report_assumptions()`: new function that reports model assumption checks for a model, covering influential observations (via `performance::check_outliers()`) and homoskedasticity (via `performance::check_heteroskedasticity()`). Calling `summary()` on the result returns a compact one-sentence version integrating all checks. Supports `audience = "ai"` for a compact token-efficient structured output. +* `report_assumptions()`: new function that reports model assumption checks for a model, covering influential observations (via `performance::check_outliers()`), collinearity (via `performance::check_collinearity()`), and homoskedasticity (via `performance::check_heteroskedasticity()`). Calling `summary()` on the result returns a compact one-sentence version integrating all checks. Supports `audience = "ai"` for a compact token-efficient structured output. * `report()` / `report_text()` for linear models: assumption checks (from `report_assumptions()`) are now automatically included at the end of the report. For human audience, the compact one-sentence summary is appended; for AI audience, a structured `## Assumptions` section is added to the output. From cc024f8fbbf7d4a32ec40f183d9cc0caeb9fe866 Mon Sep 17 00:00:00 2001 From: Dominique Makowski Date: Mon, 25 May 2026 11:14:37 +0100 Subject: [PATCH 06/10] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- NEWS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NEWS.md b/NEWS.md index c0a85e37a..f0d399167 100644 --- a/NEWS.md +++ b/NEWS.md @@ -4,7 +4,7 @@ New features * `report_assumptions()`: new function that reports model assumption checks for a model, covering influential observations (via `performance::check_outliers()`), collinearity (via `performance::check_collinearity()`), and homoskedasticity (via `performance::check_heteroskedasticity()`). Calling `summary()` on the result returns a compact one-sentence version integrating all checks. Supports `audience = "ai"` for a compact token-efficient structured output. -* `report()` / `report_text()` for linear models: assumption checks (from `report_assumptions()`) are now automatically included at the end of the report. For human audience, the compact one-sentence summary is appended; for AI audience, a structured `## Assumptions` section is added to the output. +* `report()` / `report_text()` for linear models: assumption checks (from `report_assumptions()`) are now automatically included right after the model description (before the performance sentence). For human audience, the compact one-sentence summary is inserted there; for AI audience, a structured `## Assumptions` section is added there in the output. Bug fixes From a89739edcc6de73967f5cd36996ff311a48386d1 Mon Sep 17 00:00:00 2001 From: DominiqueMakowski Date: Mon, 25 May 2026 11:17:58 +0100 Subject: [PATCH 07/10] bump version (0.6.5) --- DESCRIPTION | 2 +- R/report_assumptions.R | 6 ++++-- man/report_assumptions.Rd | 10 ++++++---- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index c978fd675..6bb88b232 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,7 +1,7 @@ Package: report Type: Package Title: Automated Reporting of Results and Statistical Models -Version: 0.6.4 +Version: 0.6.5 Authors@R: c(person(given = "Dominique", family = "Makowski", diff --git a/R/report_assumptions.R b/R/report_assumptions.R index cd17bdfb4..0e8e90920 100644 --- a/R/report_assumptions.R +++ b/R/report_assumptions.R @@ -16,6 +16,8 @@ #' @param audience The intended audience. `"humans"` (default) produces a #' readable bulleted report; `"ai"` produces a compact structured version. #' The default can be changed globally with `options(report_audience = "ai")`. +#' Can be passed positionally as the second argument: +#' `report_assumptions(model, "ai")`. #' @param ... Additional arguments passed to [performance::check_outliers()]. #' #' @return An object of class [report_text()]. [summary()] returns a compact @@ -41,8 +43,8 @@ #' @export report_assumptions <- function( x, - ..., - audience = getOption("report_audience", "humans") + audience = getOption("report_audience", "humans"), + ... ) { insight::check_if_installed("performance") audience <- match.arg(audience, c("humans", "ai")) diff --git a/man/report_assumptions.Rd b/man/report_assumptions.Rd index 570523c65..b58d9d3a7 100644 --- a/man/report_assumptions.Rd +++ b/man/report_assumptions.Rd @@ -4,16 +4,18 @@ \alias{report_assumptions} \title{Report model assumption checks} \usage{ -report_assumptions(x, ..., audience = getOption("report_audience", "humans")) +report_assumptions(x, audience = getOption("report_audience", "humans"), ...) } \arguments{ \item{x}{A statistical model object (e.g., \code{lm}, \code{glm}).} -\item{...}{Additional arguments passed to \code{\link[performance:check_outliers]{performance::check_outliers()}}.} - \item{audience}{The intended audience. \code{"humans"} (default) produces a readable bulleted report; \code{"ai"} produces a compact structured version. -The default can be changed globally with \code{options(report_audience = "ai")}.} +The default can be changed globally with \code{options(report_audience = "ai")}. +Can be passed positionally as the second argument: +\code{report_assumptions(model, "ai")}.} + +\item{...}{Additional arguments passed to \code{\link[performance:check_outliers]{performance::check_outliers()}}.} } \value{ An object of class \code{\link[=report_text]{report_text()}}. \code{\link[=summary]{summary()}} returns a compact From 03289bd2809eafa91aea1d53ddb880e311038350 Mon Sep 17 00:00:00 2001 From: DominiqueMakowski Date: Mon, 25 May 2026 14:24:44 +0100 Subject: [PATCH 08/10] fixes --- R/report_assumptions.R | 4 +-- pkgdown/_pkgdown.yml | 1 + tests/testthat/_snaps/windows/report.ivreg.md | 10 +++--- tests/testthat/_snaps/windows/report.lm.md | 12 ++++--- tests/testthat/_snaps/windows/report.lmer.md | 8 +++-- .../testthat/_snaps/windows/report.stanreg.md | 9 ++++-- .../testthat/_snaps/windows/report.survreg.md | 7 ++-- tests/testthat/test-report_assumptions.R | 32 +++++++++++++------ 8 files changed, 56 insertions(+), 27 deletions(-) diff --git a/R/report_assumptions.R b/R/report_assumptions.R index 0e8e90920..25abc1f87 100644 --- a/R/report_assumptions.R +++ b/R/report_assumptions.R @@ -63,7 +63,7 @@ report_assumptions <- function( # --- Homoskedasticity --------------------------------------------------- heterosk <- tryCatch( - performance::check_heteroskedasticity(x), + suppressMessages(performance::check_heteroskedasticity(x)), error = function(e) NULL ) if (!is.null(heterosk)) { @@ -74,7 +74,7 @@ report_assumptions <- function( # --- Collinearity ------------------------------------------------------- collin <- tryCatch( - performance::check_collinearity(x), + suppressMessages(performance::check_collinearity(x)), error = function(e) NULL ) if (!is.null(collin)) { diff --git a/pkgdown/_pkgdown.yml b/pkgdown/_pkgdown.yml index 8861f6040..66b1cad39 100644 --- a/pkgdown/_pkgdown.yml +++ b/pkgdown/_pkgdown.yml @@ -94,6 +94,7 @@ reference: - report.estimate_contrasts - report.compare.loo - report.BFBayesFactor + - report_text.check_outliers - title: Report Non-Statistical Objects desc: | diff --git a/tests/testthat/_snaps/windows/report.ivreg.md b/tests/testthat/_snaps/windows/report.ivreg.md index 96f08a42d..c9cedf41c 100644 --- a/tests/testthat/_snaps/windows/report.ivreg.md +++ b/tests/testthat/_snaps/windows/report.ivreg.md @@ -9,10 +9,12 @@ See help("standardize") for how such terms are standardized. Output We fitted a linear model to predict packs with rprice, rincome and salestax - (formula: log(packs) ~ log(rprice) + log(rincome)). The model's explanatory - power is substantial (R2 = 0.42, adj. R2 = 0.39). The model's intercept, - corresponding to rprice = 0 and rincome = 0, is at 9.43 (95% CI [6.69, 12.17], - t(45) = 6.94, p < .001). Within this model: + (formula: log(packs) ~ log(rprice) + log(rincome)). The model's assumptions + were checked: no influential observations were detected, the error variance + appeared homoskedastic (p = 0.613) and no collinearity was detected. The + model's explanatory power is substantial (R2 = 0.42, adj. R2 = 0.39). The + model's intercept, corresponding to rprice = 0 and rincome = 0, is at 9.43 (95% + CI [6.69, 12.17], t(45) = 6.94, p < .001). Within this model: - The effect of rprice [log] is statistically significant and negative (beta = -1.14, 95% CI [-1.87, -0.42], t(45) = -3.18, p = 0.003; Std. beta = -0.53, 95% diff --git a/tests/testthat/_snaps/windows/report.lm.md b/tests/testthat/_snaps/windows/report.lm.md index 908904575..22bad2c5f 100644 --- a/tests/testthat/_snaps/windows/report.lm.md +++ b/tests/testthat/_snaps/windows/report.lm.md @@ -4,7 +4,8 @@ report(glm(vs ~ disp, data = mtcars, family = binomial(link = "probit"))) Output We fitted a probit model (estimated using ML) to predict vs with disp (formula: - vs ~ disp). The model's explanatory power is substantial (Nagelkerke's R2 = + vs ~ disp). The model's assumptions were checked: no influential observations + were detected. The model's explanatory power is substantial (Nagelkerke's R2 = 0.66). The model's intercept, corresponding to disp = 0, is at 2.51 (95% CI [1.13, 4.28], p = 0.001). Within this model: @@ -21,7 +22,8 @@ report(glm(vs ~ mpg, data = mtcars, family = "poisson")) Output We fitted a poisson model (estimated using ML) to predict vs with mpg (formula: - vs ~ mpg). The model's explanatory power is substantial (Nagelkerke's R2 = + vs ~ mpg). The model's assumptions were checked: no influential observations + were detected. The model's explanatory power is substantial (Nagelkerke's R2 = 0.39). The model's intercept, corresponding to mpg = 0, is at -3.27 (95% CI [-5.46, -1.36], p = 0.002). Within this model: @@ -39,8 +41,10 @@ Output We fitted a constant (intercept-only) linear model (estimated using OLS) to predict d_wide$group0 - d_wide$group1 (formula: d_wide$group0 - d_wide$group1 ~ - 1). The model's intercept is at -1.58 (95% CI [-2.46, -0.70], t(9) = -4.06, p = - 0.003). + 1). The model's assumptions were checked: 1 influential observation (10.00%) + were detected (Cook's distance) and the error variance appeared homoskedastic + (p > .999). The model's intercept is at -1.58 (95% CI [-2.46, -0.70], t(9) = + -4.06, p = 0.003). Standardized parameters were obtained by fitting the model on a standardized version of the dataset. 95% Confidence Intervals (CIs) and p-values were diff --git a/tests/testthat/_snaps/windows/report.lmer.md b/tests/testthat/_snaps/windows/report.lmer.md index e9561fa25..8b0e226ce 100644 --- a/tests/testthat/_snaps/windows/report.lmer.md +++ b/tests/testthat/_snaps/windows/report.lmer.md @@ -5,7 +5,9 @@ Output We fitted a linear mixed model (estimated using REML and nloptwrap optimizer) to predict Reaction with Days (formula: Reaction ~ Days). The model included - Days as random effects (formula: ~1 + Days | Subject). The model's total + Days as random effects (formula: ~1 + Days | Subject). The model's assumptions + were checked: 4 influential observations (2.22%) were detected (Cook's + distance) and heteroskedasticity was detected (p < .001). The model's total explanatory power is substantial (conditional R2 = 0.80) and the part related to the fixed effects alone (marginal R2) is of 0.28. The model's intercept, corresponding to Days = 0, is at 251.41 (95% CI [237.94, 264.87], t(174) = @@ -34,7 +36,9 @@ We fitted a linear mixed model (estimated using REML and nloptwrap optimizer) to predict Reaction with Days (formula: Reaction ~ Days). The model included mysubgrp as random effects (formula: list(~1 | mysubgrp:mygrp, ~1 | mygrp, ~1 | - Subject)). The model's explanatory power related to the fixed effects alone + Subject)). The model's assumptions were checked: 1 influential observation + (0.56%) were detected (Cook's distance) and heteroskedasticity was detected (p + < .001). The model's explanatory power related to the fixed effects alone (marginal R2) is 0.49. The model's intercept, corresponding to Days = 0, is at 252.09 (95% CI [232.50, 271.69], t(174) = 25.39, p < .001). Within this model: diff --git a/tests/testthat/_snaps/windows/report.stanreg.md b/tests/testthat/_snaps/windows/report.stanreg.md index 42313b27c..a68a8423e 100644 --- a/tests/testthat/_snaps/windows/report.stanreg.md +++ b/tests/testthat/_snaps/windows/report.stanreg.md @@ -7,18 +7,21 @@ of 300 iterations and a warmup of 150) to predict mpg with qsec and wt (formula: mpg ~ qsec + wt). Priors over parameters were all set as normal (mean = 0.00, SD = 8.43; mean = 0.00, SD = 15.40) distributions. The model's - explanatory power is substantial (R2 = 0.81, 95% CI [0.70, 0.90], adj. R2 = + assumptions were checked: no influential observations were detected, the error + variance appeared homoskedastic (p = 0.423) and no collinearity was detected. + The model's explanatory power is substantial (R2 = 0.81, 95% CI [0.70, 0.90], + adj. R2 = 0.80). The model's intercept, corresponding to qsec = 0 and wt = 0, is at 19.71 (95% CI [9.04, 30.18]). Within this model: - The effect of qsec (Median = 0.92, 95% CI [0.40, 1.47]) has a 99.83% probability of being positive (> 0), 99.00% of being significant (> 0.30), and 0.33% of being large (> 1.81). The estimation successfully converged (Rhat = - 0.999) but the indices are unreliable (ESS = 451) + 0.999) - The effect of wt (Median = -5.06, 95% CI [-6.02, -4.18]) has a 100.00% probability of being negative (< 0), 100.00% of being significant (< -0.30), and 100.00% of being large (< -1.81). However, the estimation might not have - successfuly converged (Rhat = 1.013) and the indices are unreliable (ESS = 478) + successfuly converged (Rhat = 1.013) Following the Sequential Effect eXistence and sIgnificance Testing (SEXIT) framework, we report the median of the posterior distribution and its 95% CI diff --git a/tests/testthat/_snaps/windows/report.survreg.md b/tests/testthat/_snaps/windows/report.survreg.md index 0707d6879..d18e8126e 100644 --- a/tests/testthat/_snaps/windows/report.survreg.md +++ b/tests/testthat/_snaps/windows/report.survreg.md @@ -17,9 +17,10 @@ `performance_pcp()` only works for models with binary response values. We fitted a logistic model to predict survival::Surv(futime, fustat) with ecog.ps and rx (formula: survival::Surv(futime, fustat) ~ ecog.ps + rx). The - model's explanatory power is weak (Nagelkerke's R2 = 0.07). The model's - intercept, corresponding to ecog.ps = 0 and rx = 0, is at 667.43 (95% CI - [-415.59, 1750.45], p = 0.227). Within this model: + model's assumptions were checked: no collinearity was detected. The model's + explanatory power is weak (Nagelkerke's R2 = 0.07). The model's intercept, + corresponding to ecog.ps = 0 and rx = 0, is at 667.43 (95% CI [-415.59, + 1750.45], p = 0.227). Within this model: - The effect of ecog ps is statistically non-significant and negative (beta = -210.59, 95% CI [-726.18, 305.01], p = 0.423; Std. beta = -107.06, 95% CI diff --git a/tests/testthat/test-report_assumptions.R b/tests/testthat/test-report_assumptions.R index 4c86c29eb..dde208632 100644 --- a/tests/testthat/test-report_assumptions.R +++ b/tests/testthat/test-report_assumptions.R @@ -24,12 +24,18 @@ test_that("report_assumptions - summary sentence structure", { }) test_that("report_assumptions - outliers detected", { - dat <- mtcars - dat[1L, "mpg"] <- 200 - dat[2L, "mpg"] <- 200 - m <- lm(mpg ~ wt + hp, data = dat) + m <- lm(mpg ~ wt + hp, data = mtcars) + mock_outliers <- structure( + c(TRUE, TRUE, rep(FALSE, 30L)), + method = "cook", + threshold = list(cook = 0.125), + class = c("check_outliers", "logical") + ) + local_mocked_bindings( + check_outliers = function(...) mock_outliers, + .package = "performance" + ) summ <- as.character(summary(report_assumptions(m))) - expect_match(summ, "The model's assumptions were checked", fixed = TRUE) expect_false(grepl("no influential observations", summ, fixed = TRUE)) expect_match(summ, "influential observation", ignore.case = TRUE) @@ -52,10 +58,17 @@ test_that("report_assumptions - AI no outliers", { }) test_that("report_assumptions - AI outliers detected", { - dat <- mtcars - dat[1L, "mpg"] <- 200 - dat[2L, "mpg"] <- 200 - m <- lm(mpg ~ wt + hp, data = dat) + m <- lm(mpg ~ wt + hp, data = mtcars) + mock_outliers <- structure( + c(TRUE, TRUE, rep(FALSE, 30L)), + method = "cook", + threshold = list(cook = 0.125), + class = c("check_outliers", "logical") + ) + local_mocked_bindings( + check_outliers = function(...) mock_outliers, + .package = "performance" + ) result <- report_assumptions(m, audience = "ai") # Should show a count, not "none" expect_false(grepl("Influential Observations: none", result, fixed = TRUE)) @@ -101,6 +114,7 @@ test_that("report_assumptions - full fallback when both checks fail", { local_mocked_bindings( check_outliers = function(...) stop("not supported"), check_heteroskedasticity = function(...) stop("not supported"), + check_collinearity = function(...) stop("not supported"), .package = "performance" ) result <- expect_no_error(report_assumptions(m)) From ce079285335fa6efa8112dcfcfe2a925af221200 Mon Sep 17 00:00:00 2001 From: DominiqueMakowski Date: Tue, 26 May 2026 08:59:09 +0100 Subject: [PATCH 09/10] not run collinearity when not enough predictors --- R/report_assumptions.R | 31 +++++++++++++++++++++--- tests/testthat/test-report_assumptions.R | 26 ++++++++++++++++++++ 2 files changed, 53 insertions(+), 4 deletions(-) diff --git a/R/report_assumptions.R b/R/report_assumptions.R index 25abc1f87..af2a9e82d 100644 --- a/R/report_assumptions.R +++ b/R/report_assumptions.R @@ -73,10 +73,13 @@ report_assumptions <- function( } # --- Collinearity ------------------------------------------------------- - collin <- tryCatch( - suppressMessages(performance::check_collinearity(x)), - error = function(e) NULL - ) + collin <- NULL + if (.can_check_collinearity(x)) { + collin <- tryCatch( + suppressMessages(performance::check_collinearity(x)), + error = function(e) NULL + ) + } if (!is.null(collin)) { collin_info <- .collinearity_info(collin) } @@ -284,6 +287,26 @@ report_assumptions <- function( ) } +#' @keywords internal +.can_check_collinearity <- function(x) { + predictors <- tryCatch( + insight::find_predictors(x, effects = "fixed", flatten = TRUE), + error = function(e) NULL + ) + + if (is.null(predictors)) { + return(FALSE) + } + + if (is.list(predictors)) { + predictors <- unlist(predictors, use.names = FALSE) + } + + predictors <- unique(stats::na.omit(as.character(predictors))) + + length(predictors) > 1L +} + #' @keywords internal .collinearity_info <- function(collin) { vif_col <- if ("VIF" %in% names(collin)) { diff --git a/tests/testthat/test-report_assumptions.R b/tests/testthat/test-report_assumptions.R index dde208632..cce2a63d2 100644 --- a/tests/testthat/test-report_assumptions.R +++ b/tests/testthat/test-report_assumptions.R @@ -225,6 +225,32 @@ test_that("report_assumptions - collinearity AI fallback shows N/A", { expect_match(result, "Collinearity: N/A", fixed = TRUE) }) +test_that("report_assumptions - collinearity check skipped for one predictor", { + m <- lm(mpg ~ wt, data = mtcars) + local_mocked_bindings( + check_collinearity = function(...) stop("should not be called"), + .package = "performance" + ) + + result <- expect_no_error(report_assumptions(m)) + full <- as.character(result) + summ <- as.character(summary(result)) + + expect_false(grepl("Collinearity", full, fixed = TRUE)) + expect_false(grepl("collinearity", summ, fixed = TRUE)) +}) + +test_that("report() - collinearity check skipped for one predictor", { + m <- lm(mpg ~ wt, data = mtcars) + local_mocked_bindings( + check_collinearity = function(...) stop("should not be called"), + .package = "performance" + ) + + result <- expect_no_error(report(m)) + expect_false(is.null(result)) +}) + # --------------------------------------------------------------------------- # Integration with report() # --------------------------------------------------------------------------- From 2686aef3811eb90ff491ed9f3a3280c740e7b811 Mon Sep 17 00:00:00 2001 From: DominiqueMakowski Date: Tue, 26 May 2026 09:41:44 +0100 Subject: [PATCH 10/10] minor consistency --- R/report_assumptions.R | 2 +- tests/testthat/test-report_assumptions.R | 10 +++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/R/report_assumptions.R b/R/report_assumptions.R index af2a9e82d..1d896f63f 100644 --- a/R/report_assumptions.R +++ b/R/report_assumptions.R @@ -90,7 +90,7 @@ report_assumptions <- function( n_outliers <- sum(outliers) n_total <- length(outliers) if (n_outliers == 0L) { - outlier_ai <- "none" + outlier_ai <- "OK (none)" } else { pct <- insight::format_value(n_outliers / n_total, as_percent = TRUE) methods <- attr(outliers, "method") diff --git a/tests/testthat/test-report_assumptions.R b/tests/testthat/test-report_assumptions.R index cce2a63d2..612dab157 100644 --- a/tests/testthat/test-report_assumptions.R +++ b/tests/testthat/test-report_assumptions.R @@ -54,7 +54,7 @@ test_that("report_assumptions - AI audience structure", { test_that("report_assumptions - AI no outliers", { m <- lm(mpg ~ wt + hp, data = mtcars) result <- report_assumptions(m, audience = "ai") - expect_match(result, "Influential Observations: none", fixed = TRUE) + expect_match(result, "Influential Observations: OK (none)", fixed = TRUE) }) test_that("report_assumptions - AI outliers detected", { @@ -70,8 +70,12 @@ test_that("report_assumptions - AI outliers detected", { .package = "performance" ) result <- report_assumptions(m, audience = "ai") - # Should show a count, not "none" - expect_false(grepl("Influential Observations: none", result, fixed = TRUE)) + # Should show a count, not the no-outlier "OK (none)" text + expect_false(grepl( + "Influential Observations: OK (none)", + result, + fixed = TRUE + )) expect_match(result, "Influential Observations:", fixed = TRUE) })