diff --git a/.github/workflows/rev-dep-check.yaml b/.github/workflows/rev-dep-check.yaml index 18cb00876..4fbb09e87 100644 --- a/.github/workflows/rev-dep-check.yaml +++ b/.github/workflows/rev-dep-check.yaml @@ -37,26 +37,48 @@ jobs: - name: Check Reverse Dependencies shell: Rscript {0} run: | - df <- - checked::rev_dep_check_tasks_df( - path = ".", - repos = "https://packagemanager.posit.co/cran/latest", - versions = c("dev", "release") - ) - df <- df[startsWith(df$alias, "gtsummary"), ] + g <- checked::plan_rev_dep_checks( + path = ".", + repos = c('https://ddsjoberg.r-universe.dev', 'https://cloud.r-project.org') + ) + vs <- igraph::V(g) + mt <- vs[checked:::is_meta(vs$task)] + nodes_to_skip <- mt[!xor( + endsWith(mt$name, "meta-revdep-gtsummary-of-cards"), + endsWith(mt$name, "meta-revdeps-of-cards") + )] + g <- igraph::delete_vertices( + g, + unlist(igraph::neighborhood(g, order = 1, nodes_to_skip, mode = "out")) + ) + g <- checked:::task_graph_class(g) - design <- - checked::check_design$new( - df = df, - repos = "https://packagemanager.posit.co/cran/latest", - output = tempdir(), - restore = FALSE - ) - checked::run(design) - checked::results(design) + design <- checked::checker$new( + g, + n = 20, + output = tempdir(), + lib.loc = .libPaths(), + repos = c( + 'https://ddsjoberg.r-universe.dev', + 'https://cloud.r-project.org' + ), + restore = FALSE + ) + + checked::run(design, reporter = NULL) + (r <- checked::results(design)) # Logic to check for errors and abort if found - issues <- checked::results(design)$revdep_check_task_spec$gtsummary$errors$issues + potential_issues <- r[[1]][[1]]$errors$potential_issues + issues <- r[[1]][[1]]$errors$issues + + if (!rlang::is_empty(potential_issues)) { + cli::cli_warn( + c("A potential issue was flagged in {.pkg gtsummary}.", + "i" = "This might mean the CRAN baseline is currently unstable, but a manual review is required just in case.") + ) + } + if (!rlang::is_empty(issues)) { cli::cli_abort( c("This PR newly breaks {.pkg gtsummary}", @@ -64,4 +86,5 @@ jobs: "i" = "In rare cases where the break is acceptable, the change {.emph MUST} first be approved by Daniel Sjoberg (danieldsjoberg@gmail.com) {.emph before} this PR could potentially be merged." ) ) - } + } + diff --git a/DESCRIPTION b/DESCRIPTION index 6e85cc161..c51705a52 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,6 +1,6 @@ Package: cards Title: Analysis Results Data -Version: 0.7.1.9012 +Version: 0.7.1.9011 Authors@R: c( person("Daniel D.", "Sjoberg", , "danield.sjoberg@gmail.com", role = c("aut", "cre"), comment = c(ORCID = "0000-0003-0862-2018")), diff --git a/NAMESPACE b/NAMESPACE index 6ca7b709f..b7c7f6d8f 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -16,6 +16,7 @@ S3method(ard_tabulate_value,data.frame) S3method(ard_total_n,data.frame) S3method(fill_formula_selectors,data.frame) S3method(print,card) +S3method(print,compare_ard) S3method(process_formula_selectors,data.frame) S3method(process_selectors,data.frame) export("%>%") @@ -58,6 +59,7 @@ export(bind_ard) export(captured_condition_as_error) export(captured_condition_as_message) export(cards_select) +export(check_ard_equal) export(check_ard_structure) export(check_list_elements) export(compare_ard) @@ -72,6 +74,7 @@ export(fill_formula_selectors) export(filter_ard_hierarchical) export(get_ard_statistics) export(get_cards_fn_stat_names) +export(is_ard_equal) export(is_cards_fn) export(label_round) export(last_col) diff --git a/NEWS.md b/NEWS.md index a0eae97f8..59e131f14 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,9 +1,14 @@ -# cards 0.7.1.9012 +# cards 0.7.1.9011 + +* Fixed bug in `rename_ard_columns()` whereby factor variables were getting converted to integers and added parameter `fct_as_chr` as is used in `unlist_ard_columns()` (#542) * Adding `ard_tabulate_rows()` function to tabulate the number of rows in a data frame. (#531) * Users are now messaged if the `by` or `strata` arguments pass columns with different classes in the `ard_tabulate(data,denominator)` arguments as this _may_ cause issues downstream. (#515) +* `as_card` now has the argument `check = TRUE` which when `TRUE` will confirm if the data frame being converted matches the cards spec using `check_ard_structure`. To support this, `check_ard_structure` has a new argument `error_on_fail` which is FALSE by default. When TRUE any failures will generate an error. (#514) + + # cards 0.7.1 * Updated `ard_stack_hierarchical()` so that the `denominator` dataset only contains the `id` and `by` variables. (#482) diff --git a/R/ard_attributes.R b/R/ard_attributes.R index 1ce505193..b2b08019f 100644 --- a/R/ard_attributes.R +++ b/R/ard_attributes.R @@ -54,7 +54,7 @@ ard_attributes.data.frame <- function(data, # return empty ARD if no variables selected ---------------------------------- if (is_empty(variables)) { - return(dplyr::tibble() |> as_card()) + return(dplyr::tibble() |> as_card(check = FALSE)) } diff --git a/R/ard_formals.R b/R/ard_formals.R index 4946bdee3..ba89c0623 100644 --- a/R/ard_formals.R +++ b/R/ard_formals.R @@ -46,5 +46,5 @@ ard_formals <- function(fun, arg_names, passed_args = list(), # put formals list in ARD structure ------------------------------------------ enframe(lst_args[arg_names], "stat_name", "stat") |> dplyr::mutate(stat_label = .data$stat_name, .after = "stat_name") |> - as_card() + as_card(check = FALSE) } diff --git a/R/ard_hierarchical.R b/R/ard_hierarchical.R index 01c57f791..d3564ca49 100644 --- a/R/ard_hierarchical.R +++ b/R/ard_hierarchical.R @@ -114,7 +114,7 @@ ard_hierarchical.data.frame <- function(data, # return empty ARD if no variables selected ---------------------------------- if (is_empty(variables)) { - return(dplyr::tibble() |> as_card()) + return(dplyr::tibble() |> as_card(check = FALSE)) } # if denominator doesn't have all by, they need to be added ------------------ @@ -192,7 +192,7 @@ ard_hierarchical_count.data.frame <- function(data, # return empty ARD if no variables selected ---------------------------------- if (is_empty(variables)) { - return(dplyr::tibble() |> as_card()) + return(dplyr::tibble() |> as_card(check = FALSE)) } # add dummy variable for counting -------------------------------------------- @@ -210,7 +210,7 @@ ard_hierarchical_count.data.frame <- function(data, ) |> .rename_last_group_as_variable(by = by, variables = variables) |> dplyr::mutate(context = "hierarchical_count") |> - as_card() + as_card(check = FALSE) } #' Rename Last Group to Variable diff --git a/R/ard_missing.R b/R/ard_missing.R index 3f63903ca..b1132fd2a 100644 --- a/R/ard_missing.R +++ b/R/ard_missing.R @@ -58,7 +58,7 @@ ard_missing.data.frame <- function(data, # return empty ARD if no variables selected ---------------------------------- if (is_empty(variables)) { - return(dplyr::tibble() |> as_card()) + return(dplyr::tibble() |> as_card(check = FALSE)) } # convert all variables to T/F whether it's missing -------------------------- diff --git a/R/ard_mvsummary.R b/R/ard_mvsummary.R index 00ae13c1b..3cb9168c1 100644 --- a/R/ard_mvsummary.R +++ b/R/ard_mvsummary.R @@ -91,7 +91,7 @@ ard_mvsummary.data.frame <- function(data, # return empty ARD if no variables selected ---------------------------------- if (is_empty(variables)) { - return(dplyr::tibble() |> as_card()) + return(dplyr::tibble() |> as_card(check = FALSE)) } missing_statistics_vars <- setdiff(variables, names(statistic)) diff --git a/R/ard_stack_hierarchical.R b/R/ard_stack_hierarchical.R index a3d3d79a2..02102c9a2 100644 --- a/R/ard_stack_hierarchical.R +++ b/R/ard_stack_hierarchical.R @@ -517,7 +517,7 @@ internal_stack_hierarchical <- function( } # return final result -------------------------------------------------------- - result |> as_card() + result |> as_card(check = FALSE) } # this function calculates either the counts or the rates of the events diff --git a/R/ard_strata.R b/R/ard_strata.R index a4686eed6..99af6d6dc 100644 --- a/R/ard_strata.R +++ b/R/ard_strata.R @@ -111,6 +111,6 @@ ard_strata <- function(.data, .by = NULL, .strata = NULL, .f, ...) { # unnest ard data frame and return final table ------------------------------- df_nested_data |> tidyr::unnest(cols = all_of("ard")) |> - as_card() |> + as_card(check = FALSE) |> tidy_ard_column_order(group_order = "descending") } diff --git a/R/ard_summary.R b/R/ard_summary.R index 11e3a6563..a0800f024 100644 --- a/R/ard_summary.R +++ b/R/ard_summary.R @@ -127,7 +127,7 @@ ard_summary.data.frame <- function(data, # return empty ARD if no variables selected ---------------------------------- if (is_empty(variables)) { - return(dplyr::tibble() |> as_card()) + return(dplyr::tibble() |> as_card(check = FALSE)) } @@ -187,7 +187,7 @@ ard_summary.data.frame <- function(data, dplyr::mutate(context = "summary") |> tidy_ard_column_order() |> tidy_ard_row_order() |> - as_card() + as_card(check = FALSE) } diff --git a/R/ard_tabulate.R b/R/ard_tabulate.R index cad6d3dc2..010f15d97 100644 --- a/R/ard_tabulate.R +++ b/R/ard_tabulate.R @@ -145,7 +145,7 @@ ard_tabulate.data.frame <- function(data, # return empty ARD if no variables selected ---------------------------------- if (is_empty(variables)) { - return(dplyr::tibble() |> as_card()) + return(dplyr::tibble() |> as_card(check = FALSE)) } # return note about column names that result in errors ----------------------- @@ -218,7 +218,7 @@ ard_tabulate.data.frame <- function(data, dplyr::mutate(context = "tabulate") |> tidy_ard_column_order() |> tidy_ard_row_order() |> - as_card() + as_card(check = FALSE) } diff --git a/R/ard_tabulate_value.R b/R/ard_tabulate_value.R index 6e10ff605..a428ec297 100644 --- a/R/ard_tabulate_value.R +++ b/R/ard_tabulate_value.R @@ -70,7 +70,7 @@ ard_tabulate_value.data.frame <- function(data, # return empty ARD if no variables selected ---------------------------------- if (is_empty(variables)) { - return(dplyr::tibble() |> as_card()) + return(dplyr::tibble() |> as_card(check = FALSE)) } # calculate summary statistics ----------------------------------------------- diff --git a/R/as_card.R b/R/as_card.R index d81dcedf7..d0bdc18ec 100644 --- a/R/as_card.R +++ b/R/as_card.R @@ -2,9 +2,11 @@ #' #' Convert data frames to ARDs of class 'card'. #' +#' #' @param x (`data.frame`)\cr #' a data frame -#' +#' @param check (scalar `logical`)\cr +#' Whether the input data frame should be checked for standard ARD features #' @return an ARD data frame of class 'card' #' @export #' @@ -14,17 +16,36 @@ #' stat_label = c("N", "Mean"), #' stat = c(10, 0.5) #' ) |> +#' as_card(check = FALSE) +#' dplyr::tibble( +#' variable = "AGE", +#' stat_name = c("N", "mean"), +#' stat_label = c("N", "Mean"), +#' stat = list(10, 0.5), +#' fmt_fun = replicate(2, list()), +#' warning = replicate(2, list()), +#' error = replicate(2, list()) +#' ) |> #' as_card() -as_card <- function(x) { +as_card <- function(x, check = TRUE) { set_cli_abort_call() # check in inputs ------------------------------------------------------------ check_class(x, cls = "data.frame") + check_scalar_logical(check) # convert to class "card" ---------------------------------------------------- if (inherits(x, "card")) { - x + out <- x } else { - structure(x, class = c("card", class(x))) + out <- structure(x, class = c("card", class(x))) + } + if (check) { + check_ard_structure(out, + column_order = FALSE, method = FALSE, + error_on_fail = TRUE + ) } + + out } diff --git a/R/bind_ard.R b/R/bind_ard.R index 30112c93d..b42304093 100644 --- a/R/bind_ard.R +++ b/R/bind_ard.R @@ -99,5 +99,5 @@ bind_ard <- function(..., .distinct = TRUE, .update = FALSE, .order = FALSE, .qu } # return stacked ARDs -------------------------------------------------------- - tidy_ard_column_order(data) |> as_card() + tidy_ard_column_order(data) |> as_card(check = FALSE) } diff --git a/R/check_ard_structure.R b/R/check_ard_structure.R index 43bc50f6c..c6b137e1f 100644 --- a/R/check_ard_structure.R +++ b/R/check_ard_structure.R @@ -9,6 +9,8 @@ #' check whether ordering of columns adheres to to `cards::tidy_ard_column_order()`. #' @param method (scalar `logical`)\cr #' check whether a `"stat_name"` equal to `"method"` appears in results. +#' @param error_on_fail (scalar `logical`)\cr +#' Error if a check is failed? FALSE by default. #' @return an ARD data frame of class 'card' (invisible) #' @export #' @@ -16,18 +18,24 @@ #' ard_summary(ADSL, variables = "AGE") |> #' dplyr::select(-warning, -error) |> #' check_ard_structure() -check_ard_structure <- function(x, column_order = TRUE, method = TRUE) { +check_ard_structure <- function(x, column_order = TRUE, method = TRUE, + error_on_fail = FALSE) { set_cli_abort_call() check_scalar_logical(method) check_scalar_logical(column_order) + check_scalar_logical(error_on_fail) # check class ---------------------------------------------------------------- if (!inherits(x, "card")) { - cli::cli_inform("Object is not of class {.cls card}.") + .message_or_error("Object is not of class {.cls card}.", error_on_fail) } # exit if not a data frame --------------------------------------------------- if (!inherits(x, "data.frame")) { + .message_or_error( + "Object is not of class {.cls data.frame}.", + error = error_on_fail + ) return(invisible()) } @@ -39,27 +47,38 @@ check_ard_structure <- function(x, column_order = TRUE, method = TRUE) { ) |> setdiff(names(x)) if (!is_empty(missing_variables)) { - cli::cli_inform("The following columns are not present: {.val {missing_variables}}.") + .message_or_error( + "The following columns are not present: {.val {missing_variables}}.", + error = error_on_fail + ) } # check whether AR contains a method stat ------------------------------------ if (isTRUE(method)) { if (!"method" %in% x$stat_name) { - cli::cli_inform("Expecting a row with {.code stat_name = 'method'}, but it is not present.") + .message_or_error( + "Expecting a row with {.code stat_name = 'method'}, but it is not present.", + error = error_on_fail + ) } } # check order of columns ----------------------------------------------------- if (isTRUE(column_order)) { if (!identical(names(x), names(tidy_ard_column_order(x)))) { - cli::cli_inform( - c("The column order is not in the standard order.", + .message_or_error( + c( + "The column order is not in the standard order.", i = "Use {.fun cards::tidy_ard_column_order} for standard ordering." - ) + ), + error = error_on_fail ) } } + # Check whether expected columns are present --------------------------------- + + # check columns are list columns as expected --------------------------------- expected_lst_columns <- dplyr::select( @@ -67,14 +86,17 @@ check_ard_structure <- function(x, column_order = TRUE, method = TRUE) { any_of(c("stat", "fmt_fun", "warning", "error")) ) |> # remove group## and variable columns - dplyr::select(-matches("^group[0-9]$"), -"variable") |> + dplyr::select(-matches("^group[0-9]$"), -any_of("variable")) |> names() not_a_lst_columns <- x[expected_lst_columns] |> dplyr::select(-where(is.list)) |> names() if (!is_empty(not_a_lst_columns)) { - cli::cli_inform("The following columns are expected to be list columns: {.val {not_a_lst_columns}}.") + .message_or_error( + "The following columns are expected to be list columns: {.val {not_a_lst_columns}}.", + error = error_on_fail + ) } invisible(x) diff --git a/R/compare_ard.R b/R/compare_ard.R index e2e9d48a3..31ba22363 100644 --- a/R/compare_ard.R +++ b/R/compare_ard.R @@ -1,9 +1,14 @@ #' Compare ARDs #' #' @description +#' `r lifecycle::badge('experimental')`\cr #' `compare_ard()` compares columns of two ARDs row-by-row using a shared set #' of key columns. Rows where the column values differ are returned. #' +#' The `is_ard_equal()` function accepts a `compare_ard()` +#' object, and returns `TRUE` or `FALSE` depending on whether the comparison +#' reported difference. `check_ard_equal()` returns as error if not equal. +#' #' @param x (`card`)\cr #' first ARD to compare. #' @param y (`card`)\cr @@ -12,7 +17,7 @@ #' columns identifying unique records. The intersection of the selected #' columns in both ARDs is used. Default is #' `c(all_ard_groups(), all_ard_variables(), any_of(c("variable", "variable_level", "stat_name")))`. -#' @param compare ([`tidy-select`][dplyr::dplyr_tidy_select])\cr +#' @param columns ([`tidy-select`][dplyr::dplyr_tidy_select])\cr #' columns to compare between the two ARDs. Default is #' `any_of(c("stat_label", "stat", "stat_fmt"))`. #' @param tolerance (`numeric(1)`)\cr @@ -33,20 +38,24 @@ #' `difference` column with the `all.equal()` description for rows where #' values differ #' -#' @export +#' @name compare_ard #' #' @examples #' base <- ard_summary(ADSL, by = ARM, variables = AGE) -#' compare <- ard_summary(dplyr::mutate(ADSL, AGE = AGE + 1), -#' by = ARM, -#' variables = AGE) +#' compare <- ard_summary(dplyr::mutate(ADSL, AGE = AGE + 1), +#' by = ARM, +#' variables = AGE +#' ) #' #' compare_ard(base, compare)$compare$stat -#' +NULL + +#' @name compare_ard +#' @export compare_ard <- function(x, y, keys = c(all_ard_groups(), all_ard_variables(), any_of(c("variable", "variable_level", "stat_name"))), - compare = any_of(c("stat_label", "stat", "stat_fmt")), + columns = any_of(c("stat_label", "stat", "stat_fmt")), tolerance = sqrt(.Machine$double.eps), check.attributes = TRUE) { set_cli_abort_call() @@ -56,23 +65,61 @@ compare_ard <- function(x, # process keys and compare arguments ----------------------------------------- keys <- .process_keys_arg(x, y, keys = {{ keys }}) - compare <- .process_compare_arg(x, y, compare = {{ compare }}) + columns <- .process_compare_arg(x, y, columns = {{ columns }}) # check for duplicates in keys ----------------------------------------------- .check_keys_unique(x, keys, arg_name = "x") .check_keys_unique(y, keys, arg_name = "y") # initialize results list ---------------------------------------------------- - results <- rlang::rep_named(c("rows_in_x_not_y", "rows_in_y_not_x"), list(NULL)) - results[["compare"]] <- rlang::rep_named(compare, list(NULL)) + results <- rlang::rep_named(c("keys", "columns", "rows_in_x_not_y", "rows_in_y_not_x"), list(NULL)) + results[["comparison"]] <- rlang::rep_named(columns, list(NULL)) + + # save keys and columns ------------------------------------------------------ + results[["keys"]] <- keys + results[["columns"]] <- columns # find rows present in one ARD but not the other ----------------------------- results[["rows_in_x_not_y"]] <- .compare_rows(x, y, keys) results[["rows_in_y_not_x"]] <- .compare_rows(y, x, keys) # compare columns and find mismatches ---------------------------------------- - results[["compare"]] <- .compare_columns(x, y, keys, compare, tolerance, check.attributes) + results[["comparison"]] <- .compare_columns(x, y, keys, columns, tolerance, check.attributes) # return results with class -------------------------------------------------- - structure(results, class = c("ard_comparison", class(results))) + structure(results, class = c("compare_ard", class(results))) +} + +#' @name compare_ard +#' @export +is_ard_equal <- function(x) { + # check input class ---------------------------------------------------------- + check_class(x, "compare_ard") + + # check if there are mismatches rows ----------------------------------------- + if (nrow(x[["rows_in_x_not_y"]]) > 0L || nrow(x[["rows_in_y_not_x"]]) > 0L) { + return(FALSE) + } + + # check comparison results --------------------------------------------------- + for (i in seq_along(x[["comparison"]])) { + if (nrow(x[["comparison"]][[i]]) > 0L) { + return(FALSE) + } + } + + # If not triggered earlier, then ARDs are equal + TRUE +} + +#' @name compare_ard +#' @export +check_ard_equal <- function(x) { + is_ard_equal <- is_ard_equal(x) + + if (isFALSE(is_ard_equal)) { + cli::cli_abort("ARDs are not equal.") + } + + invisible(TRUE) } diff --git a/R/compare_ard_helpers.R b/R/compare_ard_helpers.R index 95fb0d323..5751d1e9a 100644 --- a/R/compare_ard_helpers.R +++ b/R/compare_ard_helpers.R @@ -13,7 +13,7 @@ cards_select({{ keys }}, data = y) ) .check_not_empty(keys) - cli::cli_inform("The comparison {.arg keys} are {.val {keys}}.") + keys } @@ -26,14 +26,14 @@ #' @return character vector of column names to compare #' @keywords internal #' @noRd -.process_compare_arg <- function(x, y, compare) { - compare <- union( - cards_select({{ compare }}, data = x), - cards_select({{ compare }}, data = y) +.process_compare_arg <- function(x, y, columns) { + columns <- intersect( + cards_select({{ columns }}, data = x), + cards_select({{ columns }}, data = y) ) - .check_not_empty(compare) - cli::cli_inform("The comparison {.arg compare} columns are {.val {compare}}.") - compare + .check_not_empty(columns) + + columns } #' Check Argument is Not Empty @@ -64,7 +64,7 @@ #' @keywords internal #' @noRd .check_keys_unique <- function(data, keys, arg_name) { - if (anyDuplicated(dplyr::select(data, dplyr::all_of(keys))) > 0) { + if (anyDuplicated(data[keys]) > 0) { duplicated_keys <- .format_duplicate_keys(data, keys) cli::cli_abort( @@ -113,8 +113,8 @@ paste(formatted, collapse = ", ") }, character(1) - ) |> - paste0("- ", x = _) + ) %>% + paste0("- ", x = .) } #' Format a Single Key Value @@ -201,32 +201,28 @@ #' @noRd .compare_columns <- function(x, y, keys, compare, tolerance, check.attributes) { # select relevant columns - x_selected <- dplyr::select(x, dplyr::all_of(keys), dplyr::any_of(compare)) - y_selected <- dplyr::select(y, dplyr::all_of(keys), dplyr::any_of(compare)) + x <- x[c(keys, compare)] + y <- y[c(keys, compare)] # ensure all compare columns exist in both data frames for (column in compare) { - if (!column %in% names(x_selected)) { - x_selected[[column]] <- vector("list", nrow(x_selected)) + if (!column %in% names(x)) { + x[[column]] <- vector("list", nrow(x)) } - if (!column %in% names(y_selected)) { - y_selected[[column]] <- vector("list", nrow(y_selected)) + if (!column %in% names(y)) { + y[[column]] <- vector("list", nrow(y)) } } # perform inner join to compare only matching rows # perform inner join to compare only matching rows comparison <- dplyr::inner_join( - x_selected, - y_selected, + x, + y, by = keys, suffix = c(".x", ".y") - ) |> - unlist_ard_columns() - - # Remove "cards" class from the object - class(comparison) <- setdiff(class(comparison), "card") - + ) + # build mismatch data frame for each compare column lapply( stats::setNames(compare, compare), diff --git a/R/filter_ard_hierarchical.R b/R/filter_ard_hierarchical.R index 269b7c184..822aad7a9 100644 --- a/R/filter_ard_hierarchical.R +++ b/R/filter_ard_hierarchical.R @@ -304,7 +304,7 @@ filter_ard_hierarchical <- function(x, filter, var = NULL, keep_empty = FALSE, q # add overall stats - derive values if overall=FALSE if (!no_overall) { .df_overall <- .g |> - as_card() |> + as_card(check = FALSE) |> cards::rename_ard_groups_shift() .df_overall <- dplyr::left_join(.df_overall, x_overall, by = names(.df_overall)) } @@ -423,5 +423,5 @@ filter_ard_hierarchical <- function(x, filter, var = NULL, keep_empty = FALSE, q # if present, keep attributes at bottom of ARD if (has_attr) x <- dplyr::bind_rows(x, x_attr) - as_card(x) + as_card(x, check = FALSE) } diff --git a/R/mock.R b/R/mock.R index 24ca32dc9..58ff13a73 100644 --- a/R/mock.R +++ b/R/mock.R @@ -96,7 +96,7 @@ mock_categorical <- function(variables, # merge the by ARD and the primary variable ARD ------------------------------ merge(ard_by, ard_variables, by = NULL) |> - as_card() |> + as_card(check = FALSE) |> tidy_ard_row_order() |> tidy_ard_column_order() } @@ -154,7 +154,7 @@ mock_continuous <- function(variables, # merge the by ARD and the primary variable ARD ------------------------------ merge(ard_by, ard_variables, by = NULL) |> - as_card() |> + as_card(check = FALSE) |> tidy_ard_row_order() |> tidy_ard_column_order() } diff --git a/R/print.R b/R/print.R index 530bcda6f..17ca419cd 100644 --- a/R/print.R +++ b/R/print.R @@ -20,12 +20,16 @@ #' not used #' #' @return an ARD data frame of class 'card' (invisibly) -#' @export +#' @name print.card #' @keywords internal #' #' @examples #' ard_tabulate(ADSL, variables = AGEGR1) |> #' print() +NULL + +#' @export +#' @rdname print.card print.card <- function(x, n = NULL, columns = c("auto", "all"), n_col = 6L, ...) { set_cli_abort_call() @@ -137,3 +141,43 @@ print.card <- function(x, n = NULL, columns = c("auto", "all"), n_col = 6L, ...) } invisible(x) } + + +#' @export +#' @rdname print.card +print.compare_ard <- function(x, ...) { + # print comparison details --------------------------------------------------- + cli::cli_inform("The comparison {.arg keys} are {.val {x$keys}}.") + cli::cli_inform("The comparison columns are {.val {x$columns}}.") + + # print the mismatches rows -------------------------------------------------- + cli::cli_h1("Mis-matched Rows") + if (nrow(x$rows_in_x_not_y) == 0L) { + cli::cli_alert_success("No rows in {.arg x} that do not appear in {.arg y}.") + } else { + cli::cli_h3("Rows in {.arg x} that do not appear in {.arg y}.") + as.data.frame(x$rows_in_x_not_y) + } + if (nrow(x$rows_in_y_not_x) == 0L) { + cli::cli_alert_success("No rows in {.arg y} that do not appear in {.arg x}.") + } else { + cli::cli_h3("Rows in {.arg y} that do not appear in {.arg x}.") + as.data.frame(x$rows_in_y_not_x) |> print() + } + + # print comparison results --------------------------------------------------- + cli::cli_h1("Comparison Results") + for (i in seq_along(x$comparison)) { + if (nrow(x$comparison[[i]]) == 0L) { + cli::cli_alert_success("No differences found in column {.val {names(x$comparison[i])}}.") + next + } + cli::cli_alert_warning("Differences found in column {.val {names(x$comparison[i])}} for {.val {nrow(x$comparison[[i]])}} rows.") + as.data.frame(x$comparison[[i]]) |> + utils::head(n = 10) |> + print() + } + + # return input invisibly ----------------------------------------------------- + invisible(x) +} diff --git a/R/rename_ard_columns.R b/R/rename_ard_columns.R index 7a4dafc71..ca2cc4be3 100644 --- a/R/rename_ard_columns.R +++ b/R/rename_ard_columns.R @@ -16,6 +16,11 @@ #' where the `colname` element is available to inject into the string, #' e.g. `'Overall {colname}'` may resolve to `'Overall AGE'` for an AGE column. #' Default is `'{colname}'`. +#' @param fct_as_chr (scalar `logical`)\cr +#' When `TRUE`, factor elements will be converted to character before unlisting. +#' When the column being unlisted contains mixed types of classes, the +#' factor elements are often converted to the underlying integer value instead +#' of retaining the label. Default is `TRUE`. #' @param unlist `r lifecycle::badge("deprecated")` #' #' @return data frame @@ -37,7 +42,9 @@ #' unlist_ard_columns() rename_ard_columns <- function(x, columns = c(all_ard_groups("names"), all_ard_variables("names")), - fill = "{colname}", unlist = NULL) { + fill = "{colname}", + fct_as_chr = TRUE, + unlist = NULL) { # check inputs --------------------------------------------------------------- if (!missing(unlist)) { lifecycle::deprecate_warn( @@ -52,6 +59,8 @@ rename_ard_columns <- function(x, check_class(x, "card") process_selectors(x, columns = {{ columns }}) check_scalar(fill) + check_scalar_logical(fct_as_chr) + if (!is_empty(setdiff(columns, dplyr::select(x, all_ard_groups("names"), all_ard_variables("names")) |> names()))) { bad_columns <- setdiff(columns, dplyr::select(x, all_ard_groups("names"), all_ard_variables("names")) |> names()) @@ -125,6 +134,13 @@ rename_ard_columns <- function(x, dplyr::select(-"...ard_row_order...") |> dplyr::mutate( # replace NULL values with NA, then unlist - across(all_of(all_new_names), ~ map(., \(value) value %||% NA) |> unlist()) + across( + all_of(all_new_names), + ~ map(., \(value){ + if (isTRUE(fct_as_chr) && inherits(value, "factor")) value <- as.character(value) + value %||% NA + }) |> + unlist() + ) ) } diff --git a/R/sort_ard_hierarchical.R b/R/sort_ard_hierarchical.R index e228bbbfd..8560b3cc7 100644 --- a/R/sort_ard_hierarchical.R +++ b/R/sort_ard_hierarchical.R @@ -281,7 +281,7 @@ sort_ard_hierarchical <- function(x, sort = everything() ~ "descending") { .data$stat_name == sort_stat, # select statistic to sum if (!is_empty(ard_args$by)) .data$group1 %in% ard_args$by else TRUE, if (length(c(ard_args$by, ard_args$variables)) > 1) { - if (ard_args$variable[i] %in% ard_args$include & !cur_var %in% "variable") { + if (ard_args$variables[i] %in% ard_args$include & !cur_var %in% "variable") { # if current variable is in include, sum *only* summary rows for the current variable .data$variable %in% "..overall.." & if (!next_var %in% "variable") .data[[next_var]] %in% "..empty.." else TRUE diff --git a/R/tidy_as_ard.R b/R/tidy_as_ard.R index cfcc1e51e..c1e23ba94 100644 --- a/R/tidy_as_ard.R +++ b/R/tidy_as_ard.R @@ -117,5 +117,5 @@ tidy_as_ard <- function(lst_tidy, ) |> tidy_ard_column_order() |> tidy_ard_row_order() |> - as_card() + as_card(check = FALSE) } diff --git a/R/utils.R b/R/utils.R index 2436786f0..719b1be78 100644 --- a/R/utils.R +++ b/R/utils.R @@ -90,3 +90,36 @@ ret } + + +#' Message or error +#' +#' Either error or message depending on input. +#' +#' @param msg (scalar `character`)\cr +#' Error message +#' @param error (scalar `logical`)\cr +#' If this should produce an error or a warning. FALSE by default +#' @param envir (`environment`)\cr +#' Environment to evaluate the glue expressions in passed in `cli::cli_abort(message)`. +#' Default is `rlang::current_env()` +#' @inheritParams cli::cli_abort +#' @return Invisible NULL +#' @keywords internal +#' +#' @examples +#' \dontrun{ +#' cards:::.message_or_error("This will be a message", FALSE) +#' cards:::.message_or_error("This will be an error", TRUE) +#' } +.message_or_error <- function(msg, error = FALSE, call = get_cli_abort_call(), + envir = rlang::caller_env()) { + if (error) { + cli::cli_abort( + msg, + call = call, .envir = envir + ) + } else { + cli::cli_inform(msg, call = call, .envir = envir) + } +} diff --git a/_pkgdown.yml b/_pkgdown.yml index c7e16f7fb..31abb7737 100644 --- a/_pkgdown.yml +++ b/_pkgdown.yml @@ -88,6 +88,7 @@ reference: - get_ard_statistics - replace_null_statistic - rename_ard_groups + - compare_ard - subtitle: "Table Shells" contents: - mock diff --git a/inst/WORDLIST b/inst/WORDLIST index ed6c58b6c..cec3af2cc 100644 --- a/inst/WORDLIST +++ b/inst/WORDLIST @@ -26,6 +26,7 @@ ata cardx cli de +dplyr env esult ets diff --git a/man/as_card.Rd b/man/as_card.Rd index c35cee54f..927b8f956 100644 --- a/man/as_card.Rd +++ b/man/as_card.Rd @@ -4,11 +4,14 @@ \alias{as_card} \title{Data Frame as ARD} \usage{ -as_card(x) +as_card(x, check = TRUE) } \arguments{ \item{x}{(\code{data.frame})\cr a data frame} + +\item{check}{(scalar \code{logical})\cr +Whether the input data frame should be checked for standard ARD features} } \value{ an ARD data frame of class 'card' @@ -21,6 +24,16 @@ data.frame( stat_name = c("N", "mean"), stat_label = c("N", "Mean"), stat = c(10, 0.5) +) |> + as_card(check = FALSE) +dplyr::tibble( + variable = "AGE", + stat_name = c("N", "mean"), + stat_label = c("N", "Mean"), + stat = list(10, 0.5), + fmt_fun = replicate(2, list()), + warning = replicate(2, list()), + error = replicate(2, list()) ) |> as_card() } diff --git a/man/check_ard_structure.Rd b/man/check_ard_structure.Rd index 22a0ce4f6..18f4825e0 100644 --- a/man/check_ard_structure.Rd +++ b/man/check_ard_structure.Rd @@ -4,7 +4,12 @@ \alias{check_ard_structure} \title{Check ARD Structure} \usage{ -check_ard_structure(x, column_order = TRUE, method = TRUE) +check_ard_structure( + x, + column_order = TRUE, + method = TRUE, + error_on_fail = FALSE +) } \arguments{ \item{x}{(\code{data.frame})\cr @@ -15,6 +20,9 @@ check whether ordering of columns adheres to to \code{cards::tidy_ard_column_ord \item{method}{(scalar \code{logical})\cr check whether a \code{"stat_name"} equal to \code{"method"} appears in results.} + +\item{error_on_fail}{(scalar \code{logical})\cr +Error if a check is failed? FALSE by default.} } \value{ an ARD data frame of class 'card' (invisible) diff --git a/man/compare_ard.Rd b/man/compare_ard.Rd index c12d531e0..5e981bf4e 100644 --- a/man/compare_ard.Rd +++ b/man/compare_ard.Rd @@ -2,6 +2,8 @@ % Please edit documentation in R/compare_ard.R \name{compare_ard} \alias{compare_ard} +\alias{is_ard_equal} +\alias{check_ard_equal} \title{Compare ARDs} \usage{ compare_ard( @@ -9,10 +11,14 @@ compare_ard( y, keys = c(all_ard_groups(), all_ard_variables(), any_of(c("variable", "variable_level", "stat_name"))), - compare = any_of(c("stat_label", "stat", "stat_fmt")), + columns = any_of(c("stat_label", "stat", "stat_fmt")), tolerance = sqrt(.Machine$double.eps), check.attributes = TRUE ) + +is_ard_equal(x) + +check_ard_equal(x) } \arguments{ \item{x}{(\code{card})\cr @@ -26,7 +32,7 @@ columns identifying unique records. The intersection of the selected columns in both ARDs is used. Default is \code{c(all_ard_groups(), all_ard_variables(), any_of(c("variable", "variable_level", "stat_name")))}.} -\item{compare}{(\code{\link[dplyr:dplyr_tidy_select]{tidy-select}})\cr +\item{columns}{(\code{\link[dplyr:dplyr_tidy_select]{tidy-select}})\cr columns to compare between the two ARDs. Default is \code{any_of(c("stat_label", "stat", "stat_fmt"))}.} @@ -52,13 +58,20 @@ values differ } } \description{ +\ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#experimental}{\figure{lifecycle-experimental.svg}{options: alt='[Experimental]'}}}{\strong{[Experimental]}}\cr \code{compare_ard()} compares columns of two ARDs row-by-row using a shared set of key columns. Rows where the column values differ are returned. + +The \code{is_ard_equal()} function accepts a \code{compare_ard()} +object, and returns \code{TRUE} or \code{FALSE} depending on whether the comparison +reported difference. \code{check_ard_equal()} returns as error if not equal. } \examples{ base <- ard_summary(ADSL, by = ARM, variables = AGE) -compare <- ard_summary(dplyr::mutate(ADSL, AGE = AGE + 1), by = ARM, variables = AGE) +compare <- ard_summary(dplyr::mutate(ADSL, AGE = AGE + 1), + by = ARM, + variables = AGE +) compare_ard(base, compare)$compare$stat - } diff --git a/man/dot-message_or_error.Rd b/man/dot-message_or_error.Rd new file mode 100644 index 000000000..69e0ea22f --- /dev/null +++ b/man/dot-message_or_error.Rd @@ -0,0 +1,51 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/utils.R +\name{.message_or_error} +\alias{.message_or_error} +\title{Message or error} +\usage{ +.message_or_error( + msg, + error = FALSE, + call = get_cli_abort_call(), + envir = rlang::caller_env() +) +} +\arguments{ +\item{msg}{(scalar \code{character})\cr +Error message} + +\item{error}{(scalar \code{logical})\cr +If this should produce an error or a warning. FALSE by default} + +\item{call}{The execution environment of a currently running +function, e.g. \code{call = caller_env()}. The corresponding function +call is retrieved and mentioned in error messages as the source +of the error. + +You only need to supply \code{call} when throwing a condition from a +helper function which wouldn't be relevant to mention in the +message. + +Can also be \code{NULL} or a \link[rlang:topic-defuse]{defused function call} to +respectively not display any call or hard-code a code to display. + +For more information about error calls, see \ifelse{html}{\link[rlang:topic-error-call]{Including function calls in error messages}}{\link[rlang:topic-error-call]{Including function calls in error messages}}.} + +\item{envir}{(\code{environment})\cr +Environment to evaluate the glue expressions in passed in \code{cli::cli_abort(message)}. +Default is \code{rlang::current_env()}} +} +\value{ +Invisible NULL +} +\description{ +Either error or message depending on input. +} +\examples{ +\dontrun{ +cards:::.message_or_error("This will be a message", FALSE) +cards:::.message_or_error("This will be an error", TRUE) +} +} +\keyword{internal} diff --git a/man/print.card.Rd b/man/print.card.Rd index c1b276662..136fa2ae6 100644 --- a/man/print.card.Rd +++ b/man/print.card.Rd @@ -2,9 +2,12 @@ % Please edit documentation in R/print.R \name{print.card} \alias{print.card} +\alias{print.compare_ard} \title{Print} \usage{ \method{print}{card}(x, n = NULL, columns = c("auto", "all"), n_col = 6L, ...) + +\method{print}{compare_ard}(x, ...) } \arguments{ \item{x}{(\code{data.frame})\cr diff --git a/man/rename_ard_columns.Rd b/man/rename_ard_columns.Rd index caf736ed3..d9ec05d30 100644 --- a/man/rename_ard_columns.Rd +++ b/man/rename_ard_columns.Rd @@ -8,6 +8,7 @@ rename_ard_columns( x, columns = c(all_ard_groups("names"), all_ard_variables("names")), fill = "{colname}", + fct_as_chr = TRUE, unlist = NULL ) } @@ -29,6 +30,12 @@ where the \code{colname} element is available to inject into the string, e.g. \code{'Overall {colname}'} may resolve to \code{'Overall AGE'} for an AGE column. Default is \code{'{colname}'}.} +\item{fct_as_chr}{(scalar \code{logical})\cr +When \code{TRUE}, factor elements will be converted to character before unlisting. +When the column being unlisted contains mixed types of classes, the +factor elements are often converted to the underlying integer value instead +of retaining the label. Default is \code{TRUE}.} + \item{unlist}{\ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#deprecated}{\figure{lifecycle-deprecated.svg}{options: alt='[Deprecated]'}}}{\strong{[Deprecated]}}} } \value{ diff --git a/tests/testthat/_snaps/as_card.md b/tests/testthat/_snaps/as_card.md index ac14b2530..54697423a 100644 --- a/tests/testthat/_snaps/as_card.md +++ b/tests/testthat/_snaps/as_card.md @@ -1,8 +1,8 @@ -# as_card() works +# as_card() allows non card objects with check = FALSE Code as_card(data.frame(stat_name = c("N", "mean"), stat_label = c("N", "Mean"), - stat = c(10, 0.5))) + stat = c(10, 0.5)), check = FALSE) Message {cards} data frame: 2 x 3 Output @@ -18,3 +18,11 @@ Error in `as_card()`: ! The `x` argument must be class , not a string. +--- + + Code + as_card(nonliststat) + Condition + Error in `as_card()`: + ! The following columns are expected to be list columns: "stat". + diff --git a/tests/testthat/_snaps/check_ard_structure.md b/tests/testthat/_snaps/check_ard_structure.md index ec7547de3..9e0acb21c 100644 --- a/tests/testthat/_snaps/check_ard_structure.md +++ b/tests/testthat/_snaps/check_ard_structure.md @@ -9,3 +9,45 @@ Expecting a row with `stat_name = 'method'`, but it is not present. The following columns are expected to be list columns: "stat". +# check_ard_structure() does not error if the tested dataset has none of the expected variables + + Code + expect_no_error(check_ard_structure(data.frame(badname = 3))) + Message + Object is not of class . + The following columns are not present: "variable", "stat_name", "stat_label", "stat", "fmt_fun", "warning", and "error". + Expecting a row with `stat_name = 'method'`, but it is not present. + +# check_ard_structure() errors when flagged appropriately + + Code + check_ard_structure(data.frame(badname = 3), error_on_fail = TRUE) + Condition + Error in `check_ard_structure()`: + ! Object is not of class . + +--- + + Code + check_ard_structure(nolist, error_on_fail = TRUE) + Condition + Error in `check_ard_structure()`: + ! The following columns are expected to be list columns: "stat". + +--- + + Code + check_ard_structure(novariable, error_on_fail = TRUE) + Condition + Error in `check_ard_structure()`: + ! The following columns are not present: "variable". + +--- + + Code + check_ard_structure(wrongorder, error_on_fail = TRUE) + Condition + Error in `check_ard_structure()`: + ! The column order is not in the standard order. + i Use `cards::tidy_ard_column_order()` for standard ordering. + diff --git a/tests/testthat/test-ard_summary.R b/tests/testthat/test-ard_summary.R index 8aeae84ca..8597afaeb 100644 --- a/tests/testthat/test-ard_summary.R +++ b/tests/testthat/test-ard_summary.R @@ -26,7 +26,7 @@ test_that("ard_summary() works", { mtcars, variables = starts_with("xxxxx") ), - dplyr::tibble() |> as_card() + dplyr::tibble() |> as_card(check = FALSE) ) }) diff --git a/tests/testthat/test-ard_tabulate.R b/tests/testthat/test-ard_tabulate.R index 0d40a99bb..c85db4889 100644 --- a/tests/testthat/test-ard_tabulate.R +++ b/tests/testthat/test-ard_tabulate.R @@ -31,7 +31,7 @@ test_that("ard_tabulate() univariate", { mtcars, variables = starts_with("xxxxx") ), - dplyr::tibble() |> as_card() + dplyr::tibble() |> as_card(check = FALSE) ) # works for ordered factors diff --git a/tests/testthat/test-as_card.R b/tests/testthat/test-as_card.R index a42e256bc..0f019f755 100644 --- a/tests/testthat/test-as_card.R +++ b/tests/testthat/test-as_card.R @@ -1,11 +1,11 @@ -test_that("as_card() works", { +test_that("as_card() allows non card objects with check = FALSE", { expect_snapshot( data.frame( stat_name = c("N", "mean"), stat_label = c("N", "Mean"), stat = c(10, 0.5) ) |> - as_card() + as_card(check = FALSE) ) }) @@ -24,4 +24,36 @@ test_that("as_card() error catching works correctly", { as_card(), error = TRUE ) + nonliststat <- dplyr::tibble( + variable = "AGE", + stat_name = c("N", "mean"), + stat_label = c("N", "Mean"), + stat = c(10, 0.5), + fmt_fun = replicate(2, list()), + warning = replicate(2, list()), + error = replicate(2, list()) + ) + + expect_snapshot( + nonliststat |> + as_card(), + error = TRUE + ) +}) + +test_that("as_card() does not care about column order", { + badlyordered <- dplyr::tibble( + stat_label = c("N", "Mean"), + variable = "AGE", + stat_name = c("N", "mean"), + stat = list(10, 0.5), + fmt_fun = replicate(2, list()), + warning = replicate(2, list()), + error = replicate(2, list()) + ) + + expect_no_error( + badlyordered |> + as_card() + ) }) diff --git a/tests/testthat/test-check_ard_structure.R b/tests/testthat/test-check_ard_structure.R index 0b42f7485..e8bd49142 100644 --- a/tests/testthat/test-check_ard_structure.R +++ b/tests/testthat/test-check_ard_structure.R @@ -7,3 +7,65 @@ test_that("check_ard_structure() works", { check_ard_structure() ) }) + + +test_that("check_ard_structure() does not error if the tested dataset has none of the expected variables", { + expect_snapshot( + expect_no_error( + check_ard_structure(data.frame(badname = 3)) + ) + ) +}) + +test_that("check_ard_structure() errors when flagged appropriately", { + expect_snapshot( + check_ard_structure(data.frame(badname = 3), error_on_fail = TRUE), + error = TRUE + ) + + nolist <- dplyr::tibble( + variable = "AGE", + stat_name = c("method", "mean"), + stat_label = c("N", "Mean"), + stat = c(10, 0.5), + fmt_fun = replicate(2, list()), + warning = replicate(2, list()), + error = replicate(2, list()) + ) + nolist <- structure(nolist, class = c("card", class(nolist))) + + expect_snapshot( + check_ard_structure(nolist, error_on_fail = TRUE), + error = TRUE + ) + novariable <- dplyr::tibble( + stat_name = c("method", "mean"), + stat_label = c("N", "Mean"), + stat = list(10, 0.5), + fmt_fun = replicate(2, list()), + warning = replicate(2, list()), + error = replicate(2, list()) + ) + novariable <- structure(novariable, class = c("card", class(novariable))) + + expect_snapshot( + check_ard_structure(novariable, error_on_fail = TRUE), + error = TRUE + ) + + wrongorder <- dplyr::tibble( + variable = "AGE", + stat_name = c("method", "mean"), + stat_label = c("N", "Mean"), + fmt_fun = replicate(2, list()), + warning = replicate(2, list()), + stat = list(10, 0.5), + error = replicate(2, list()) + ) + wrongorder <- structure(wrongorder, class = c("card", class(wrongorder))) + + expect_snapshot( + check_ard_structure(wrongorder, error_on_fail = TRUE), + error = TRUE + ) +}) diff --git a/tests/testthat/test-compare_ard.R b/tests/testthat/test-compare_ard.R index 64ec41580..745ac8bd1 100644 --- a/tests/testthat/test-compare_ard.R +++ b/tests/testthat/test-compare_ard.R @@ -2,16 +2,15 @@ test_that("compare_ard identifies stat mismatches", { ard_base <- ard_summary(ADSL, variables = AGE) ard_modified <- ard_summary(dplyr::mutate(ADSL, AGE = AGE + 1), variables = AGE) - expect_message( - result <- compare_ard(ard_base, ard_modified), - "keys" + expect_silent( + result <- compare_ard(ard_base, ard_modified) ) - expect_s3_class(result, "ard_comparison") - expect_true("stat" %in% names(result$compare)) - expect_gt(nrow(result$compare$stat), 0L) + expect_s3_class(result, "compare_ard") + expect_true("stat" %in% names(result$comparison)) + expect_gt(nrow(result$comparison$stat), 0L) - mean_row <- result$compare$stat |> + mean_row <- result$comparison$stat |> dplyr::filter(stat_name == "mean") expect_equal(nrow(mean_row), 1L) @@ -21,9 +20,9 @@ test_that("compare_ard identifies stat mismatches", { test_that("compare_ard returns empty data frames when ARDs are identical", { ard <- ard_tabulate(ADSL, variables = AGEGR1) - expect_message(result <- compare_ard(ard, ard)) + expect_silent(result <- compare_ard(ard, ard)) - expect_s3_class(result, "ard_comparison") + expect_s3_class(result, "compare_ard") expect_equal(nrow(result$rows_in_x_not_y), 0L) expect_equal(nrow(result$rows_in_y_not_x), 0L) expect_true(all(vapply(result$compare, nrow, integer(1)) == 0L)) @@ -43,12 +42,12 @@ test_that("compare_ard supports custom keys", { ard <- ard_summary(ADSL, variables = AGE) ard_modified <- ard_summary(dplyr::mutate(ADSL, AGE = AGE + 1), variables = AGE) - expect_message( + expect_silent( result <- compare_ard(ard, ard_modified, keys = c("variable", "stat_name")) ) - expect_gt(nrow(result$compare$stat), 0L) - expect_true(all(c("variable", "stat_name") %in% names(result$compare$stat))) + expect_gt(nrow(result$comparison$stat), 0L) + expect_true(all(c("variable", "stat_name") %in% names(result$comparison$stat))) }) test_that("compare_ard supports custom compare columns", { @@ -58,12 +57,12 @@ test_that("compare_ard supports custom compare columns", { ard_modified <- ard ard_modified$stat_label[1] <- "Modified Label" - expect_message( - result <- compare_ard(ard, ard_modified, compare = "stat_label") + expect_silent( + result <- compare_ard(ard, ard_modified, columns = "stat_label") ) - expect_true("stat_label" %in% names(result$compare)) - expect_equal(nrow(result$compare$stat_label), 1L) + expect_true("stat_label" %in% names(result$comparison)) + expect_equal(nrow(result$comparison$stat_label), 1L) }) test_that("compare_ard handles ARDs with different grouping structures", { @@ -74,20 +73,17 @@ test_that("compare_ard handles ARDs with different grouping structures", { ard_without_group <- ard_summary(ADSL, variables = AGE) # Should use intersection of keys - expect_message( - result <- compare_ard(ard_with_group, ard_without_group) + expect_error( + compare_ard(ard_with_group, ard_without_group), + "argument cannot be empty" ) - - expect_s3_class(result, "ard_comparison") - # Keys should not include group1/group1_level since they're not in both - expect_false("group1" %in% names(result$compare$stat)) }) test_that("compare_ard detects rows in x not in y", { ard_base <- ard_summary(ADSL, variables = AGE) ard_subset <- ard_base |> dplyr::filter(stat_name != "mean") - expect_message(result <- compare_ard(ard_base, ard_subset)) + expect_silent(result <- compare_ard(ard_base, ard_subset)) expect_equal(nrow(result$rows_in_x_not_y), 1L) expect_equal(result$rows_in_x_not_y$stat_name, "mean") @@ -98,7 +94,7 @@ test_that("compare_ard detects rows in y not in x", { ard_base <- ard_summary(ADSL, variables = AGE) ard_subset <- ard_base |> dplyr::filter(stat_name != "mean") - expect_message(result <- compare_ard(ard_subset, ard_base)) + expect_silent(result <- compare_ard(ard_subset, ard_base)) expect_equal(nrow(result$rows_in_x_not_y), 0L) expect_equal(nrow(result$rows_in_y_not_x), 1L) @@ -122,7 +118,7 @@ test_that("compare_ard errors when compare columns are empty", { # Error comes from cards_select, not our check expect_error( - suppressMessages(compare_ard(ard, ard, compare = "nonexistent_col")) + compare_ard(ard, ard, compare = "nonexistent_col") ) }) @@ -131,8 +127,8 @@ test_that("compare_ard handles missing columns gracefully", { ard_no_stat_fmt <- ard |> dplyr::select(-any_of("stat_fmt")) # stat_fmt not in either, but stat and stat_label are - expect_message( - result <- compare_ard(ard_no_stat_fmt, ard_no_stat_fmt, compare = any_of(c("stat", "stat_label"))) + expect_silent( + result <- compare_ard(ard_no_stat_fmt, ard_no_stat_fmt, columns = any_of(c("stat", "stat_label"))) ) expect_true(all(vapply(result$compare, nrow, integer(1)) == 0L)) @@ -144,11 +140,11 @@ test_that("compare_ard works with by variables", { # Modify one value ard2$stat[1] <- list(999L) - expect_message(result <- compare_ard(ard1, ard2)) + expect_silent(result <- compare_ard(ard1, ard2)) - expect_equal(nrow(result$compare$stat), 1L) - expect_true("group1" %in% names(result$compare$stat)) - expect_true("group1_level" %in% names(result$compare$stat)) + expect_equal(nrow(result$comparison$stat), 1L) + expect_true("group1" %in% names(result$comparison$stat)) + expect_true("group1_level" %in% names(result$comparison$stat)) }) test_that("compare_ard validates input classes", { @@ -156,28 +152,25 @@ test_that("compare_ard validates input classes", { expect_error(compare_ard(ard_summary(ADSL, variables = AGE), data.frame()), class = "check_class") }) -test_that("compare_ard prints informative messages about keys and compare columns", { +test_that("compare_ard returns compare_ard class", { ard <- ard_summary(ADSL, variables = AGE) - expect_message( - compare_ard(ard, ard), - regexp = "keys" - ) + expect_silent(result <- compare_ard(ard, ard)) - expect_message( - compare_ard(ard, ard), - regexp = "compare" - ) -}) - -test_that("compare_ard returns ard_comparison class", { - ard <- ard_summary(ADSL, variables = AGE) - - expect_message(result <- compare_ard(ard, ard)) - - expect_s3_class(result, "ard_comparison") + expect_s3_class(result, "compare_ard") expect_true("rows_in_x_not_y" %in% names(result)) expect_true("rows_in_y_not_x" %in% names(result)) - expect_true("compare" %in% names(result)) - expect_true(is.list(result$compare)) + expect_true("comparison" %in% names(result)) + expect_true(is.list(result$comparison)) +}) + +test_that("check_ard_equal() returns error with unequal ARDs", { + expect_error( + compare_ard( + ard_summary(ADSL[1:10, ], variables = AGE), + ard_summary(ADSL[1:20, ], variables = AGE) + ) |> + check_ard_equal(), + "ARDs are not equal" + ) }) diff --git a/tests/testthat/test-rename_ard_columns.R b/tests/testthat/test-rename_ard_columns.R index 67d097fe6..10261ae88 100644 --- a/tests/testthat/test-rename_ard_columns.R +++ b/tests/testthat/test-rename_ard_columns.R @@ -53,3 +53,36 @@ test_that("rename_ard_columns(unlist) lifecycle", { rename_ard_columns(unlist = "stat") ) }) + + +test_that("rename_ard_columns(fct_as_chr)", { + adsl_ <- ADSL |> + dplyr::mutate( + RACE = factor(RACE) + ) + + # check fct_to_chr = TRUE + res <- ard_tabulate( + data = adsl_, + by = TRT01A, + variables = c(RACE, ETHNIC) + ) |> + rename_ard_columns() + + # Check that 'race' is a character vector + expect_type(res$RACE, "character") + + # Check that it contains the actual level string, not an integer "1", "2", etc. + expect_true("WHITE" %in% res$RACE) + + # check fct_to_chr = FALSE + res <- ard_tabulate( + data = adsl_, + by = TRT01A, + variables = c(RACE, ETHNIC) + ) |> + rename_ard_columns(fct_as_chr = FALSE) + + # Check that 'race' is an interger vector + expect_type(res$RACE, "integer") +}) diff --git a/tests/testthat/test-utils.R b/tests/testthat/test-utils.R new file mode 100644 index 000000000..c44fd835d --- /dev/null +++ b/tests/testthat/test-utils.R @@ -0,0 +1,8 @@ +test_that(".warn_or_error errors/warns depending on input", { + expect_message( + .message_or_error("something", FALSE) + ) + expect_error( + .message_or_error("something", TRUE) + ) +}) diff --git a/vignettes/articles/.gitignore b/vignettes/articles/.gitignore new file mode 100644 index 000000000..9e2bd63c1 --- /dev/null +++ b/vignettes/articles/.gitignore @@ -0,0 +1,4 @@ +*.html +*.R + +/.quarto/ diff --git a/vignettes/articles/update-ard.Rmd b/vignettes/articles/update-ard.Rmd new file mode 100644 index 000000000..2ddad0a53 --- /dev/null +++ b/vignettes/articles/update-ard.Rmd @@ -0,0 +1,140 @@ +--- +title: "Updating ARD Objects" +--- + +```{r, include = FALSE} +knitr::opts_chunk$set( + collapse = TRUE, + comment = "#>" +) +``` + +```{r setup, echo = FALSE, warning = FALSE, message = FALSE} +library(cards) +library(dplyr) +``` + +## Introduction + +ARD (Analysis Results Data) objects are data frames that contain statistical summaries. Because they are data frames, any value in the ARD can be updated using standard dplyr or base R functions. + +### Working with List Columns + +ARD objects contain **list columns** such as `stat`, `fmt_fun`, `stat_label`, and others. List columns are columns where each cell can hold any R object—a number, a function, a vector, or even another data frame. If you're unfamiliar with list columns, they may look unusual when printed: + +```{r} +ard <- ard_summary(ADSL, variables = AGE) |> + select(variable, stat_name, stat, fmt_fun) |> + head(3) +print(ard) +``` + +While list columns can be updated just like any other column in a data frame, the syntax can be less intuitive. For example, to update the `fmt_fun` column for specific statistics, you might write: +```{r} +ard |> + mutate( + fmt_fun = ifelse(stat_name %in% c("mean", "sd"), list(2L), fmt_fun) + ) +``` + +This requires wrapping values in `list()` and can become cumbersome for more complex updates. + +### Helper Functions + +To simplify working with these list columns, {cards} provides helper functions: + +- **`update_ard_fmt_fun()`**: Update formatting functions for specific statistics +- **`update_ard_stat_label()`**: Update statistic labels + +These functions handle the list column mechanics for you, making it easier to customize your ARD objects. + +## Basic Usage + +### Updating Formatting Functions + +By default, statistics may use simple formatting. You can update the formatting function for specific statistics: + +```{r} +# Create a basic ARD +ard <- ard_summary(ADSL, variables = AGE) + +# Update formatting for mean and sd to show more decimal places +ard_updated <- ard |> + update_ard_fmt_fun( + stat_names = c("mean", "sd"), + fmt_fun = 2L # 2 decimal places + ) |> + apply_fmt_fun() + +# View results +ard_updated |> + select(stat_name, stat, stat_fmt) +``` + +### Updating Statistic Labels + +Combine formatting updates with custom labels: + +```{r} +ard_summary(ADSL, variables = AGE) |> + update_ard_fmt_fun(stat_names = c("mean", "sd"), fmt_fun = 1L) |> + update_ard_stat_label( + stat_names = c("mean", "sd"), + stat_label = "Mean (SD)" + ) |> + apply_fmt_fun() +``` + +## Selective Updates + +### Filtering by Variable + +Update formatting for specific variables only: + +```{r} +ard_summary(ADSL, variables = c(AGE, BMIBL)) |> + update_ard_fmt_fun( + variables = AGE, # Only update AGE + stat_names = "mean", + fmt_fun = 3L + ) |> + apply_fmt_fun() +``` + +### Filtering by Group + +When working with stratified analyses, use the `filter` argument to target specific groups: + +```{r} +# Update formatting only for the Placebo arm +ard_summary( + ADSL, + by = ARM, + variables = AGE, + statistic = ~ continuous_summary_fns(c("N", "mean")) +) |> + update_ard_fmt_fun( + stat_names = "mean", + fmt_fun = 3L, + filter = group1_level == "Placebo" + ) |> + apply_fmt_fun() +``` + +## Custom Formatting Functions + +Beyond integer aliases, you can pass custom functions: + +```{r} +# Custom formatter that adds parentheses +format_with_parens <- function(x) { + paste0("(", format(round(x, 1), nsmall = 1), ")") +} + +ard_summary(ADSL, variables = AGE) |> + update_ard_fmt_fun( + stat_names = "sd", + fmt_fun = format_with_parens + ) |> + apply_fmt_fun() +```