Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 4 additions & 1 deletion DESCRIPTION
Original file line number Diff line number Diff line change
@@ -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",
Expand Down Expand Up @@ -117,6 +117,7 @@ Collate:
'report.stanreg.R'
'report.brmsfit.R'
'report.character.R'
'report.check_outliers.R'
'report.compare.loo.R'
Comment on lines 117 to 121
'report.compare_performance.R'
'report.coxph.R'
Expand All @@ -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'
Expand Down Expand Up @@ -162,3 +164,4 @@ Collate:
'zzz.R'
Roxygen: list(markdown = TRUE)
Config/roxygen2/version: 8.0.0
RoxygenNote: 7.3.3
4 changes: 3 additions & 1 deletion NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
14 changes: 14 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -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()`), 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 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

* 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
Expand Down
112 changes: 112 additions & 0 deletions R/report.check_outliers.R
Original file line number Diff line number Diff line change
@@ -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)
}
27 changes: 25 additions & 2 deletions R/report.lm.R
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -55,6 +58,7 @@ report.lm <- function(
x,
include_effectsize = TRUE,
effectsize_method = "refit",
assumptions = TRUE,
...
) {
result_table <- report_table(
Expand All @@ -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, ...)
}
Expand Down Expand Up @@ -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

Expand All @@ -753,6 +762,18 @@ report_text.lm <- function(x, table = NULL, ...) {
)
}

# --- Assumptions (requires performance) ---------------------------------
assumptions_summary <- ""
if (isTRUE(assumptions) && 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)
Expand All @@ -773,6 +794,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,
Expand All @@ -787,6 +809,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),
Expand Down
18 changes: 17 additions & 1 deletion R/report_ai.R
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
UseMethod("report_ai")
}

#' @exportS3Method
report_ai.default <- function(x, ...) {
insight::format_warning(
paste0(
Expand All @@ -14,16 +15,22 @@
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, ...)
}
Expand Down Expand Up @@ -79,6 +86,8 @@
} 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, ...)

Expand Down Expand Up @@ -153,6 +162,12 @@
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) &
Expand Down Expand Up @@ -221,6 +236,7 @@
"\n\n",
"## Variables\n",
desc_str,
if (!is.null(assumptions_str)) paste0("\n\n", assumptions_str) else "",

Check warning on line 239 in R/report_ai.R

View workflow job for this annotation

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

file=R/report_ai.R,line=239,col=9,[if_not_else_linter] Prefer `if (A) x else y` to the less-readable `if (!A) y else x` in a simple if/else statement.

Check warning on line 239 in R/report_ai.R

View workflow job for this annotation

GitHub Actions / lint / lint

file=R/report_ai.R,line=239,col=9,[if_not_else_linter] Prefer `if (A) x else y` to the less-readable `if (!A) y else x` in a simple if/else statement.
"\n\n",
param_section,
"\n\n",
Expand Down
Loading
Loading