diff --git a/NAMESPACE b/NAMESPACE index 5129b999..d7432de8 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -26,6 +26,7 @@ export(ard_car_anova) export(ard_car_vif) export(ard_categorical) export(ard_categorical_ci) +export(ard_categorical_max) export(ard_continuous) export(ard_continuous_ci) export(ard_dichotomous) diff --git a/NEWS.md b/NEWS.md index 928024a0..a59c0f33 100644 --- a/NEWS.md +++ b/NEWS.md @@ -6,6 +6,8 @@ * Fixed a bug in `ard_survival_survfit()` causing an error when "=" character is present in stratification variable level labels. (#252) +* Added function `ard_categorical_max()` to calculate categorical occurrence rates by maximum level per unique ID. (#240) + # cardx 0.2.2 * Added a `data.frame` method to `ard_survival_survfit()`. diff --git a/R/ard_categorical_max.R b/R/ard_categorical_max.R new file mode 100644 index 00000000..659386bd --- /dev/null +++ b/R/ard_categorical_max.R @@ -0,0 +1,108 @@ +#' ARD to Calculate Categorical Occurrence Rates by Maximum Level Per Unique ID +#' +#' Function calculates categorical variable level occurrences rates by maximum level per unique ID. +#' Each variable in `variables` is evaluated independently and then results for all variables are stacked. +#' Only the highest-ordered level will be counted for each unique ID. +#' Unordered, non-numeric variables will be converted to factor and the default level order used for ordering. +#' +#' @inheritParams cards::ard_categorical +#' @inheritParams cards::ard_stack +#' @param variables ([`tidy-select`][dplyr::dplyr_tidy_select])\cr +#' The categorical variables for which occurrence rates per unique ID (by maximum level) will be calculated. +#' @param id ([`tidy-select`][dplyr::dplyr_tidy_select])\cr +#' Argument used to subset `data` to identify rows in `data` to calculate categorical variable level occurrence rates. +#' @param denominator (`data.frame`, `integer`)\cr +#' An optional argument to change the denominator used for `"N"` and `"p"` statistic calculations. +#' Defaults to `NULL`, in which case `dplyr::distinct(data, dplyr::pick(all_of(c(id, by))))` is used for these +#' calculations. See [cards::ard_categorical()] for more details on specifying denominators. +#' @param quiet (scalar `logical`)\cr +#' Logical indicating whether to suppress additional messaging. Default is `FALSE`. +#' +#' @return an ARD data frame of class 'card' +#' @name ard_categorical_max +#' +#' @examples +#' # Occurrence Rates by Max Level (Highest Severity) -------------------------- +#' ard_categorical_max( +#' cards::ADAE, +#' variables = c(AESER, AESEV), +#' id = USUBJID, +#' by = TRTA, +#' denominator = cards::ADSL |> dplyr::rename(TRTA = ARM) +#' ) +NULL + +#' @rdname ard_categorical_max +#' @export +ard_categorical_max <- function(data, + variables, + id, + by = dplyr::group_vars(data), + statistic = everything() ~ c("n", "p", "N"), + denominator = NULL, + fmt_fn = NULL, + stat_label = everything() ~ cards::default_stat_labels(), + quiet = FALSE, + ...) { + set_cli_abort_call() + + # check inputs --------------------------------------------------------------- + check_not_missing(data) + check_not_missing(variables) + check_not_missing(id) + cards::process_selectors(data, variables = {{ variables }}, id = {{ id }}, by = {{ by }}) + data <- dplyr::ungroup(data) + + # check the id argument is not empty + if (is_empty(id)) { + cli::cli_abort("Argument {.arg id} cannot be empty.", call = get_cli_abort_call()) + } + + # return empty ARD if no variables selected ---------------------------------- + if (is_empty(variables)) { + return(dplyr::tibble() |> cards::as_card()) + } + + lst_results <- lapply( + variables, + function(x) { + ard_categorical( + data = data |> + arrange_using_order(c(id, by, x)) |> + dplyr::slice_tail(n = 1L, by = all_of(c(id, by))), + variables = all_of(x), + by = all_of(by), + statistic = statistic, + denominator = denominator, + fmt_fn = fmt_fn, + stat_label = stat_label + ) + } + ) + + # print default order of variable levels ------------------------------------- + for (v in variables) { + lvls <- .unique_and_sorted(data[[v]]) + vec <- cli::cli_vec( + lvls, + style = list("vec-sep" = " < ", "vec-sep2" = " < ", "vec-last" = " < ", "vec-trunc" = 3) + ) + if (!quiet) cli::cli_inform("{.var {v}}: {.val {vec}}") + } + + # combine results ------------------------------------------------------------ + result <- lst_results |> + dplyr::bind_rows() |> + dplyr::mutate(context = "categorical_max") |> + cards::tidy_ard_column_order() |> + cards::tidy_ard_row_order() + + # return final result -------------------------------------------------------- + result +} + +# internal function copied from cards +# like `dplyr::arrange()`, but uses base R's `order()` to keep consistency in some edge cases +arrange_using_order <- function(data, columns) { + inject(data[with(data, order(!!!syms(columns))), ]) +} diff --git a/_pkgdown.yml b/_pkgdown.yml index c129d233..f6e21349 100644 --- a/_pkgdown.yml +++ b/_pkgdown.yml @@ -88,6 +88,7 @@ reference: - ard_categorical_ci.data.frame - ard_regression - ard_regression_basic + - ard_categorical_max - title: "Helpers" - contents: diff --git a/man/ard_categorical_max.Rd b/man/ard_categorical_max.Rd new file mode 100644 index 00000000..25bb6a1a --- /dev/null +++ b/man/ard_categorical_max.Rd @@ -0,0 +1,79 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/ard_categorical_max.R +\name{ard_categorical_max} +\alias{ard_categorical_max} +\title{ARD to Calculate Categorical Occurrence Rates by Maximum Level Per Unique ID} +\usage{ +ard_categorical_max( + data, + variables, + id, + by = dplyr::group_vars(data), + statistic = everything() ~ c("n", "p", "N"), + denominator = NULL, + fmt_fn = NULL, + stat_label = everything() ~ cards::default_stat_labels(), + quiet = FALSE, + ... +) +} +\arguments{ +\item{data}{(\code{data.frame})\cr +a data frame} + +\item{variables}{(\code{\link[dplyr:dplyr_tidy_select]{tidy-select}})\cr +The categorical variables for which occurrence rates per unique ID (by maximum level) will be calculated.} + +\item{id}{(\code{\link[dplyr:dplyr_tidy_select]{tidy-select}})\cr +Argument used to subset \code{data} to identify rows in \code{data} to calculate categorical variable level occurrence rates.} + +\item{by}{(\code{\link[dplyr:dplyr_tidy_select]{tidy-select}})\cr +columns to tabulate by in the series of ARD function calls. +Any rows with \code{NA} or \code{NaN} values are removed from all calculations.} + +\item{statistic}{(\code{\link[cards:syntax]{formula-list-selector}})\cr +a named list, a list of formulas, +or a single formula where the list element one or more of \code{c("n", "N", "p")} +(or the RHS of a formula).} + +\item{denominator}{(\code{data.frame}, \code{integer})\cr +An optional argument to change the denominator used for \code{"N"} and \code{"p"} statistic calculations. +Defaults to \code{NULL}, in which case \code{dplyr::distinct(data, dplyr::pick(all_of(c(id, by))))} is used for these +calculations. See \code{\link[cards:ard_categorical]{cards::ard_categorical()}} for more details on specifying denominators.} + +\item{fmt_fn}{(\code{\link[cards:syntax]{formula-list-selector}})\cr +a named list, a list of formulas, +or a single formula where the list element is a named list of functions +(or the RHS of a formula), +e.g. \verb{list(mpg = list(mean = \\(x) round(x, digits = 2) |> as.character()))}.} + +\item{stat_label}{(\code{\link[cards:syntax]{formula-list-selector}})\cr +a named list, a list of formulas, or a single formula where +the list element is either a named list or a list of formulas defining the +statistic labels, e.g. \code{everything() ~ list(n = "n", p = "pct")} or +\code{everything() ~ list(n ~ "n", p ~ "pct")}.} + +\item{quiet}{(scalar \code{logical})\cr +Logical indicating whether to suppress additional messaging. Default is \code{FALSE}.} + +\item{...}{Arguments passed to methods.} +} +\value{ +an ARD data frame of class 'card' +} +\description{ +Function calculates categorical variable level occurrences rates by maximum level per unique ID. +Each variable in \code{variables} is evaluated independently and then results for all variables are stacked. +Only the highest-ordered level will be counted for each unique ID. +Unordered, non-numeric variables will be converted to factor and the default level order used for ordering. +} +\examples{ +# Occurrence Rates by Max Level (Highest Severity) -------------------------- +ard_categorical_max( + cards::ADAE, + variables = c(AESER, AESEV), + id = USUBJID, + by = TRTA, + denominator = cards::ADSL |> dplyr::rename(TRTA = ARM) +) +} diff --git a/tests/testthat/_snaps/ard_categorical_max.md b/tests/testthat/_snaps/ard_categorical_max.md new file mode 100644 index 00000000..51937e20 --- /dev/null +++ b/tests/testthat/_snaps/ard_categorical_max.md @@ -0,0 +1,194 @@ +# ard_categorical_max() works with default settings + + Code + print(res, n = 20, columns = "all") + Message + {cards} data frame: 27 x 11 + Output + group1 group1_level variable variable_level context stat_name stat_label stat fmt_fn warning error + 1 TRTA Placebo AESEV MILD categori… n n 36 0 + 2 TRTA Placebo AESEV MILD categori… N N 69 0 + 3 TRTA Placebo AESEV MILD categori… p % 0.522 + 4 TRTA Placebo AESEV MODERATE categori… n n 26 0 + 5 TRTA Placebo AESEV MODERATE categori… N N 69 0 + 6 TRTA Placebo AESEV MODERATE categori… p % 0.377 + 7 TRTA Placebo AESEV SEVERE categori… n n 7 0 + 8 TRTA Placebo AESEV SEVERE categori… N N 69 0 + 9 TRTA Placebo AESEV SEVERE categori… p % 0.101 + 10 TRTA Xanomeli… AESEV MILD categori… n n 22 0 + 11 TRTA Xanomeli… AESEV MILD categori… N N 79 0 + 12 TRTA Xanomeli… AESEV MILD categori… p % 0.278 + 13 TRTA Xanomeli… AESEV MODERATE categori… n n 49 0 + 14 TRTA Xanomeli… AESEV MODERATE categori… N N 79 0 + 15 TRTA Xanomeli… AESEV MODERATE categori… p % 0.62 + 16 TRTA Xanomeli… AESEV SEVERE categori… n n 8 0 + 17 TRTA Xanomeli… AESEV SEVERE categori… N N 79 0 + 18 TRTA Xanomeli… AESEV SEVERE categori… p % 0.101 + 19 TRTA Xanomeli… AESEV MILD categori… n n 19 0 + 20 TRTA Xanomeli… AESEV MILD categori… N N 77 0 + Message + i 7 more rows + i Use `print(n = ...)` to see more rows + +--- + + Code + print(ard_categorical_max(dplyr::group_by(cards::ADAE, TRTA), variables = AESEV, id = USUBJID, denominator = dplyr::rename(cards::ADSL, TRTA = ARM)), n = 20, columns = "all") + Message + `AESEV`: "MILD" < "MODERATE" < "SEVERE" + {cards} data frame: 27 x 11 + Output + group1 group1_level variable variable_level context stat_name stat_label stat fmt_fn warning error + 1 TRTA Placebo AESEV MILD categori… n n 36 0 + 2 TRTA Placebo AESEV MILD categori… N N 86 0 + 3 TRTA Placebo AESEV MILD categori… p % 0.419 + 4 TRTA Placebo AESEV MODERATE categori… n n 26 0 + 5 TRTA Placebo AESEV MODERATE categori… N N 86 0 + 6 TRTA Placebo AESEV MODERATE categori… p % 0.302 + 7 TRTA Placebo AESEV SEVERE categori… n n 7 0 + 8 TRTA Placebo AESEV SEVERE categori… N N 86 0 + 9 TRTA Placebo AESEV SEVERE categori… p % 0.081 + 10 TRTA Xanomeli… AESEV MILD categori… n n 22 0 + 11 TRTA Xanomeli… AESEV MILD categori… N N 84 0 + 12 TRTA Xanomeli… AESEV MILD categori… p % 0.262 + 13 TRTA Xanomeli… AESEV MODERATE categori… n n 49 0 + 14 TRTA Xanomeli… AESEV MODERATE categori… N N 84 0 + 15 TRTA Xanomeli… AESEV MODERATE categori… p % 0.583 + 16 TRTA Xanomeli… AESEV SEVERE categori… n n 8 0 + 17 TRTA Xanomeli… AESEV SEVERE categori… N N 84 0 + 18 TRTA Xanomeli… AESEV SEVERE categori… p % 0.095 + 19 TRTA Xanomeli… AESEV MILD categori… n n 19 0 + 20 TRTA Xanomeli… AESEV MILD categori… N N 84 0 + Message + i 7 more rows + i Use `print(n = ...)` to see more rows + +# ard_categorical_max(statistic) works + + Code + ard_categorical_max(cards::ADAE, variables = AESEV, id = USUBJID, by = TRTA, denominator = dplyr::rename(cards::ADSL, TRTA = ARM), statistic = ~"n") + Message + `AESEV`: "MILD" < "MODERATE" < "SEVERE" + {cards} data frame: 9 x 11 + Output + group1 group1_level variable variable_level stat_name stat_label stat + 1 TRTA Placebo AESEV MILD n n 36 + 2 TRTA Placebo AESEV MODERATE n n 26 + 3 TRTA Placebo AESEV SEVERE n n 7 + 4 TRTA Xanomeli… AESEV MILD n n 22 + 5 TRTA Xanomeli… AESEV MODERATE n n 49 + 6 TRTA Xanomeli… AESEV SEVERE n n 8 + 7 TRTA Xanomeli… AESEV MILD n n 19 + 8 TRTA Xanomeli… AESEV MODERATE n n 42 + 9 TRTA Xanomeli… AESEV SEVERE n n 16 + Message + i 4 more variables: context, fmt_fn, warning, error + +# ard_categorical_max(denominator) works + + Code + ard_categorical_max(cards::ADAE, variables = AESEV, id = USUBJID, by = TRTA) + Message + `AESEV`: "MILD" < "MODERATE" < "SEVERE" + {cards} data frame: 27 x 11 + Output + group1 group1_level variable variable_level stat_name stat_label stat + 1 TRTA Placebo AESEV MILD n n 36 + 2 TRTA Placebo AESEV MILD N N 69 + 3 TRTA Placebo AESEV MILD p % 0.522 + 4 TRTA Placebo AESEV MODERATE n n 26 + 5 TRTA Placebo AESEV MODERATE N N 69 + 6 TRTA Placebo AESEV MODERATE p % 0.377 + 7 TRTA Placebo AESEV SEVERE n n 7 + 8 TRTA Placebo AESEV SEVERE N N 69 + 9 TRTA Placebo AESEV SEVERE p % 0.101 + 10 TRTA Xanomeli… AESEV MILD n n 22 + Message + i 17 more rows + i Use `print(n = ...)` to see more rows + i 4 more variables: context, fmt_fn, warning, error + +--- + + Code + ard_categorical_max(cards::ADAE, variables = AESEV, id = USUBJID, by = TRTA, denominator = 100) + Message + `AESEV`: "MILD" < "MODERATE" < "SEVERE" + {cards} data frame: 27 x 11 + Output + group1 group1_level variable variable_level stat_name stat_label stat + 1 TRTA Placebo AESEV MILD n n 36 + 2 TRTA Placebo AESEV MILD N N 100 + 3 TRTA Placebo AESEV MILD p % 0.36 + 4 TRTA Placebo AESEV MODERATE n n 26 + 5 TRTA Placebo AESEV MODERATE N N 100 + 6 TRTA Placebo AESEV MODERATE p % 0.26 + 7 TRTA Placebo AESEV SEVERE n n 7 + 8 TRTA Placebo AESEV SEVERE N N 100 + 9 TRTA Placebo AESEV SEVERE p % 0.07 + 10 TRTA Xanomeli… AESEV MILD n n 22 + Message + i 17 more rows + i Use `print(n = ...)` to see more rows + i 4 more variables: context, fmt_fn, warning, error + +# ard_categorical_max() works with pre-ordered factor variables + + Code + print(res, n = 20, columns = "all") + Message + {cards} data frame: 27 x 11 + Output + group1 group1_level variable variable_level context stat_name stat_label stat fmt_fn warning error + 1 TRTA Placebo AESEV MILD categori… n n 36 0 + 2 TRTA Placebo AESEV MILD categori… N N 86 0 + 3 TRTA Placebo AESEV MILD categori… p % 0.419 + 4 TRTA Placebo AESEV MODERATE categori… n n 26 0 + 5 TRTA Placebo AESEV MODERATE categori… N N 86 0 + 6 TRTA Placebo AESEV MODERATE categori… p % 0.302 + 7 TRTA Placebo AESEV SEVERE categori… n n 7 0 + 8 TRTA Placebo AESEV SEVERE categori… N N 86 0 + 9 TRTA Placebo AESEV SEVERE categori… p % 0.081 + 10 TRTA Xanomeli… AESEV MILD categori… n n 22 0 + 11 TRTA Xanomeli… AESEV MILD categori… N N 84 0 + 12 TRTA Xanomeli… AESEV MILD categori… p % 0.262 + 13 TRTA Xanomeli… AESEV MODERATE categori… n n 49 0 + 14 TRTA Xanomeli… AESEV MODERATE categori… N N 84 0 + 15 TRTA Xanomeli… AESEV MODERATE categori… p % 0.583 + 16 TRTA Xanomeli… AESEV SEVERE categori… n n 8 0 + 17 TRTA Xanomeli… AESEV SEVERE categori… N N 84 0 + 18 TRTA Xanomeli… AESEV SEVERE categori… p % 0.095 + 19 TRTA Xanomeli… AESEV MILD categori… n n 19 0 + 20 TRTA Xanomeli… AESEV MILD categori… N N 84 0 + Message + i 7 more rows + i Use `print(n = ...)` to see more rows + +# ard_categorical_max() errors with incomplete factor columns + + Code + ard_categorical_max(dplyr::mutate(cards::ADAE, AESOC = factor(AESOC, levels = character( + 0))), variables = AESOC, id = USUBJID, by = TRTA) + Condition + Error in `ard_categorical_max()`: + ! Factors with empty "levels" attribute are not allowed, which was identified in column "AESOC". + +--- + + Code + ard_categorical_max(dplyr::mutate(cards::ADAE, SEX = factor(SEX, levels = c("F", + "M", NA), exclude = NULL)), variables = SEX, id = USUBJID, by = TRTA) + Condition + Error in `ard_categorical_max()`: + ! Factors with NA levels are not allowed, which are present in column "SEX". + +# ard_categorical_max() works without any variables + + Code + ard_categorical_max(data = cards::ADAE, variables = starts_with("xxxx"), id = USUBJID, + by = c(TRTA, AESEV)) + Message + {cards} data frame: 0 x 0 + Output + data frame with 0 columns and 0 rows + diff --git a/tests/testthat/test-ard_categorical_max.R b/tests/testthat/test-ard_categorical_max.R new file mode 100644 index 00000000..075e3455 --- /dev/null +++ b/tests/testthat/test-ard_categorical_max.R @@ -0,0 +1,253 @@ +test_that("ard_categorical_max() works with default settings", { + withr::local_options(list(width = 200)) + + expect_message( + res <- ard_categorical_max( + cards::ADAE, + variables = AESEV, + id = USUBJID, + by = TRTA + ) + ) + expect_snapshot(res |> print(n = 20, columns = "all")) + + expect_equal( + res |> + dplyr::filter( + group1_level == "Placebo", + variable_level == "SEVERE", + stat_name == "n" + ) |> + cards::get_ard_statistics(), + list( + n = cards::ADAE |> + dplyr::filter( + TRTA == "Placebo", + AESEV == "SEVERE" + ) |> + dplyr::slice_tail(n = 1L, by = all_of(c("USUBJID", "TRTA", "AESEV"))) |> + nrow() + ) + ) + + # with denominator + expect_snapshot( + ard_categorical_max( + cards::ADAE |> dplyr::group_by(TRTA), + variables = AESEV, + id = USUBJID, + denominator = cards::ADSL |> dplyr::rename(TRTA = ARM) + ) |> + print(n = 20, columns = "all") + ) + + # with multiple variables + expect_message(expect_message( + res2 <- ard_categorical_max( + cards::ADAE, + variables = c(AESEV, AESER), + id = USUBJID, + by = TRTA + ) + )) + expect_equal(unique(res2$variable), c("AESEV", "AESER")) + expect_equal( + res, + res2[-c(28:45), ] + ) +}) + +test_that("ard_categorical_max(statistic) works", { + withr::local_options(list(width = 200)) + + expect_snapshot( + ard_categorical_max( + cards::ADAE, + variables = AESEV, + id = USUBJID, + by = TRTA, + denominator = cards::ADSL |> dplyr::rename(TRTA = ARM), + statistic = ~"n" + ) + ) +}) + +test_that("ard_categorical_max(denominator) works", { + withr::local_options(list(width = 200)) + + # default denominator + expect_snapshot( + ard_categorical_max( + cards::ADAE, + variables = AESEV, + id = USUBJID, + by = TRTA + ) + ) + + # numeric denominator + expect_snapshot( + ard_categorical_max( + cards::ADAE, + variables = AESEV, + id = USUBJID, + by = TRTA, + denominator = 100 + ) + ) +}) + +test_that("ard_categorical_max(quiet) works", { + withr::local_options(list(width = 200)) + + expect_silent( + ard_categorical_max( + cards::ADAE, + variables = c(AESER, AESEV), + id = USUBJID, + by = TRTA, + denominator = cards::ADSL |> dplyr::rename(TRTA = ARM), + quiet = TRUE + ) + ) +}) + +test_that("ard_categorical_max() works with pre-ordered factor variables", { + withr::local_options(list(width = 200)) + + # ordered factor variable + adae <- cards::ADAE |> + dplyr::mutate(AESEV = factor(cards::ADAE$AESEV, ordered = TRUE)) + # unordered factor variable + adae_unord <- cards::ADAE |> + dplyr::mutate(AESEV = factor(cards::ADAE$AESEV, ordered = FALSE)) + + expect_message( + res <- ard_categorical_max( + adae_unord, + variables = AESEV, + id = USUBJID, + by = TRTA, + denominator = cards::ADSL |> dplyr::rename(TRTA = ARM), + ordered = TRUE + ) + ) + expect_snapshot(res |> print(n = 20, columns = "all")) + + expect_equal( + res |> + dplyr::mutate(variable_level = as.character(unlist(variable_level))) |> + dplyr::filter( + group1_level == "Placebo", + variable_level == "MODERATE", + stat_name == "n" + ) |> + cards::get_ard_statistics(), + list( + n = adae |> + dplyr::arrange(AESEV) |> + dplyr::slice_tail(n = 1L, by = all_of(c("USUBJID", "TRTA"))) |> + dplyr::filter( + TRTA == "Placebo", + AESEV == "MODERATE" + ) |> + nrow() + ) + ) + + expect_message( + res_unord <- ard_categorical_max( + adae_unord, + variables = AESEV, + id = USUBJID, + by = TRTA, + denominator = cards::ADSL |> dplyr::rename(TRTA = ARM) + ) + ) + expect_equal(res$stat[[1]], res_unord$stat[[1]]) + + expect_message( + res2 <- ard_categorical_max( + adae, + variables = AESEV, + id = USUBJID, + by = TRTA, + denominator = cards::ADSL |> dplyr::rename(TRTA = ARM) + ) + ) + expect_equal(res, res2, ignore_attr = "class") + + # multiple variables + expect_message(expect_message( + res3 <- ard_categorical_max( + adae, + variables = c(SEX, AESEV), + id = USUBJID, + by = TRTA, + denominator = cards::ADSL |> dplyr::rename(TRTA = ARM), + ordered = c(FALSE, TRUE) + ) + )) + expect_equal(res, res3[-c(1:18), ]) + + # named vector + expect_message(expect_message( + res4 <- ard_categorical_max( + adae, + variables = c(SEX, AESEV), + id = USUBJID, + by = TRTA, + denominator = cards::ADSL |> dplyr::rename(TRTA = ARM), + ordered = c(AESEV = TRUE, SEX = FALSE) + ) + )) + expect_equal(res3, res4) +}) + +test_that("ard_categorical_max() errors with incomplete factor columns", { + # Check error when factors have no levels + expect_snapshot( + error = TRUE, + ard_categorical_max( + cards::ADAE |> + dplyr::mutate(AESOC = factor(AESOC, levels = character(0))), + variables = AESOC, + id = USUBJID, + by = TRTA + ) + ) + + # Check error when factor has NA level + expect_snapshot( + error = TRUE, + ard_categorical_max( + cards::ADAE |> + dplyr::mutate(SEX = factor(SEX, levels = c("F", "M", NA), exclude = NULL)), + variables = SEX, + id = USUBJID, + by = TRTA + ) + ) +}) + +test_that("ard_categorical_max() works without any variables", { + expect_snapshot( + ard_categorical_max( + data = cards::ADAE, + variables = starts_with("xxxx"), + id = USUBJID, + by = c(TRTA, AESEV) + ) + ) +}) + +test_that("ard_categorical_max() follows ard structure", { + expect_message( + ard_categorical_max( + cards::ADAE, + variables = AESOC, + id = USUBJID + ) |> + cards::check_ard_structure(method = FALSE) + ) +})