Skip to content
Open
Show file tree
Hide file tree
Changes from 7 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
3 changes: 2 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.3
Version: 0.6.3.1
Authors@R:
c(person(given = "Dominique",
family = "Makowski",
Expand Down Expand Up @@ -135,6 +135,7 @@ Collate:
'report.survreg.R'
'report.test_performance.R'
'report.zeroinfl.R'
'report_ai.R'
'report_effectsize.R'
'report_htest_chi2.R'
'report_htest_cor.R'
Expand Down
7 changes: 7 additions & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ S3method(format_model,character)
S3method(format_model,default)
S3method(print,cite_easystats)
S3method(print,report)
S3method(print,report_ai)
S3method(print,report_effectsize)
S3method(print,report_info)
S3method(print,report_intercept)
Expand Down Expand Up @@ -60,6 +61,11 @@ S3method(report,stanreg)
S3method(report,survreg)
S3method(report,test_performance)
S3method(report,zeroinfl)
S3method(report_ai,default)
S3method(report_ai,glm)
S3method(report_ai,glmmTMB)
S3method(report_ai,lm)
S3method(report_ai,merMod)
S3method(report_effectsize,MixMod)
S3method(report_effectsize,anova)
S3method(report_effectsize,aov)
Expand Down Expand Up @@ -299,6 +305,7 @@ export(is.report)
export(print_html)
export(print_md)
export(report)
export(report_ai)
export(report_date)
export(report_effectsize)
export(report_info)
Expand Down
10 changes: 10 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
# report 0.6.x

New features

* `report_ai()`: add support for `glm`, `merMod` (lme4), and `glmmTMB` model classes.
* `report_ai()`: `## Model` section now includes a CI / degrees-of-freedom estimation line (e.g., `Inference: 95% CI [Satterthwaite df]`) when the information is available from `parameters::model_parameters()`.
* `report_ai.default()`: instead of stopping with an error, now emits a warning and falls back to the standard `report()` output so that documents continue to render for unsupported model classes.
* `report()`: new `audience` argument (`"humans"` (default) or `"ai"`). When `"ai"`, `report()` delegates to `report_ai()`. The default can be set globally via `options(report_audience = "ai")`.
* New vignette: *AI-Optimized Reports* — explains `report_ai()`, the `audience` argument, and how to convert an entire Quarto document with a single option.

# report 0.6.3

Bug fixes
Expand Down
11 changes: 10 additions & 1 deletion R/report.R
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@
#'
#' @param x The R object that you want to report (see list of of supported
#' objects above).
#' @param audience The intended audience for the report. `"humans"` (default)
#' produces the standard formatted text report. `"ai"` produces a compact,
#' structured output optimised for consumption by a Large Language Model (LLM)
#' or AI agent via [report_ai()]. The default can be changed globally with
#' `options(report_audience = "ai")`.
#' @param ... Arguments passed to or from other methods.
#'
#' @details
Expand Down Expand Up @@ -97,7 +102,11 @@
#' summary(as.data.frame(r))
#'
#' @export
report <- function(x, ...) {
report <- function(x, ..., audience = getOption("report_audience", "humans")) {
audience <- match.arg(audience, c("humans", "ai"))
if (audience == "ai") {
return(report_ai(x, ...))
}
UseMethod("report")
Comment on lines +110 to 115
}

Expand Down
290 changes: 290 additions & 0 deletions R/report_ai.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,290 @@
#' Generate AI-optimized reports
#'
#' 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.
#'
#' @param x A statistical model.
#' @param ... Arguments passed to other functions, like \code{parameters::model_parameters()},
#' \code{performance::model_performance()} or \code{insight::format_table()}.
#' @return A character vector of class `report_ai` containing the formatted text.
#'
#' @examples
#' m <- lm(mpg ~ wt + hp, data = mtcars)
#' report_ai(m)
#' @export
report_ai <- function(x, ...) {
UseMethod("report_ai")
}

#' @export
report_ai.default <- function(x, ...) {
warning(
sprintf(
"AI-optimized reports are not yet available for objects of class '%s'. Falling back to report().",
class(x)[1]
),
call. = FALSE
)
Comment thread
strengejacke marked this conversation as resolved.
Outdated
report(x, ..., audience = "humans")
}

#' @export
report_ai.lm <- function(x, ...) {
.report_ai_models(x, ...)
}

#' @export
report_ai.glm <- report_ai.lm

#' @rdname report_ai
#' @examplesIf requireNamespace("lme4", quietly = TRUE)
#' \donttest{
#' m <- lme4::lmer(Reaction ~ Days + (1 | Subject), data = lme4::sleepstudy)
#' report_ai(m)
#' }
#' @export
report_ai.merMod <- function(x, ...) {
.report_ai_models(x, ...)
}

#' @rdname report_ai
#' @examplesIf requireNamespace("glmmTMB", quietly = TRUE)
#' \donttest{
#' m <- glmmTMB::glmmTMB(count ~ mined + (1 | site), family = poisson(), data = glmmTMB::Salamanders)
#' report_ai(m)
#' }
#' @export
report_ai.glmmTMB <- function(x, ...) {
.report_ai_models(x, ...)
}


# --- Internal Workhorse Function ---
.report_ai_models <- function(x, ...) {
mi <- insight::model_info(x)
dat <- insight::get_data(x)
n_obs <- insight::n_obs(x)
form <- insight::find_formula(x)

func_name <- tryCatch(
as.character(insight::get_call(x)[[1]]),
error = function(e) class(x)[1]
)
Comment thread
strengejacke marked this conversation as resolved.
mod_family <- if (!is.null(mi$family)) mi$family else "Unknown"

Check warning on line 75 in R/report_ai.R

View workflow job for this annotation

GitHub Actions / lint / lint

file=R/report_ai.R,line=75,col=21,[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 75 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=75,col=21,[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.

model_vars_list <- insight::find_variables(x)
# Use only response + conditional (fixed) variables for descriptives;
# random grouping variables (e.g. Subject) are excluded.
fixed_var_comps <- intersect(
c("response", "conditional"),
names(model_vars_list)
)
fixed_vars <- unique(unlist(
model_vars_list[fixed_var_comps],
use.names = FALSE
))
fixed_vars <- fixed_vars[fixed_vars %in% colnames(dat)]

if (length(fixed_vars) > 0) {
desc_report <- suppressWarnings(summary(report::report(
dat[, fixed_vars, drop = FALSE],
audience = "humans"
)))
desc_lines <- unlist(strsplit(as.character(desc_report), "\n"))

Check warning on line 95 in R/report_ai.R

View workflow job for this annotation

GitHub Actions / lint / lint

file=R/report_ai.R,line=95,col=62,[fixed_regex_linter] Use "\n" with fixed = TRUE here. This regular expression is static, i.e., its matches can be expressed as a fixed substring expression, which is faster to compute.

Check warning on line 95 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=95,col=62,[fixed_regex_linter] Use "\n" with fixed = TRUE here. This regular expression is static, i.e., its matches can be expressed as a fixed substring expression, which is faster to compute.

if (length(desc_lines) > 1) {
# Use trimws() to kill the spaces that cause nested bullets
clean_lines <- trimws(desc_lines[-1])
desc_str <- paste0(clean_lines, collapse = "\n")

Check warning on line 100 in R/report_ai.R

View workflow job for this annotation

GitHub Actions / lint / lint

file=R/report_ai.R,line=100,col=19,[paste_linter] Use paste(), not paste0(), to collapse a character vector when sep= is not used.

Check warning on line 100 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=100,col=19,[paste_linter] Use paste(), not paste0(), to collapse a character vector when sep= is not used.
} else {
desc_str <- paste0(trimws(desc_lines), collapse = "\n")

Check warning on line 102 in R/report_ai.R

View workflow job for this annotation

GitHub Actions / lint / lint

file=R/report_ai.R,line=102,col=19,[paste_linter] Use paste(), not paste0(), to collapse a character vector when sep= is not used.

Check warning on line 102 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=102,col=19,[paste_linter] Use paste(), not paste0(), to collapse a character vector when sep= is not used.
}
} else {
desc_str <- "- No variables found."
}

params <- parameters::model_parameters(x, ...)

Comment on lines +83 to +84
# Separate fixed and random effects to avoid duplicated table headers
# (model_parameters returns both in one table for mixed models)
has_random <- "Effects" %in%
names(params) &&
any(!is.na(params$Effects) & params$Effects != "fixed")

if (has_random) {
fixed_params <- params[
!is.na(params$Effects) & params$Effects == "fixed",
,
drop = FALSE
]
random_params <- params[
!is.na(params$Effects) & params$Effects != "fixed",
,
drop = FALSE
]
} else {
fixed_params <- params
random_params <- NULL
}

param_str <- insight::format_table(fixed_params) |>
insight::export_table(format = "markdown") |>
paste0(collapse = "\n")

Check warning on line 134 in R/report_ai.R

View workflow job for this annotation

GitHub Actions / lint / lint

file=R/report_ai.R,line=134,col=5,[paste_linter] Use paste(), not paste0(), to collapse a character vector when sep= is not used.

Check warning on line 134 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=134,col=5,[paste_linter] Use paste(), not paste0(), to collapse a character vector when sep= is not used.
Comment thread
strengejacke marked this conversation as resolved.
Outdated

# Format random effect variances as metadata bullet points
random_str <- NULL
if (!is.null(random_params) && nrow(random_params) > 0) {
coef_col <- intersect(
c("Coefficient", "Estimate", "SD"),
names(random_params)
)[1]
random_str <- paste(
vapply(
seq_len(nrow(random_params)),
function(i) {
row <- random_params[i, , drop = FALSE]

Check warning on line 147 in R/report_ai.R

View workflow job for this annotation

GitHub Actions / lint / lint

file=R/report_ai.R,line=147,col=11,[object_overwrite_linter] 'row' is an exported object from package 'base'. Avoid re-using such symbols.

Check warning on line 147 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=147,col=11,[object_overwrite_linter] 'row' is an exported object from package 'base'. Avoid re-using such symbols.
param_name <- if ("Parameter" %in% names(row)) {
as.character(row$Parameter)
} else {
"?"
}
group_tag <- if (
"Group" %in%
names(row) &&
!is.na(row$Group) &&
nchar(as.character(row$Group)) > 0

Check warning on line 157 in R/report_ai.R

View workflow job for this annotation

GitHub Actions / lint / lint

file=R/report_ai.R,line=157,col=15,[nzchar_linter] Use nzchar(x) instead of nchar(x) > 0. Whenever missing data is possible, please take care to use nzchar(., keepNA = TRUE); nzchar(NA) is TRUE by default.

Check warning on line 157 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=157,col=15,[nzchar_linter] Use nzchar(x) instead of nchar(x) > 0. Whenever missing data is possible, please take care to use nzchar(., keepNA = TRUE); nzchar(NA) is TRUE by default.
) {
paste0(" [", row$Group, "]")
} else {
""
}
val <- if (!is.na(coef_col) && coef_col %in% names(row)) {
sprintf("%.3f", as.numeric(row[[coef_col]]))
} else {
"?"
}
paste0("- ", param_name, group_tag, ": ", val)
},
character(1)
),
collapse = "\n"
)
}

perf <- performance::model_performance(x, ...)
perf_str <- insight::format_table(perf) |>
insight::export_table(format = "markdown") |>
paste0(collapse = "\n")

Check warning on line 179 in R/report_ai.R

View workflow job for this annotation

GitHub Actions / lint / lint

file=R/report_ai.R,line=179,col=5,[paste_linter] Use paste(), not paste0(), to collapse a character vector when sep= is not used.

Check warning on line 179 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=179,col=5,[paste_linter] Use paste(), not paste0(), to collapse a character vector when sep= is not used.
Comment thread
strengejacke marked this conversation as resolved.
Outdated

if ("p" %in% names(fixed_params) && "Parameter" %in% names(fixed_params)) {
sig_effects <- fixed_params$Parameter[
!is.na(fixed_params$p) &
fixed_params$p < 0.05 &
fixed_params$Parameter != "(Intercept)"
]
highlights_str <- if (length(sig_effects) == 0) {
"- Significant effects: None"
} else {
sprintf(
"- Significant effects (p < 0.05): %s",
paste(sig_effects, collapse = ", ")

Check warning on line 192 in R/report_ai.R

View workflow job for this annotation

GitHub Actions / lint / lint

file=R/report_ai.R,line=192,col=9,[paste_linter] toString(.) is more expressive than paste(., collapse = ", "). Note also glue::glue_collapse() and and::and() for constructing human-readable / translation-friendly lists

Check warning on line 192 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=192,col=9,[paste_linter] toString(.) is more expressive than paste(., collapse = ", "). Note also glue::glue_collapse() and and::and() for constructing human-readable / translation-friendly lists
)
}
} else {
highlights_str <- "- Significant effects: Could not be determined."
}

formula_str <- if (is.list(form)) {
Reduce(paste, deparse(form$conditional))
} else {
Reduce(paste, deparse(form))
}
Comment thread
strengejacke marked this conversation as resolved.

# CI / degrees-of-freedom estimation method
ci_level <- attr(params, "ci")
ci_method <- attr(params, "ci_method")
if (!is.null(ci_level) && !is.null(ci_method)) {
ci_pct <- sprintf("%.0f%%", ci_level * 100)
ci_label <- .ci_method_label(ci_method)
inference_str <- paste0("- Inference: ", ci_pct, " CI [", ci_label, "]")
} else if (!is.null(ci_level)) {

Check warning on line 212 in R/report_ai.R

View workflow job for this annotation

GitHub Actions / lint / lint

file=R/report_ai.R,line=212,col=14,[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 212 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=212,col=14,[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.
inference_str <- paste0(
"- Inference: ",
sprintf("%.0f%%", ci_level * 100),
" CI"
)
} else {
inference_str <- NULL
}

param_section <- if (!is.null(random_str)) {
paste0("## Parameters\n", param_str, "\n\n### Random Effects\n", random_str)
} else {
paste0("## Parameters\n", param_str)
}

model_section <- paste0(
"## Model\n",
"- Call: ",
func_name,
"\n",
"- Formula: ",
formula_str,
"\n",
"- Family: ",
mod_family,
"\n",
"- N: ",
n_obs,
if (!is.null(inference_str)) paste0("\n", inference_str) else ""
)

res <- paste0(
model_section,
"\n\n",
"## Variables\n",
desc_str,
"\n\n",
param_section,
"\n\n",
"## Performance\n",
perf_str,
"\n\n",
"## Highlights\n",
highlights_str
)

class(res) <- c("report_ai", "character")
return(res)
}

# Helper: human-readable CI / df-method label
.ci_method_label <- function(method) {
labels <- c(
wald = "Wald",
residual = "Residual df (t/F)",
satterthwaite = "Satterthwaite df",
kenward = "Kenward-Roger df",
normal = "Normal (z)",
profile = "Profile likelihood",
boot = "Bootstrap",
uniroot = "Uniroot",
hdi = "HDI",
eti = "ETI",
si = "SI"
)
lab <- labels[tolower(as.character(method))]
if (is.na(lab)) {
tools::toTitleCase(tolower(as.character(method)))
} else {
unname(lab)
}
}

#' @export
print.report_ai <- function(x, ...) {
cat(x, "\n")
invisible(x)
}
8 changes: 7 additions & 1 deletion man/report.Rd

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

Loading
Loading