From 858a4553f0887e9d17eeda21cda0922824d98cd1 Mon Sep 17 00:00:00 2001 From: kallea03 Date: Sat, 4 Jan 2025 20:41:46 +0000 Subject: [PATCH 01/48] latest updates from tlfcarver --- R/adae_risk_summary.R | 115 ++++++++++--- R/adlb_r301.R | 74 ++++++--- R/adsl_r001.R | 116 ++++++++----- R/ae_forestplot.R | 17 +- R/ae_pre_processor.R | 85 ++++++++-- R/ae_volcano_plot.R | 7 +- R/bar_plot.R | 9 +- R/dataset_merge.R | 1 + R/edish_plot.R | 106 ++++++------ R/event_analysis.R | 76 ++++----- R/forest_plot.R | 17 +- R/graph_utils.R | 84 +++++----- R/line_plot.R | 2 +- R/mcatstat.R | 170 ++++++++++++------- R/mentry.R | 45 ++++-- R/msumstat.R | 117 +++++++++----- R/occ_tier_summary.R | 350 ++++++++++++++++++++++++++++++++++------ R/process_vx_bar_plot.R | 15 +- R/ptly_utils.R | 8 +- R/risk_stat.R | 237 ++++++++++++++------------- R/riskdiff_wald.R | 117 -------------- R/scatter_plot.R | 34 ++-- R/surv_utils.R | 26 ++- R/tbl_display.R | 104 +++++++++--- R/tornado_plot.R | 115 +++++++------ R/utils.R | 129 ++++++++++++--- R/vx_boxplot.R | 221 ------------------------- 27 files changed, 1402 insertions(+), 995 deletions(-) delete mode 100644 R/riskdiff_wald.R delete mode 100644 R/vx_boxplot.R diff --git a/R/adae_risk_summary.R b/R/adae_risk_summary.R index 81ccf62..a2794fa 100644 --- a/R/adae_risk_summary.R +++ b/R/adae_risk_summary.R @@ -11,13 +11,19 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +# #' ADAE Summary with Risk Statistics #' #' @inheritParams risk_stat #' @param hterm High Level Adverse Event term variable, used for analysis #' @param lterm Low Level Adverse Event term variable, used for analysis +#' @param risklabels List containing labels for table with elements: risk, riskci, p, low, up, lowup +#' @param sum_row To show summary/any term row or not. 'Y'/'N' +#' @param sum_row_label Label for Summary Row to be displayed, if Y. +#' @param sigdec_cat Number of decimal places for % displayed in output #' -#' @return List of summarized data frames for Adverse Events based on high and lower term. +#' @return Data frame to be displayed with risk/counts of higher and lower AE terms +#' @export #' #' @examples #' data(adae) @@ -50,7 +56,7 @@ #' trtgrp = "Xanomeline Low Dose", #' statistics = "Risk Ratio", #' alpha = 0.05, -#' cutoff = 5, +#' cutoff_where = "PCT > 5", #' sort_opt = "Ascending", #' sort_var = "Count" #' ) @@ -67,11 +73,19 @@ adae_risk_summary <- function(datain, ctrlgrp, trtgrp, statistics = "Risk Ratio", + riskdiff_pct = "N", alpha = 0.05, - cutoff = 5, + cutoff_where = NA, sort_opt = "Ascending", - sort_var = "Count") { - stopifnot("Input data is empty" = nrow(datain) > 0) + sort_var = "Count", + sum_row = "N", + sum_row_label = "Participants with Any AE", + risklabels = tbl_risk_labels(), + sigdec_cat = 1, + pctsyn = "Y") { + if (nrow(datain) < 1) { + return(datain) + } stopifnot(length(ctrlgrp) == 1) stopifnot("Invalid Control Group" = ctrlgrp %in% unique(datain[["TRTVAR"]])) stopifnot( @@ -80,18 +94,56 @@ adae_risk_summary <- function(datain, ) stopifnot( "Invalid Risk Statistics; specify any one of `Risk Ratio` or `Risk Difference`" = - statistics %in% c("Risk Ratio", "Risk Difference") + tolower(statistics) %in% c("risk ratio", "risk difference") ) stopifnot( "`byvar` in `mentry()` cannot be `NA` or ''" = - identical(var_start(datain, "BYVAR"), "BYVAR1") + "BYVAR1" %in% var_start(datain, "BYVAR") ) - stopifnot( - "`byvar` in `mentry()` should be identical to `hterm` in `adae_summary()" = - identical(unique(datain[["BYVAR1"]]), unique(datain[[hterm]])) + # Get low term and apply cutoff + ae_lsumm <- risk_stat( + datain = datain, + a_subset = a_subset, + summary_by = summary_by, + eventvar = lterm, + ctrlgrp = ctrlgrp, + trtgrp = trtgrp, + statistics = statistics, + alpha = alpha, + cutoff_where = cutoff_where, + sort_opt = sort_opt, + sort_var = sort_var, + riskdiff_pct = riskdiff_pct, + hoveryn = "N", + sigdec = sigdec_cat, + pctsyn = pctsyn ) - - ae_summ <- map(c(hterm, lterm), \(term) { + if (nrow(ae_lsumm) == 0 || ncol(ae_lsumm) == 1) { + return(ae_lsumm) + } else { + ae_lsumm <- ae_lsumm |> + mutate(DPTVAR = lterm, CN = "C") + } + # Apply if CUTOFF exists + if (!is.na(cutoff_where) && str_detect(cutoff_where, "PCT|FREQ")) { + datain <- datain |> + left_join(select(ae_lsumm, all_of(c("BYVAR1", "CUTFL")), {{ lterm }} := "DPTVAL"), + by = c("BYVAR1", lterm) + ) + a_subset <- paste(na.omit(c(a_subset, "CUTFL == 'Y'")), collapse = "&") + } + # If ANy AE row: + if (sum_row == "Y") { + h_terms <- c(hterm, "TIER") + } else { + h_terms <- hterm + } + ae_hsumm <- map(h_terms, \(term) { + if (term == "TIER") { + datain <- datain |> + select(-starts_with("BYVAR")) |> + mutate(TIER = sum_row_label) + } risk_stat( datain = datain, a_subset = a_subset, @@ -101,23 +153,42 @@ adae_risk_summary <- function(datain, trtgrp = trtgrp, statistics = statistics, alpha = alpha, - cutoff = cutoff, sort_opt = sort_opt, - sort_var = sort_var + sort_var = sort_var, + riskdiff_pct = riskdiff_pct, + hoveryn = "N", + sigdec = sigdec_cat, + pctsyn = pctsyn ) |> mutate(DPTVAR = term, CN = "C") }) |> - set_names(c("hterm_summ", "lterm_summ")) - + set_names(h_terms) + ## retrun empty flextable - if (nrow(ae_summ[["hterm_summ"]]) < 1) { - return(data.frame("Note" = "No data available under these conditions")) + if (nrow(ae_hsumm[[1]]) < 1) { + return(data.frame()) } - - ae_summ |> + list(ae_hsumm[[1]], ae_lsumm) |> post_occ_tier( riskyn = "Y", ctrlgrp = ctrlgrp, - statistics = statistics - ) + risklabels = risklabels, + sum_row = ae_hsumm[["TIER"]] + ) |> + mutate(DPTVAR = "TIER") +} + +#' Labels for AE risk table +#' +#' @return list of labels +#' @export +tbl_risk_labels <- function() { + list( + riskci = "Risk Ratio (CI)", + p = "P-value", + risk = "Risk Ratio", + low = "Lower Limit", + up = "Upper Limit", + lowup = "(Lower-Upper)" + ) } diff --git a/R/adlb_r301.R b/R/adlb_r301.R index 6c0a824..893a192 100644 --- a/R/adlb_r301.R +++ b/R/adlb_r301.R @@ -1,11 +1,24 @@ +# Copyright 2024 Pfizer Inc +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# #' Incidence of Laboratory Test Abnormalities (Without Regard to Baseline Abnormality) #' #' @param datain Input dataset (`adlb`). #' @param crit_vars Criteria variables -#' @param pctdisp Denominator to calculate percentages by. +#' @param stathead Column label to display `n` in the output. Default is `n (%)` #' Values: `"TRT", "VAR","COL", "SUBGRP", "SGRPN", "CAT", "NONE", "NO", "DPTVAR"` -#' @param a_subset Subset conditions for analysis of dependent variable. -#' @param denom_subset Subset conditions for denominator eg. `"APSBLFL == 'Y'"` +#' @inheritParams mcatstat #' #' @return `data.frame` with summary of laboratory abnormality incidence counts #' @export @@ -32,12 +45,11 @@ #' crit_vars = "CRIT3~CRIT4", #' pctdisp = "SUBGRP", #' a_subset = NA_character_, -#' denom_subset = NA_character_ +#' denom_subset = NA_character_, +#' sigdec = 1 #' ) |> #' display_bign_head(mentry_data = lb_entry) |> -#' tbl_processor( -#' dptlabel = "" -#' ) +#' tbl_processor() #' #' out #' @@ -52,15 +64,21 @@ lab_abnormality_summary <- function(datain, crit_vars = "CRIT3~CRIT4", pctdisp = "SUBGRP", a_subset = NA_character_, - denom_subset = NA_character_) { + denom_subset = NA_character_, + sigdec = 2, + sparseyn = "Y", + pctsyn = "N", + stathead = "n (%)") { # Data checks and error messages - stopifnot(is.data.frame(datain) && nrow(datain) > 0) + if (nrow(datain) < 1) { + return(datain) + } dptvars <- toupper(str_to_vec(crit_vars)) dptvars_fl <- glue("{dptvars}FL") byvars <- var_start(datain, "BYVAR") byvarsN <- glue("{byvars}N") stopifnot("Criteria Variables/Flags not present in `datain`" = all(dptvars %in% names(datain)) || - all(dptvars_fl %in% names(datain))) + all(dptvars_fl %in% names(datain))) # handle denom_subset when not specified if (is.na(denom_subset) || str_squish(denom_subset) == "") { if ("APSBLFL" %in% names(datain)) { @@ -80,11 +98,11 @@ lab_abnormality_summary <- function(datain, # Replace missing values numeric equivalent grouping variables with 0 mutate(across(any_of(byvarsN), ~ replace_na(., 0))) # Calculate lab abnormalities by Criteria Flags - seq_along(dptvars) |> + out_data <- seq_along(dptvars) |> map(\(dptval) { asubset <- glue("{dptvars_fl[dptval]} == 'Y'") if (!is.na(a_subset) && - str_squish(a_subset) != "") { + str_squish(a_subset) != "") { asubset <- glue("{a_subset} & {asubset}") } ## add lab abnormality counts @@ -94,14 +112,23 @@ lab_abnormality_summary <- function(datain, dsubset, toupper(byvars), dptvars[[dptval]], - pctdisp + pctdisp, + sigdec, + sparseyn, + pctsyn, + dptval ) }) |> # combine and display lab abnormality table - bind_rows() |> + bind_rows() + # Data check: + if (nrow(out_data) == 0) { + return(data.frame()) + } + out_data |> mutate(across(c("DENOMN", "CVALUE"), as.character)) |> - rename(N = DENOMN, n = CVALUE) |> - pivot_longer(c("N", "n"), names_to = "SUBGRPVARX", values_to = "CVALUE") |> + rename(N = DENOMN, !!stathead := CVALUE) |> + pivot_longer(c("N", stathead), names_to = "SUBGRPVARX", values_to = "CVALUE") |> mutate(SUBGRPVARXN = 9999) } @@ -118,7 +145,14 @@ count_abnormalities <- denom_subset, byvars, dptvars, - pctdisp) { + pctdisp, + sigdec, + sparseyn, + pctsyn, + dptvarn) { + if (nrow(datain) == 0) { + return(data.frame()) + } crit_df <- datain |> filter(.data[[dptvars]] != "") |> @@ -132,7 +166,11 @@ count_abnormalities <- a_subset = a_subset, denom_subset = denom_subset, dptvar = "DPTVAR", + dptvarn = dptvarn, pctdisp = pctdisp, - pctsyn = "N" + pctsyn = pctsyn, + sigdec = sigdec, + return_zero = "Y", + sparseyn = sparseyn ) } diff --git a/R/adsl_r001.R b/R/adsl_r001.R index a4de465..c52f7cd 100644 --- a/R/adsl_r001.R +++ b/R/adsl_r001.R @@ -18,16 +18,13 @@ #' @param vars Names of `adsl` variables to display (Add `"-S"` to numeric variables), #' tilde-separated #' @param stat_vars Statistics to display in table for numeric vars, tilde-separated. -#' @param pctdisp Denominator to calculate percentages by. -#' Values: `"TRT", "VAR", "COL", "SUBGRP", "SGRPN", "CAT", "NONE", "NO", "DPTVAR"`. -#' @param total_catyn To return a 'Total' row for categorical analysis in `vars`. Values: `"Y"/"N"` -#' @param total_catlabel Label for total category row. eg- "All"/"Total" -#' @param miss_catyn To include empty/blank values as `miss_catlabel` in categories of -#' `dptvar` variable or not. Values: `"Y"/"N"` -#' @param miss_catlabel Label for missing values #' @param a_subset Analysis Subset condition; tilde-separated for each variable in `vars`. #' @param denom_subset Subset condition to be applied to dataset for calculating denominator, #' tilde-separated for categorical variables within `vars`. +#' @param sigdec_stat Number of base decimal places to retain in output of summary statistic. +#' Applies to mean, min, max, sd etc +#' @param sigdec_cat Number of decimal places for % displayed in output +#' @inheritParams mcatstat #' #' @details #' \itemize{ @@ -36,7 +33,7 @@ #' eg. for `"AGEGR1/AGEGR1N~AGE-S~SEX/SEXN~BMIBL-S"`, `AGEGR1` and `SEX` will be analysed by #' category and `AGE` and `BMIBL` as summary statistics. #' \item Argument `stat_vars` should contain names of statistic to apply to all summary analysis -#' variables. +#' variables. `sigdec` applies only to statistical analysis of numeric variables (-S) #' \item Arguments `pctdisp`, `total_catyn`, `miss_catyn`, `miss_catlabel` apply to all variables #' under categorical analyses. #' \item `a_subset` should tilde-separated subset conditions, corresponding to each variable in @@ -76,8 +73,25 @@ #' adsl_sum |> #' display_bign_head(mentry_data = mentry_df) |> #' tbl_processor( +#' statlabel = "N~Range~Meansd~Median~Q1Q3", +#' dptlabel = "Age Group~_NONE_~Sex~Race", +#' addrowvar = "DPTVAR" +#' ) |> +#' tbl_display() |> +#' flextable::autofit() +#' +#' # Same variable with 2 unique subset conditions +#' adsl_sum <- mentry_df |> +#' adsl_summary( +#' vars = "AGEGR1~AGE-S~SEX~SEX~RACE", +#' a_subset = "AGE<65~AGE>80~SEX=='F'~SEX=='M'~NA" +#' ) +#' +#' adsl_sum |> +#' display_bign_head(mentry_data = mentry_df) |> +#' tbl_processor( #' statlabel = "N~Range~Meansd~Median~IQR", -#' dptlabel = "Age Group~NONE~Sex~Race", +#' dptlabel = "Age Group~_NONE_~Sex1~Sex2~Race", #' addrowvar = "DPTVAR" #' ) |> #' tbl_display() |> @@ -159,13 +173,13 @@ #' adsl_vs |> #' adsl_summary( #' vars = "SEX~AGE-S~AGEGR1~RACE~ETHNIC~HEIGHT-S~WEIGHT-S~BMI-S", -#' stat_vars = "medianrange~meansd" +#' stat_vars = "median(minmax)~mean(sd)" #' ) |> #' display_bign_head(adsl_vs) |> #' tbl_processor( #' dptlabel = "Sex, n(%)~Age (Years)~Age Category (Years), n(%)~Race, n(%)~Ethnicity, #' n(%)~Height (cm)~Weight (kg)~BMI (kg/m2)", -#' statlabel = "Median (Range)~Mean (SD)", +#' statlabel = "Median (Min, Max)~Mean (SD)", #' addrowvars = "DPTVAR" #' ) |> #' tbl_display() |> @@ -174,63 +188,77 @@ #' adsl_summary <- function(datain, vars, - stat_vars = "N~Range~Meansd~Median~IQR", + stat_vars = "n~minmaxc~mean(sd)~median~q1q3", pctdisp = "TRT", total_catyn = "N", total_catlabel = "Total", miss_catyn = "N", miss_catlabel = "Missing", + pctsyn = "Y", + sigdec_stat = 2, + sigdec_cat = 2, a_subset = NA_character_, - denom_subset = NA_character_) { - stopifnot(nrow(datain) > 0) + denom_subset = NA_character_, + sparseyn = "N", + sparsebyvalyn = "N") { + if (nrow(datain) == 0) { + return(datain) + } vars <- split_var_types(toupper(str_to_vec(vars))) stat_vars <- str_to_vec(stat_vars) # mapping `a_subset` to all variables, `denom_subset` to only categorical variables map_df <- map_var_subsets( - list(vars[["all_vars"]], vars[["cat_vars"]]), + vars[["all_vars"]], list(a_subset, denom_subset) ) + cat_df <- map_df |> + filter(!.data$vars %in% .env$vars[["num_vars"]]) + num_df <- map_df |> + filter(.data$vars %in% .env$vars[["num_vars"]]) # analysis for categorical variables - datacat <- map(vars[["cat_vars"]], \(x) { - if (!str_to_vec(x, "/")[1] %in% names(datain)) { - cat_df <- data.frame() + datacat <- map(seq_along(cat_df$vars), \(x) { + if (!str_to_vec(cat_df$vars[x], "/")[1] %in% names(datain)) { + cat_sums <- data.frame() } else { - vars_df <- map_df |> - filter(.data$vars == x) - cat_df <- mcatstat( + cat_sums <- mcatstat( datain = datain, - a_subset = vars_df[["subset1"]], - denom_subset = vars_df[["subset2"]], - dptvar = x, + a_subset = cat_df[["subset1"]][x], + denom_subset = cat_df[["subset2"]][x], + dptvar = cat_df[["vars"]][x], uniqid = "USUBJID", pctdisp = pctdisp, total_catyn = total_catyn, miss_catyn = miss_catyn, miss_catlabel = miss_catlabel, - dptvarn = which(vars[["all_vars"]] == x) + dptvarn = cat_df[["ord"]][x], + sigdec = sigdec_cat, + pctsyn = pctsyn, + sparseyn = sparseyn, + sparsebyvalyn = sparsebyvalyn, + return_zero = "Y" ) } - cat_df + cat_sums }) |> bind_rows() |> - select(-any_of(c("XVAR", "FREQ", "PCT"))) + select(-any_of(c("XVAR", "FREQ", "PCT", "CPCT"))) # analysis for numeric variables if (length(vars[["num_vars"]]) > 0) { datasums <- map( - vars[["num_vars"]], + seq_along(num_df$vars), \(x) { - if (!x %in% names(datain)) { + if (!num_df[["vars"]][x] %in% names(datain)) { df <- data.frame() } else { - vars_df <- map_df |> - filter(.data$vars == x) df <- msumstat( datain = datain, - a_subset = vars_df[["subset1"]], - dptvar = x, + a_subset = num_df[["subset1"]][x], + dptvar = num_df[["vars"]][x], statvar = stat_vars, - dptvarn = which(vars[["all_vars"]] == x) + dptvarn = num_df[["ord"]][x], + sigdec = sigdec_stat, + sparsebyvalyn = sparsebyvalyn )[["tsum"]] } df @@ -251,11 +279,11 @@ adsl_summary <- function(datain, #' @noRd #' split_var_types <- function(vars) { - num_vars <- vars[stringr::str_which(vars, "-S")] - + num_vars <- vars[str_which(vars, "-S")] + list( num_vars = str_replace_all(num_vars, "-S", ""), - cat_vars = setdiff(vars, num_vars), + cat_vars = vars[!vars %in% num_vars], all_vars = str_replace_all(vars, "-S", "") ) } @@ -269,10 +297,9 @@ split_var_types <- function(vars) { #' @noRd #' map_var_subsets <- function(varlist, subsetlist) { - map(seq_along(varlist), \(i) { - vars <- varlist[[i]] - var_len <- length(vars) - subset <- str_replace_all(str_to_vec(subsetlist[[i]]), "NA", NA_character_) + sub <- map(seq_along(subsetlist), \(i) { + var_len <- length(varlist) + subset <- dplyr::na_if(str_to_vec(subsetlist[[i]]), "NA") # repeat subset for all variables if only one subset is given if (length(subset) == 1) { subset <- rep(subset, var_len) @@ -281,7 +308,8 @@ map_var_subsets <- function(varlist, subsetlist) { "Number of subsets should be 1 or equal to number of corresponding variables" = var_len == length(subset) ) - bind_cols(vars = vars, !!paste0("subset", i) := subset) - }) |> - reduce(left_join, by = "vars") + bind_cols(!!paste0("subset", i) := subset) |> + mutate(ord = row_number()) + }) + bind_cols(vars = varlist, reduce(sub, left_join, by = "ord")) } diff --git a/R/ae_forestplot.R b/R/ae_forestplot.R index 4386792..c29eecf 100644 --- a/R/ae_forestplot.R +++ b/R/ae_forestplot.R @@ -90,7 +90,7 @@ #' trtgrp = "Xanomeline High Dose", #' statistics = "Risk Ratio", #' alpha = 0.05, -#' cutoff = 5, +#' cutoff_where = "FREQ >5", #' sort_opt = "Ascending", #' sort_var = "Count" #' ) |> @@ -134,7 +134,9 @@ ae_forest_plot <- interactive = "N") { # Common processing for all plots: datain <- datain |> - filter(!is.nan(.data[["RISK"]]), !is.infinite(.data[["RISK"]])) + group_by(across(all_of(c("DPTVAL", "TRTPAIR")))) |> + filter(!any(.data[["FREQ"]] == 0)) |> + ungroup() # Check risk data exists: stopifnot("Input ae_forest_plot data is empty" = nrow(datain) != 0) # If axis position not added, default it: @@ -153,8 +155,8 @@ ae_forest_plot <- per_page <- split( events, rep(seq_along(events), - each = terms_perpg, - length.out = length(events) + each = terms_perpg, + length.out = length(events) ) ) } else { @@ -178,7 +180,7 @@ ae_forest_plot <- dat_out <- dat_out |> mutate( YCAT = ifelse(.data[["BYVAR1"]] == lead(.data[["BYVAR1"]], default = "last"), - "", .data[["BYVAR1"]] + "", .data[["BYVAR1"]] ), XVAR = "HT" ) @@ -271,15 +273,14 @@ ae_forest_hlt_sig <- function(plotin, TRUE ~ NA_character_ )) |> filter(!is.na(.data[["EFFECT"]])) - + plotin + geom_point( data = hltpts, aes( x = .data[["PCT"]], y = .data[["DPTVAL"]], - fill = .data[["EFFECT"]], - key = .data[["key"]] + fill = .data[["EFFECT"]] ), inherit.aes = FALSE, shape = 23, diff --git a/R/ae_pre_processor.R b/R/ae_pre_processor.R index 6f69404..d6a92d9 100644 --- a/R/ae_pre_processor.R +++ b/R/ae_pre_processor.R @@ -21,9 +21,19 @@ #' Permissible Values: "ANY", "ANY EVENT", "TREATMENT EMERGENT", "SERIOUS", #' "DRUG-RELATED", "RELATED", "MILD", "MODERATE", "SEVERE", "RECOVERED/RESOLVED", #' "RECOVERING/RESOLVING", "NOT RECOVERING/NOT RESOLVING", "FATAL", "GRADE N" +#' @param subset Analysis subset condition to be applied to `ADAE` dataset prior to ADSL join; +#' will be appended to `ae_filter` #' @param obs_residual If not NA, use this argument to pass a period (numeric) to extend the #' observation period. If passed as NA, overall study duration is considered for analysis. #' eg. if 5, only events occurring upto 5 days past the TRTEDT are considered. +#' @param max_sevctc If needed to filter maximum severity/ctc grade rows. Values: NA/"SEV"/"CTC" +#' @param sev_ctcvar Variable to determine max severity. eg: ASEVN, ATOXGRN +#' @param hterm High Level Event Term (req for max Sev tables only) +#' @param lterm Low Level Event Term (req for max Sev tables only) +#' @param rpt_byvar Page/report by variable if any, to identify max sev/ctc +#' @param trtvar Treatment Variable +#' @param pt_total Required to calculate total of preferred terms? Y/N +#' #' @return : a list containing 2 objects #' \itemize{ #' \item data - Processed dataframe output for further utilities (pass to `mentry()`) @@ -49,8 +59,18 @@ ae_pre_processor <- function(datain, fmq_data = NULL, date_vars = c("ASTDT", "AENDT", "TRTSDT", "TRTEDT"), ae_filter = "Any Event", - obs_residual = NA_real_) { - stopifnot("Empty Data Frame passed" = nrow(datain) != 0) + subset = NA, + obs_residual = NA_real_, + max_sevctc = NA_character_, + sev_ctcvar = "ASEVN", + hterm = "AEBODSYS", + lterm = "AEDECOD", + rpt_byvar = character(0), + trtvar = "TRTA", + pt_total = "N") { + if (nrow(datain) == 0) { + return(list(data = datain, a_subset = NA_character_)) + } # Processing FMQ values if exists if (is.data.frame(fmq_data)) { fmq <- fmq_data |> @@ -65,7 +85,7 @@ ae_pre_processor <- function(datain, mutate(PT = str_trim(toupper(.data[["AEDECOD"]]))) |> left_join(fmq, by = "PT") } - + # Standardizing date format to common format data_pro <- datain |> mutate(across( @@ -78,7 +98,7 @@ ae_pre_processor <- function(datain, if ("ASTDT" %in% names(data_pro) && any(is.na(data_pro[["AEDECOD"]]))) { data_pro <- data_pro |> mutate(AEDECOD = if_else(!is.na(.data[["ASTDT"]]) & is.na(.data[["AEDECOD"]]), - "Not Yet Coded", .data[["AEDECOD"]] + "Not Yet Coded", .data[["AEDECOD"]] )) } # AE-Specific filter conditions @@ -86,7 +106,9 @@ ae_pre_processor <- function(datain, data_pro, ae_filter ) - + if (!is.na(subset)) { + filters <- paste(na.omit(c(filters, subset)), collapse = " & ") + } # filter for events occurring in given observation period obs_residual <- as.numeric(obs_residual) if (!is.na(obs_residual) && obs_residual >= 0) { @@ -94,12 +116,57 @@ ae_pre_processor <- function(datain, "Obs period cannot be used; dates unavailable" = all(c("ASTDT", "TRTSDT", "TRTEDT") %in% names(data_pro)) ) - filters <- c(filters, glue("(ASTDT > TRTSDT) & (ASTDT < (TRTEDT + {obs_residual}))")) |> - na.omit() |> - paste(collapse = " & ") + filters <- paste(na.omit( + c(filters, glue("(ASTDT > TRTSDT) & (ASTDT < (TRTEDT + {obs_residual}))")) + ), collapse = " & ") + } + + # Apply AE filters if exist: + if (!is.na(filters) && filters != "") { + data_pro <- data_pro |> + filter(!!!parse_exprs(filters)) + if (nrow(data_pro) < 1) { + return(list(data = data_pro, a_subset = filters)) + } + } + ################### Max SEV/CTC############## + # If maximum severity or CTC required: + # Filter analysis dataset and also flag max variable + # Any AE flag to be set + # Flag for high level term and max sevc/ctc + if (!is.na(max_sevctc) && toupper(max_sevctc) %in% c("SEV", "CTC")) { + data_pro <- data_pro |> + group_by(across( + any_of(c(rpt_byvar, trtvar, "USUBJID", hterm, lterm)) + )) |> + mutate( + MAX_SEVCTC = ifelse(.data[[sev_ctcvar]] == max(.data[[sev_ctcvar]], na.rm = TRUE), 1, 0) + ) |> + filter(MAX_SEVCTC == 1) |> + group_by(across(any_of(c(rpt_byvar, trtvar, "USUBJID")))) |> + mutate( + ANY = ifelse(.data[[sev_ctcvar]] == max(.data[[sev_ctcvar]], na.rm = TRUE), 1, 0) + ) |> + group_by(across( + any_of(c(rpt_byvar, trtvar, "USUBJID", hterm)) + )) |> + mutate( + HT_FL = ifelse(.data[[sev_ctcvar]] == max(.data[[sev_ctcvar]], na.rm = TRUE), 1, 0) + ) + # For preferred term total count + if (toupper(max_sevctc) == "SEV" && pt_total == "Y") { + data_pro <- data_pro |> + group_by(across(any_of(c(rpt_byvar, trtvar, lterm, "USUBJID")))) |> + mutate( + PT_CNT = ifelse(.data[[sev_ctcvar]] == max(.data[[sev_ctcvar]], na.rm = TRUE), 1, 0) + ) + } + filters <- paste(na.omit(c(filters, "MAX_SEVCTC == 1")), collapse = " & ") } + ################### ENDax SEV/CTC############## + # Return processed dataframe and filter conditions - return(list(data = data_pro, a_subset = filters)) + return(list(data = ungroup(data_pro), a_subset = filters)) } #' Create filter condition for Adverse Events from keyword diff --git a/R/ae_volcano_plot.R b/R/ae_volcano_plot.R index be246ab..ea11712 100644 --- a/R/ae_volcano_plot.R +++ b/R/ae_volcano_plot.R @@ -66,7 +66,7 @@ #' trtgrp = "Xanomeline High Dose", #' statistics = "Risk Ratio", #' alpha = 0.05, -#' cutoff = 5, +#' cutoff_where = "FREQ >5", #' sort_opt = "Ascending", #' sort_var = "Count" #' ) @@ -116,12 +116,11 @@ ae_volcano_plot <- function(datain, x = .data[["RISK"]], y = .data[["PVALUE"]], text = .data[["HOVER_TEXT"]], - fill = .data[["BYVAR1"]], - key = .data[["key"]] + fill = .data[["BYVAR1"]] ) ) + # color code by SOC geom_point(aes(size = .data[["CTRL_N"]]), pch = 21, alpha = 0.5) - + # Error when there are no adjusted p-values <= 0.05 so remove FDR adjusted P when no # adjusted p <= p value cutoff check_sig <- datain |> diff --git a/R/bar_plot.R b/R/bar_plot.R index f7d568f..67de73a 100644 --- a/R/bar_plot.R +++ b/R/bar_plot.R @@ -46,7 +46,8 @@ #' adsl_sum <- msumstat( #' datain = adsl_entry, #' dptvar = "AGE", -#' statvar = "mean" +#' statvar = "mean", +#' figyn = "Y" #' )[["gsum"]] |> #' plot_display_bign(adsl_entry) |> #' dplyr::mutate( @@ -94,7 +95,7 @@ bar_plot <- function(datain, # Remove empty rows datain <- datain |> mutate(YVAR = as.numeric(YVAR)) - + # Bar plot: # Legend Labels if based on other variable: series_labels <- series_leg_lab(datain, series_var, series_labelvar) @@ -110,7 +111,7 @@ bar_plot <- function(datain, values = series_opts$color, labels = series_labels ) - + if (bar_pos == "stacked") { g_plot <- g_plot + geom_bar(stat = "identity", width = bar_width) @@ -140,7 +141,7 @@ bar_plot <- function(datain, y = axis_opts$yaxis_label ) + theme_std(axis_opts, legend_opts, griddisplay) - + # Rotate plot if needed: if (flip_plot == "Y") { g_plot <- g_plot + coord_flip() diff --git a/R/dataset_merge.R b/R/dataset_merge.R index bdb5f0c..8eba0b5 100644 --- a/R/dataset_merge.R +++ b/R/dataset_merge.R @@ -11,6 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +# #' Merge Datasets #' #' @param ... Datasets to be merged. diff --git a/R/edish_plot.R b/R/edish_plot.R index 62031cf..184738a 100644 --- a/R/edish_plot.R +++ b/R/edish_plot.R @@ -1,3 +1,17 @@ +# Copyright 2024 Pfizer Inc +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# #' Process data for eDISH plot #' #' @param datain Input dataset. @@ -9,17 +23,17 @@ #' @param alt_paramcd `PARAMCD` value for `ALANINE AMINOTRANSFERASE` in `datain`. #' @param ast_paramcd `PARAMCD` value for `ASPARTATE AMINOTRANSFERASE` in `datain`. #' @param bili_paramcd `PARAMCD` value for `BILIRUBIN` in `datain`. +#' @param legendbign (`string`) Display BIGN in Legend (`Y/N`). #' #' @return A `data.frame` required for `edish_plot`. #' @export #' #' @examples -#' data("adlb") -#' data("adsl") +#' data("lab_data") #' #' merged_data <- adsl_merge( -#' adsl = adsl, -#' dataset_add = adlb +#' adsl = lab_data$adsl, +#' dataset_add = lab_data$adlb #' ) |> #' mentry( #' subset = "SAFFL == 'Y'", @@ -30,25 +44,31 @@ #' merged_data |> #' process_edish_data( #' xvar = "both", -#' alt_paramcd = "ALT", -#' ast_paramcd = "AST", -#' bili_paramcd = "BILI" +#' alt_paramcd = "L00030S", +#' ast_paramcd = "L00028S", +#' bili_paramcd = "L00021S" #' ) #' process_edish_data <- function(datain, xvar = "both", alt_paramcd = "L00030S", ast_paramcd = "L00028S", - bili_paramcd = "L00021S") { - stopifnot(is.data.frame(datain)) + bili_paramcd = "L00021S", + legendbign = "Y") { + if (!is.data.frame(datain) || nrow(datain) == 0) { + return(data.frame()) + } stopifnot("`xvar` must be one of 'alt', 'ast' or 'both'" = xvar %in% c("alt", "ast", "both")) stopifnot( "Please provide valid PARAMCD" = all(c(alt_paramcd, ast_paramcd, bili_paramcd) %in% datain$PARAMCD) ) - + hy_data <- datain |> - filter(.data$PARAMCD %in% c(alt_paramcd, ast_paramcd, bili_paramcd)) |> + filter( + .data$PARAMCD %in% c(alt_paramcd, ast_paramcd, bili_paramcd), + !is.na(.data$ANRHI) + ) |> mutate(maxv = .data$AVAL / .data$ANRHI) |> mutate( PARM = case_when( @@ -57,7 +77,7 @@ process_edish_data <- function(datain, TRUE ~ "bili" ) ) - + hy <- hy_data |> group_by(across(all_of(c("USUBJID", "TRTVAR", "PARAMCD", "PARAM", "PARM")))) |> summarise(x = max(.data$maxv)) |> @@ -66,13 +86,13 @@ process_edish_data <- function(datain, names_from = PARM, values_from = x ) - + if (xvar %in% c("alt", "ast")) { hy <- hy |> mutate(XVAR = .data[[xvar]]) } else { hy <- hy |> mutate(XVAR = pmax(.data$ast, .data$alt)) } - + hy |> mutate( text = paste0( @@ -80,7 +100,7 @@ process_edish_data <- function(datain, USUBJID, "\n", ifelse(xvar == "both", "Max of ALT/AST = ", - paste("value of", toupper(xvar), "=") + paste("value of", toupper(xvar), "=") ), round(XVAR, 3), "\n", @@ -89,7 +109,7 @@ process_edish_data <- function(datain, ), YVAR = .data[["bili"]] ) |> - na.omit() + plot_display_bign(datain, bignyn = legendbign) } #' eDISH Plot @@ -113,12 +133,11 @@ process_edish_data <- function(datain, #' @export #' #' @examples -#' data("adsl") -#' data("adlb") +#' data("lab_data") #' #' merged_data <- adsl_merge( -#' adsl = adsl, -#' dataset_add = adlb +#' adsl = lab_data$adsl, +#' dataset_add = lab_data$adlb #' ) |> #' mentry( #' subset = "SAFFL == 'Y'", @@ -129,9 +148,9 @@ process_edish_data <- function(datain, #' pt_data <- process_edish_data( #' datain = merged_data, #' xvar = "both", -#' alt_paramcd = "ALT", -#' ast_paramcd = "AST", -#' bili_paramcd = "BILI" +#' alt_paramcd = "L00028S", +#' ast_paramcd = "L00030S", +#' bili_paramcd = "L00021S" #' ) #' #' series_opts <- plot_aes_opts(pt_data, @@ -143,9 +162,9 @@ process_edish_data <- function(datain, #' datain = pt_data, #' axis_opts = plot_axis_opts( #' xlinearopts = list( -#' breaks = c(0.1, 1, 2, 10), -#' limits = c(0.1, 10), -#' labels = c("0.1", "1", "2x ULN", "10") +#' breaks = c(0.1, 1, 2, 5), +#' limits = c(0.1, 5), +#' labels = c("0.1", "1", "2x ULN", "5") #' ), #' ylinearopts = list( #' breaks = c(0.1, 1, 3, 10), @@ -196,24 +215,7 @@ edish_plot <- function(datain, dir = "horizontal" ), interactive = "N") { - stopifnot(is.data.frame(datain)) - - ### Modified plot options #### - if (length(axis_opts$Xbrks) > 0 && length(axis_opts$Xlims) > 0) { - axis_opts$Xlims[2] <- max(ceiling(max(datain$XVAR)), axis_opts$Xlims) - axis_opts$Xbrks[which.max(axis_opts$Xbrks)] <- min( - ceiling(max(datain$XVAR)), - max(axis_opts$Xbrks) - ) - } - - if (length(axis_opts$Ybrks) > 0 && length(axis_opts$Ylims) > 0) { - axis_opts$Ylims[2] <- max(ceiling(max(datain$XVAR)), axis_opts$Ylims) - axis_opts$Ybrks[which.max(axis_opts$Ybrks)] <- min( - ceiling(max(datain$XVAR)), - max(axis_opts$Ybrks) - ) - } + stopifnot(is.data.frame(datain) && nrow(datain) > 0) # plot and options # setting labels for each quadrants quad_labels <- str_to_vec(quad_labels) @@ -228,9 +230,9 @@ edish_plot <- function(datain, max(as.numeric(yrefline[1]), max(axis_opts$Ybrks)), as.numeric(yrefline[1]) - 0.2 ) - + # for ploting values per subject - + sp <- datain |> scatter_plot( axis_opts = axis_opts, @@ -252,16 +254,6 @@ edish_plot <- function(datain, color = xrefline[2], linetype = xrefline[3] ) + - geom_hline( - yintercept = 1, - color = "grey30", - linetype = "solid" - ) + - geom_vline( - xintercept = 1, - color = "grey30", - linetype = "solid" - ) + # Add annotations to graph annotate( geom = "text", x = quad_labels_opts_x[1], @@ -286,7 +278,7 @@ edish_plot <- function(datain, y = quad_labels_opts_y[4], label = quad_labels[4] ) - + # ggplotly if interactive if (interactive == "Y") { sp <- as_plotly(plot = sp, hover = c("text")) diff --git a/R/event_analysis.R b/R/event_analysis.R index a942a82..e15454c 100644 --- a/R/event_analysis.R +++ b/R/event_analysis.R @@ -40,17 +40,12 @@ #' ) #' #' ## prepare data for plot -#' prep_entry <- prep_ae[["data"]] |> -#' mentry( -#' trtvar = "TRTA", -#' trtsort = "TRTAN", -#' trttotalyn = "N", -#' byvar = "FMQ_NAM" -#' ) -#' ## prepare data for plot -#' prep_event_analysis <- prep_entry |> +#' prep_event_analysis <- prep_ae[["data"]] |> #' process_event_analysis( #' a_subset = glue::glue("AOCCPFL == 'Y' & {prep_ae$a_subset}"), +#' trtvar = "TRTA", +#' trtsort = "TRTAN", +#' trttotalyn = "Y", #' summary_by = "Events", #' hterm = "FMQ_NAM", #' ht_val = "ABDOMINAL PAIN", @@ -132,7 +127,7 @@ event_analysis_plot <- query_plot <- query_plot + geom_hline(yintercept = ref_line, linetype = "dashed") + ggtitle(query_title) - + p <- cowplot::plot_grid( pt_plot, query_plot, @@ -145,7 +140,7 @@ event_analysis_plot <- event_plotly(ref_line, pt_title) query_plot <- query_plot |> event_plotly(ref_line, query_title) - + p <- subplot( pt_plot, query_plot, @@ -184,17 +179,14 @@ event_analysis_plot <- #' obs_residual = 0, #' fmq_data = FMQ_Consolidated_List #' ) -#' prep_entry <- prep_ae[["data"]] |> -#' mentry( -#' trtvar = "TRTA", -#' trtsort = "TRTAN", -#' trttotalyn = "N", -#' byvar = "FMQ_NAM" -#' ) +#' #' ## prepare data for plot -#' prep_event_analysis <- prep_entry |> +#' prep_event_analysis <- prep_ae[["data"]] |> #' process_event_analysis( #' a_subset = glue::glue("AOCCPFL == 'Y' & {prep_ae$a_subset}"), +#' trtvar = "TRTA", +#' trtsort = "TRTAN", +#' trttotalyn = "Y", #' summary_by = "Events", #' hterm = "FMQ_NAM", #' ht_val = "ABDOMINAL PAIN", @@ -210,6 +202,9 @@ event_analysis_plot <- process_event_analysis <- function(datain, a_subset = NA_character_, + trtvar, + trtsort = NA_character_, + trttotalyn = "N", summary_by = "Events", hterm, ht_val, @@ -224,42 +219,48 @@ process_event_analysis <- ) ht_val <- get_event_scope(hterm, ht_val, ht_scope) lt_val <- get_event_scope(lterm, lt_val, lt_scope, ht_val) - + ae_counts <- datain |> + mentry( + byvar = hterm, + trtvar = trtvar, + trtsort = trtsort, + trttotalyn = trttotalyn + ) |> mcatstat( a_subset = a_subset, uniqid = ifelse(toupper(summary_by) == "PATIENTS", "USUBJID", "ALLCT"), dptvar = lterm, - pctdisp = "TRT" + pctdisp = "TRT", + sparseyn = "N", + pctsyn = "Y" ) - + hl_summ <- ae_counts |> filter_events(hterm, ht_val, "BYVAR1") |> mutate( HTERM = hterm, HVAL = str_remove_all(ht_val, glue("/{toupper(ht_scope)}")), LVAL = lt_val, - Percent = paste(.data$PCT, "% \n Low Term:", .data$DPTVAL), - PCT_N = as.numeric(.data$PCT) + Percent = paste0(.data$CPCT, " \n Low Term:", .data$DPTVAL) ) |> group_by(.data$TRTVAR) |> mutate(DECODh = ifelse(str_detect( toupper(.data$DPTVAL), toupper(lt_val) ), 9999, rank(.data$PCT))) |> ungroup() - + stopifnot("No data available for higher terms" = nrow(hl_summ) > 0) - + ll_summ <- ae_counts |> filter_events(lterm, lt_val, "DPTVAL") |> mutate( - PCT_N = as.numeric(.data$PCT), - Percent = paste(.data$PCT, "%") + Percent = .data$CPCT ) |> - arrange(.data$TRTVAR, .data$PCT_N) - + arrange(.data$TRTVAR, .data$PCT) + stopifnot("No data available for preferred terms" = nrow(ll_summ) > 0) - + list(hl_summ, ll_summ) |> set_names(c("query_df", "pt_df")) |> map(\(x) { @@ -290,7 +291,7 @@ pt_plot <- df |> ggplot(aes( x = .data$TRTVAR, - y = .data$PCT_N, + y = .data$PCT, text = .data$Percent )) + geom_bar( @@ -332,13 +333,13 @@ query_plot <- unique(df[["LVAL"]]), pt_color ) - + df |> mutate(across("DPTVAL", ~ toupper(.x))) |> ggplot( aes( x = .data$TRTVAR, - y = .data$PCT_N, + y = .data$PCT, fill = reorder(.data$DPTVAL, -.data$DECODh), group = .data$DECODh, text = .data$Percent @@ -426,7 +427,7 @@ get_event_scope <- function(var, val, scope, hval = NULL) { get_yscales <- function(df) { ymax <- df |> group_by(.data$TRTVAR) |> - summarise(pct_s = sum(.data$PCT_N)) |> + summarise(pct_s = sum(.data$PCT)) |> ungroup() |> filter(.data$pct_s == max(.data$pct_s, na.rm = TRUE)) |> mutate( @@ -434,7 +435,7 @@ get_yscales <- function(df) { .keep = "none" ) |> pull() - + list(ymax = ymax, ybreak = ifelse(ymax > 40, 5, 2)) } @@ -480,8 +481,9 @@ event_plotly <- function(p, ref_line, title_text) { yref = "paper", xref = "paper", xanchor = "center", - yanchor = "bottom", + yanchor = "top", showarrow = FALSE, + yshift = 35, font = list(size = 12) ) } diff --git a/R/forest_plot.R b/R/forest_plot.R index 1f38f4f..6d2f080 100644 --- a/R/forest_plot.R +++ b/R/forest_plot.R @@ -72,8 +72,7 @@ forest_plot_base <- function(datain, xmin = .data[[xminvar]], xmax = .data[[xmaxvar]], text = .data[[hovervar]], - group = .data[[series_var]], color = .data[[series_var]], - key = .data[["key"]] + group = .data[[series_var]], color = .data[[series_var]] ) ) + ggstance::geom_errorbarh( @@ -169,8 +168,7 @@ forest_plot_scatter <- function(datain, color = .data[[series_var]], shape = .data[[series_var]], size = .data[[series_var]], - text = .data[[hovervar]], - key = .data[["key"]] + text = .data[[hovervar]] ) ) + geom_point() + @@ -216,7 +214,8 @@ forest_plot_scatter <- function(datain, #' @param plot_height Height of plotly output, if specifically required #' @param xpos Where should X xaxis for `splot` and `fplot` be displayed in interactive plot? #' Values: "top"/"bottom". Value for static output is decided prior to passing in this function. -#' +#' @param legend_opts Legend styling option, a `list` containing `pos`(position) and +#' `dir` (direction). #' @return plot_grid object or plotly forest plot object #' @export #' @@ -263,7 +262,8 @@ forest_display <- function(plot_list, rel_widths = c(0.25, 0.38, 0.27, 0.10), interactive = "N", plot_height = NULL, - xpos = "top") { + xpos = "top", + legend_opts = list(pos = "bottom", dir = "horizontal")) { stopifnot(all(c("splot", "fplot") %in% names(plot_list))) stopifnot( "rel_widths should be equal to the number of plot columns" = @@ -298,8 +298,9 @@ forest_display <- function(plot_list, plotly_legend(lg_pos = c(0.5, -0.2), dir = "h") combine_plot$x$source <- "plot_output" } else { - legend1 <- cowplot::get_legend(plot_list[["splot"]]) - legend2 <- cowplot::get_legend(plot_list[["fplot"]]) + legpattern <- paste0("guide-box-", trimws(legend_opts$pos)) + legend1 <- cowplot::get_plot_component(plot_list[["splot"]], pattern = legpattern) + legend2 <- cowplot::get_plot_component(plot_list[["fplot"]], pattern = legpattern) plot_list[["splot"]] <- plot_list[["splot"]] + theme(legend.position = "none") plot_list[["fplot"]] <- plot_list[["fplot"]] + theme(legend.position = "none") # Combine for grid ggplot output diff --git a/R/graph_utils.R b/R/graph_utils.R index 93c03b5..f73818f 100644 --- a/R/graph_utils.R +++ b/R/graph_utils.R @@ -21,7 +21,7 @@ #' @export #' #' @examples -#' library(carver) +#' library(tlfcarver) #' library(ggplot2) #' ggplot(data = mtcars, mapping = aes(x = mpg, y = hp)) + #' geom_point() + @@ -30,8 +30,8 @@ reverselog_trans <- function(base = exp(1)) { trans <- function(x) -log(x, base) inv <- function(x) base^(-x) scales::trans_new(paste0("reverselog-", format(base)), trans, inv, - scales::log_breaks(base = base), - domain = c(1e-100, Inf) + scales::log_breaks(base = base), + domain = c(1e-100, Inf) ) } @@ -63,10 +63,10 @@ g_seriescol <- function(gdata, "aquamarine1", "tan4", "skyblue1", "orchid3", "brown", "pink", "black" ) } - + # If not factor, convert it: if (!is.factor(gdata[[SERIESVAR]])) gdata[[SERIESVAR]] <- as.factor(gdata[[SERIESVAR]]) - + # Set names as factor levels levs <- levels(unique(gdata[[SERIESVAR]])) vals <- levs[levs %in% unique(gdata[[SERIESVAR]])] @@ -131,12 +131,12 @@ g_seriessym <- function(gdata, # Standard shapes shapelist <- c(16, 17, 15, 1, 18, 2, 0, 8, 10, 3, 4, 5) } - + # If not factor, convert it: if (!is.factor(gdata[[SERIESVAR]])) { gdata[[SERIESVAR]] <- as.factor(gdata[[SERIESVAR]]) } - + # Set names as factor levels # Set names as factor levels levs <- levels(unique(gdata[[SERIESVAR]])) @@ -159,14 +159,14 @@ g_seriessym <- function(gdata, #' @export #' #' @examples -#' library(carver) +#' library(tlfcarver) #' empty_plot() empty_plot <- function(message = "No data available for these values", fontsize = 8) { g_plot <- ggplot() + annotate("text", - x = 1, y = 1, size = fontsize, - label = message + x = 1, y = 1, size = fontsize, + label = message ) + theme_void() fig <- ggplotly(g_plot, height = 200) |> @@ -204,7 +204,7 @@ def_axis_spec <- function(arg, vec, val) { #' @export #' #' @examples -#' library(carver) +#' library(tlfcarver) #' #' plot_axis_opts( #' xlinearopts = list( @@ -242,36 +242,36 @@ def_axis_spec <- function(arg, vec, val) { #' plot_axis_opts <- function(xlinearopts = list( - breaks = waiver(), - limits = NULL, - labels = waiver() - ), - ylinearopts = list( - breaks = waiver(), - limits = NULL, - labels = waiver() - ), - xaxis_scale = "identity", - yaxis_scale = "identity", - xaxis_label = "", - yaxis_label = "", - xopts = list( - labelsize = 12, - labelface = "plain", - ticksize = 8, - tickface = "plain", - angle = 0 - ), - yopts = list( - labelsize = 12, - labelface = "plain", - ticksize = 8, - tickface = "plain", - angle = 0 - )) { + breaks = waiver(), + limits = NULL, + labels = waiver() + ), + ylinearopts = list( + breaks = waiver(), + limits = NULL, + labels = waiver() + ), + xaxis_scale = "identity", + yaxis_scale = "identity", + xaxis_label = "", + yaxis_label = "", + xopts = list( + labelsize = 12, + labelface = "plain", + ticksize = 8, + tickface = "plain", + angle = 0 + ), + yopts = list( + labelsize = 12, + labelface = "plain", + ticksize = 8, + tickface = "plain", + angle = 0 + )) { stopifnot(is.list(xlinearopts)) stopifnot(is.list(ylinearopts)) - + list( Ybrks = def_axis_spec( arg = ylinearopts, @@ -425,7 +425,7 @@ plot_aes_opts <- function(datain, #' msumstat( #' adsl_entry, #' dptvar = "AGE", -#' statvar = "meansd" +#' statvar = "mean" #' )$gsum |> #' plot_display_bign(adsl_entry) plot_display_bign <- function(datain, @@ -580,7 +580,7 @@ plot_title_nsubj <- function(datain, plot_data, by) { } else { plot_data <- bind_cols(plot_data, adsin_count) } - plot_data + return(plot_data) } #' Convert dataframe into ggplot object table @@ -599,7 +599,7 @@ plot_title_nsubj <- function(datain, plot_data, by) { #' @export #' #' @examples -#' library(carver) +#' library(tlfcarver) #' MPG <- ggplot2::mpg #' MPG[["cyl"]] <- as.character(MPG[["cyl"]]) #' tbl_to_plot( diff --git a/R/line_plot.R b/R/line_plot.R index 8d5c8ae..5c6e784 100644 --- a/R/line_plot.R +++ b/R/line_plot.R @@ -87,7 +87,7 @@ line_plot <- function(datain, )) + geom_line(aes(color = .data[[series_var]])) + geom_point(aes(color = .data[[series_var]]), - shape = 16 + shape = 16 ) + labs( title = plot_title, diff --git a/R/mcatstat.R b/R/mcatstat.R index 5a9c4e1..f4a059e 100644 --- a/R/mcatstat.R +++ b/R/mcatstat.R @@ -22,7 +22,7 @@ #' category #' @param a_subset Analysis Subset condition specific to categorical analysis. #' @param denom_subset Subset condition to be applied to data set for calculating denominator. -#' @param uniqid Variable to calculate unique counts of. Expected values: `"USUBJID"`, `"SITEID"`, +#' @param uniqid Variable(s) to calculate unique counts of. eg. `"USUBJID"`, `"SITEID"`, #' `"ALLCT"` #' @param dptvar Categorical Analysis variable and ordering variable if exists, #' separated by /. eg: `"SEX"`, `"SEX/SEXN"`, `"AEDECOD"`, `"ISTPT/ISTPTN"` @@ -40,7 +40,12 @@ #' @param dptvarn Number to assign as `DPTVARN`, useful for block sorting when #' multiple `mcatstat()` outputs are created to be combined. #' @param pctsyn Display Percentage Sign in table or not. Values: `"Y"/"N"` +#' @param sigdec Number of decimal places for % displayed in output #' @param denomyn Display denominator in output or not. Values: `"Y"/"N"` +#' @param sparseyn To sparse missing categories/treatments or not? `"Y"/"N"` +#' @param sparsebyvalyn Sparse missing categories within by groups. `"Y"/"N"` +#' @param return_zero Return rows with zero counts if analysis subset/ non-missing does not +#' exist in data. `"Y"/"N"` #' #' @details #' \itemize{ @@ -113,17 +118,25 @@ mcatstat <- function(datain = NULL, total_catlabel = "Total", dptvarn = 1, pctsyn = "Y", - denomyn = "N") { - stopifnot("No data for mcatstat" = nrow(datain) != 0) - stopifnot("uniqid should exist in data or be ALLCT" = uniqid %in% c(names(datain), "ALLCT")) + sigdec = 2, + denomyn = "N", + sparseyn = "N", + sparsebyvalyn = "N", + return_zero = "N") { + if (nrow(datain) == 0) { + return(datain) + } + stopifnot("uniqid should exist in data or be ALLCT" = all(uniqid %in% c(names(datain), "ALLCT"))) # Identify by groups if exists BYVAR <- var_start(datain, "BYVAR") # Identify subgroups if exists SUBGRP <- var_start(datain, "SUBGRP") + SUBGRPN <- var_start(datain, "SUBGRPN") + BYVARN <- var_start(datain, "BYVARN") dptvars <- sep_var_order(dptvar) # Process unique ID variable (if passed as "ALLCT") # If unique ID is ALLCT, use all rows instead of unique subjects - if (uniqid == "ALLCT") { + if (all(uniqid == "ALLCT")) { datain <- datain |> mutate(ALLCT = row_number()) } @@ -145,44 +158,77 @@ mcatstat <- function(datain = NULL, .data[["DPTVAL"]] ) ) - # Apply subsets to get num - if (!is.na(a_subset) && str_squish(a_subset) != "") { - data_num <- data_pro |> - filter(!!!parse_exprs(a_subset)) - } else { - data_num <- data_pro - } - # Subset for denom data - if (!is.na(denom_subset) && str_squish(denom_subset) != "") { - data_denom <- data_pro |> - filter(!!!parse_exprs(denom_subset)) - } else { - data_denom <- data_pro - } - if (nrow(data_num) < 1 || nrow(data_denom) < 1) { - return(data.frame()) - } + # Apply subsets to get num and denom data: + dflist <- map(list(a_subset, denom_subset), \(s) { + if (!is.na(s) && str_squish(s) != "") { + filter(data_pro, !!!parse_exprs(s)) + } else { + data_pro + } + }) + data_num <- dflist[[1]] + data_denom <- dflist[[2]] + # If missing categories to be included if (miss_catyn == "N") { data_num <- data_num |> filter(.data[["DPTVAL"]] != miss_catlabel) } # Set groups by Treatment, Sub,By if any to use for counts # Get N count as variable FREQ: - counts <- data_num |> - group_by(across(any_of(c( - "TRTVAR", SUBGRP, var_start(datain, "SUBGRPN"), BYVAR, - var_start(datain, "BYVARN"), "DPTVAL", "DPTVALN" - )))) |> - summarise(FREQ = length(unique(.data[[uniqid]]))) |> - ungroup() - - # If cumulative count is required then - if (cum_ctyn == "Y") { - counts <- counts |> - group_by(across(any_of(c("TRTVAR", BYVAR, SUBGRP)))) |> - arrange(.data[["DPTVALN"]], .by_group = TRUE) |> - mutate(FREQ = cumsum(.data[["FREQ"]])) |> + countgrp <- c( + var_start(data_num, "TRTVAR"), SUBGRP, SUBGRPN, BYVAR, + BYVARN, "DPTVAL", "DPTVALN" + ) + # If a_subset returns NONE and equired to return 0 count row: + if (nrow(data_num) < 1 && return_zero == "Y") { + counts <- data_pro |> + group_by(across(any_of(countgrp))) |> + summarise(FREQ = 0) |> ungroup() + } else { + # Else proceed to calculate count and percentage + if (nrow(data_num) < 1 || nrow(data_denom) < 1) { + return(data.frame()) + } + # Get count dataset and sparse categories + counts <- data_num |> + group_by(across(any_of(countgrp))) |> + summarise(FREQ = n_distinct(across(any_of(uniqid)))) |> + ungroup() + # Sparse categories (within by groups for sparseyn) + # sparsebyvalyn will also impute for 'by' categories + if (sparseyn == "Y" || (sparsebyvalyn == "Y" && length(BYVAR) > 0)) { + data_sparse <- data_pro + } else { + data_sparse <- counts + } + counts <- counts |> + sparse_vals( + data_sparse = data_sparse, + sparseyn = "Y", + sparsebyvalyn = "N", + BYVAR, + SUBGRP, + BYVARN, + SUBGRPN + ) |> + sparse_vals( + data_sparse = data_sparse, + sparseyn = "N", + sparsebyvalyn = sparsebyvalyn, + BYVAR, + SUBGRP, + BYVARN, + SUBGRPN + ) + # If cumulative count is required then + if (cum_ctyn == "Y") { + counts <- counts |> + group_by(across(any_of(c("TRTVAR", BYVAR, SUBGRP)))) |> + arrange(.data[["DPTVALN"]], .by_group = TRUE) |> + mutate(FREQ = cumsum(.data[["FREQ"]])) |> + ungroup() + } } # Calculate denominator/pct and add requisite variables for standard display processing: df <- counts |> @@ -192,17 +238,19 @@ mcatstat <- function(datain = NULL, pctdisp, pctsyn, denomyn, + sigdec, BYVAR, SUBGRP - ) |> + ) + df <- df |> mutate( DPTVAR = dptvars$vars, XVAR = .data[["DPTVAL"]], DPTVARN = dptvarn, CN = "C" ) |> select(any_of(c(BYVAR, "TRTVAR", SUBGRP, "DPTVAR", "DPTVAL", "CVALUE")), everything()) - + message("mcatstat success") - - return(df) + + df } #' Caclulate denominator and oercentage for mcatstat @@ -227,49 +275,57 @@ calc_denom <- function(counts, pctdisp = "TRT", pctsyn = "Y", denomyn = "N", + sigdec = 2, BYVAR, SUBGRP) { # Check Allowable pctdisp values stopifnot( "Invalid pctdisp" = str_remove(pctdisp, "[[:digit:]]+") %in% - c("TRT", "VAR", "COL", "SUBGRP", "SGRPN", "CAT", "NONE", "NO", "DPTVAR", "BYVARN") + c("TRT", "VAR", "COL", "SUBGRP", "SGRPN", "CAT", "NONE", "NO", "DPTVAR", "BYVARN") ) # Set denominator values for percentage if (pctdisp %in% c("NONE", "NO")) { - df <- counts |> mutate(CVALUE = .data[["FREQ"]]) # No percentage if pctdisp is NO/NONE + df <- counts |> mutate(CVALUE = as.character(.data[["FREQ"]])) + # No percentage if pctdisp is NO/NONE } else { # Identify which variables go towards creating Denominator if (pctdisp == "VAR") { # If pctdisp = VAR, total percent across all records - df <- counts |> mutate(DENOMN = length(unique(data_denom[[uniqid]]))) + df <- counts |> mutate(DENOMN = nrow(unique(data_denom[uniqid]))) } else { percgrp <- switch(gsub("[[:digit:]]", "", pctdisp), - "TRT" = "TRTVAR", - "CAT" = c(BYVAR, "DPTVAL"), - "COL" = c("TRTVAR", SUBGRP), - "SUBGRP" = c("TRTVAR", SUBGRP, BYVAR), - "SGRPN" = SUBGRP, - "DPTVAR" = c("TRTVAR", SUBGRP, BYVAR, "DPTVAL"), - "BYVARN" = c("TRTVAR", paste0("BYVAR", str_to_vec( - str_extract(pctdisp, "[[:digit:]]+"), "" - ))) + "TRT" = "TRTVAR", + "CAT" = c(BYVAR, "DPTVAL"), + "COL" = c("TRTVAR", SUBGRP), + "SUBGRP" = c("TRTVAR", SUBGRP, BYVAR), + "SGRPN" = SUBGRP, + "DPTVAR" = c("TRTVAR", SUBGRP, BYVAR, "DPTVAL"), + "BYVARN" = c("TRTVAR", paste0("BYVAR", str_to_vec( + str_extract(pctdisp, "[[:digit:]]+"), "" + ))) ) |> intersect(names(data_denom)) # Get denominator count per above variables df <- data_denom |> group_by(across(all_of(percgrp))) |> - summarise(DENOMN = length(unique(.data[[uniqid]]))) |> + summarise(DENOMN = n_distinct(across(any_of(uniqid)))) |> inner_join(counts, by = percgrp, multiple = "all") } - + # Calculate percentage as PCT and concatenate as CVALUE p <- ifelse(pctsyn == "N", "", "%") # nolint - df <- df |> mutate(PCT = round_f((FREQ * 100) / DENOMN, 2)) + df <- df |> + mutate( + PCT = (.data[["FREQ"]] * 100) / DENOMN, + CPCT = round_f(.data[["PCT"]], sigdec) + ) if (denomyn == "Y") { - df <- df |> mutate(CVALUE = glue("{FREQ}/{DENOMN} ({PCT}{p})")) + cstat <- "{FREQ}/{DENOMN} ({CPCT}{p})" } else { - df <- df |> mutate(CVALUE = glue("{FREQ} ({PCT}{p})")) + cstat <- "{FREQ} ({CPCT}{p})" } + df <- df |> + mutate(CVALUE = ifelse(FREQ == 0, "0", glue(cstat))) } return(df |> ungroup()) } diff --git a/R/mentry.R b/R/mentry.R index 58fe185..cfeb3db 100644 --- a/R/mentry.R +++ b/R/mentry.R @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. # -#' Function to read in and process data with subsets and variables. +#' Read and process data with subsets and variables #' #' @description #' @@ -33,6 +33,8 @@ #' @param trttotalyn Add total treatment values to be displayed as column in #' table or category in plot (`"Y"/"N"`). #' @param trttotlabel Label for total Treatment column/group +#' @param trtmissyn Retain Missing treatment counts in Total (if `trttotalyn` = Y). Missing +#' treatment will not be considered as a column in analysis in any case. #' @param sgtotalyn Add total subgroup values to be displayed as column in #' table or category in plot (`"Y"/"N"`). #' @param add_grpmiss Add row or column for missing category in grouping @@ -99,10 +101,13 @@ mentry <- function(datain, trtsort = NA, trttotalyn = "N", trttotlabel = "Total", + trtmissyn = "N", sgtotalyn = "N", add_grpmiss = "N", pop_fil = "Overall Population") { - stopifnot(is.data.frame(datain), nrow(datain) > 0) + if (!is.data.frame(datain) || nrow(datain) == 0) { + return(data.frame()) + } byvarlist <- sep_var_order(byvar) byvar <- byvarlist[["vars"]] sgvarlist <- sep_var_order(subgrpvar) @@ -147,7 +152,7 @@ mentry <- function(datain, ## TRTVARs if (!is.na(trtvar) && str_squish(trtvar) != "") { dsin <- dsin |> - create_trtvar(trtvar, trtsort, trttotalyn, trttotlabel) + create_trtvar(trtvar, trtsort, trttotalyn, trttotlabel, trtmissyn) } # Remove invalid Treatment Values if ("TRTVAR" %in% names(dsin)) { @@ -157,7 +162,9 @@ mentry <- function(datain, "SCREEN FAILURE", "SCRNFAIL", "NOTRT", - "NOTASSGN" + "NOTASSGN", + "", + NA_character_ ) )) } @@ -181,10 +188,10 @@ create_grpvars <- function(dsin, vars, varN, new_var = "BYVAR", totalyn = "N", t grpvarN <- paste0(grpvar, "N") var <- vars[x] varN <- varN[x] - + df <- dsin |> mutate(!!grpvar := as.character(.data[[var]])) - + if (varN %in% names(df)) { df <- df |> mutate(!!grpvarN := .data[[varN]]) @@ -192,7 +199,7 @@ create_grpvars <- function(dsin, vars, varN, new_var = "BYVAR", totalyn = "N", t df <- df |> mutate(!!grpvarN := as.numeric(factor(.data[[var]]))) } - + if (totalyn == "Y") { df <- df |> bind_rows(mutate(df, !!grpvar := totlabel, !!grpvarN := 9999)) @@ -213,19 +220,25 @@ create_grpvars <- function(dsin, vars, varN, new_var = "BYVAR", totalyn = "N", t #' @param trtvar Treatment Variable #' @param trtsort Treatment Sorting variable #' @param trttotalyn Display treatment total (`Y/N`) +#' @param trtmissyn Retain Missing treatment counts in Total (if `trttotalyn` = Y) #' #' @return Data frame with added `TRT` variables #' @noRd -create_trtvar <- function(dsin, trtvar, trtsort, trttotalyn, trttotlabel = "Total") { +create_trtvar <- function(dsin, trtvar, trtsort, trttotalyn, trttotlabel = "Total", trtmissyn) { map <- c(trt = "TRTVAR", sort = "TRTSORT") - + df <- dsin |> mutate(!!unname(map["trt"]) := .data[[trtvar]]) - + # keep missing treatments in total if trtmissyn is given (always removed in post) + if (trtmissyn != "Y") { + df <- df |> + filter(!(.data[["TRTVAR"]] %in% c("", NA_character_))) + } + # Create trt sorting variable if (is.na(trtsort) || str_squish(trtsort) == "") { trtsort <- trtvar } - + if (trtsort %in% names(df) && is.numeric(df[[trtsort]])) { df <- df |> mutate(!!unname(map["sort"]) := .data[[trtsort]]) @@ -233,16 +246,16 @@ create_trtvar <- function(dsin, trtvar, trtsort, trttotalyn, trttotlabel = "Tota df <- df |> mutate(!!unname(map["sort"]) := as.numeric(factor(.data[[trtsort]]))) } - + if (trttotalyn == "Y") { df <- df |> bind_rows(mutate(df, !!unname(map["trt"]) := trttotlabel, !!unname(map["sort"]) := 999)) } - + df |> mutate(!!unname(map["trt"]) := factor(.data[[unname(map["trt"])]], - levels = unique(.data[[unname(map["trt"])]][order(.data[[unname(map["sort"])]])]), - ordered = TRUE + levels = unique(.data[[unname(map["trt"])]][order(.data[[unname(map["sort"])]])]), + ordered = TRUE )) } @@ -266,7 +279,7 @@ sep_var_order <- function(vars, var_sep = "~", ord_sep = "/") { } sep_vars }) - + bind_cols(map(set_names(unique(names(flatten(var_list)))), \(col) { map_chr(var_list, \(df) pluck(df, col)) })) diff --git a/R/msumstat.R b/R/msumstat.R index 3432d28..72e494c 100644 --- a/R/msumstat.R +++ b/R/msumstat.R @@ -19,17 +19,18 @@ #' @param a_subset Analysis subset condition specific to this function. #' @param statvar `Tilde` (`~`)-separated list of statistics to be computed. eg: `"mean~median"` #' @param sigdec Number of base decimal places to retain in output -#' Applies to mean, min, max etc and `+ 1` for sd +#' Applies to mean, min, max, sd etc #' @param dptvarn Number to assign as `'DPTVARN'`, used for block sorting when #' multiple blocks are created to be combined. +#' @param sparsebyvalyn Sparse missing categories within by groups. `"Y"/"N"` +#' @param figyn Determine if output is for figure or not `"Y"/"N"` #' #' @details Current available statistics (values for `statvar`) : #' n (count per group), mean, median, sd (standard deviation), min, max, #' iqr (interquartile range), var (variance), sum, range ("min, max") -#' meansd ("mean (sd)"), medianrange ("median (range)"), -#' q25/q1 (25 % quantile), q75/q3 (75 % quantile) , p10 (10% quantile), p5, p1, -#' p90, p95, p99, q1q3 ("q25, q75"), whiskerlow, whiskerup (box lower/upper -#' whiskers), outliers (boxplot outliers, tilde-separated output), +#' mean(sd), median(minmax), q25/q1 (25 % quantile), q75/q3 (75 % quantile) , p10 (10% quantile), +#' p5, p1, p90, p95, p99, q1q3 ("q25, q75"), whiskerlow, whiskerup (box lower/upper +#' whiskers), outliers (boxplot outliers, tilde-separated output), geometric mean/sd/CI #' box = median~q25~q75~whiskerlow~whiskerup~outliers (Tukey's method) #' #' @return a list containing 2 elements @@ -54,9 +55,10 @@ #' adsl_sum <- adsl_entry |> #' msumstat( #' dptvar = "AGE", -#' a_subset = "BYVAR1 == 'M'", -#' statvar = "mean", -#' sigdec = 2 +#' a_subset = "SEX == 'F'", +#' statvar = "mean(sd)~median(minmaxc)~q3", +#' sigdec = "3(2)~2(0)~1", +#' sparsebyvalyn = "N" #' ) #' #' adsl_sum$tsum @@ -66,57 +68,100 @@ msumstat <- function(datain = NULL, a_subset = NA_character_, dptvar = NULL, statvar = "", - sigdec = 1, - dptvarn = 1) { + sigdec = "", + dptvarn = 1, + sparsebyvalyn = "N", + figyn = "N") { # Check if data is present - stopifnot("No data to analyze" = nrow(datain) != 0) + if (nrow(datain) == 0) { + return(datain) + } # Check that dependent variable exists and convert it to numeric stopifnot("Dependent Variable does not Exist" = dptvar %in% names(datain)) if (!is.numeric(datain[[dptvar]])) { datain <- datain |> mutate(across(all_of(dptvar), ~ as.numeric(.))) } - datain <- datain |> filter(!is.na(.data[[dptvar]])) # IF analysis subset is given: if (!is.na(a_subset) && str_squish(a_subset) != "") { - datain <- datain |> filter(eval(parse(text = a_subset))) + datapro <- datain |> filter(eval(parse(text = a_subset))) + # Check data exists after subset: + if (nrow(datapro) == 0) { + return(datapro) + } + } else { + datapro <- datain } + # Available and customized statistics if (is.null(statvar) || all(statvar == "")) { statinput <- c("n", "mean", "min", "median", "max", "sd") - } else { + } else if (any(str_detect(statvar, "box"))) { # custom statistics given as input statinput <- statvar |> str_replace("box", "median~q25~q75~whiskerlow~whiskerup~outliers") |> str_to_vec() + } else { + statinput <- str_to_vec(statvar) } - # Define basic statistics to be printed in the output. - # Creates summary stats functions: - statinput <- recode( - tolower(statinput), - q1 = "q25", q3 = "q75" + sigdec <- str_to_vec(sigdec) + # Resolve concatenated statisitcs into simpler ones + baselist <- parse_stats(tolower(statinput), sigdec) + # Get list of basic functions from summary_functions() + list_stats <- summary_functions(names(baselist), unname(baselist)) + # Stat grouping vars: + countgrp <- c( + grep("BYVAR", names(datapro), value = TRUE), "TRTVAR", + grep("SUBGRPVAR", names(datapro), value = TRUE) ) - # Get list of functions from summary_functions() - list_stats <- summary_functions(sigdec) - # Check that input stat functions exist - stopifnot("Statistics not in summary_functions()" = all(statinput %in% names(list_stats))) - list_stats <- list_stats[statinput] # Bring two variables for name and values of category and perform analyses - data_wide <- datain |> - select(any_of(c(dptvar, "TRTVAR")), starts_with(c("BYVAR", "SUBGRP"))) |> - group_by(across(any_of(starts_with(c("BYVAR", "TRTVAR", "SUBGRP"))))) |> - summarise(across(all_of(dptvar), list_stats, .names = "{.fn}")) |> - mutate(across(where(is.character), ~ replace(., is.na(.), "-"))) |> - mutate(DPTVAR = dptvar, CN = "N", DPTVARN = dptvarn) |> - ungroup() - + data_wide <- datapro |> + select(any_of(c(dptvar, countgrp))) |> + group_by(across(any_of(countgrp))) |> + summarise(across(all_of(dptvar), list_stats, .names = "{tolower(.fn)}")) |> + ungroup() |> + mutate(across(any_of(names(baselist)), ~ ifelse(.x %in% c("Inf", "-Inf"), NA, .x))) + # If required, sparse empty by groups: + # Note this only works if by variables exist and for tables: + BYVAR <- var_start(data_wide, "BYVAR") + BYVARN <- var_start(data_wide, "BYVARN") + if (length(BYVAR) > 0 && figyn != "Y") { + if (sparsebyvalyn == "Y") { + data_sparse <- datain + } else { + data_sparse <- data_wide |> group_by(across(starts_with("BYVAR"))) + BYVAR <- character(0) + BYVARN <- character(0) + } + data_wide <- data_wide |> + sparse_vals( + data_sparse = data_sparse, + sparseyn = "N", + sparsebyvalyn = "Y", + BYVAR, + var_start(data_wide, "SUBGRP"), + BYVARN, + var_start(data_wide, "SUBGRPN"), + fillvar = colnames(data_wide)[!colnames(data_wide) %in% countgrp], + fill_with = "-" + ) + } + data_wide <- data_wide |> + derv_stats(statinput) |> + select(any_of(c(countgrp, tolower(statinput)))) |> + mutate(DPTVAR = dptvar, CN = "N", DPTVARN = dptvarn) + # Post-sparsing, n/miss/obs should be 0 and not - + if (any(c("n", "nmiss", "nobs") %in% names(data_wide))) { + data_wide <- data_wide |> + mutate(across(any_of(c("n", "nmiss", "nobs")), ~ gsub("^-$", "0", .x))) + } # Tidy into long dataframe for use in tabular display data_long <- data_wide |> - pivot_longer(all_of(statinput), - names_to = "DPTVAL", - values_to = "CVALUE" + pivot_longer(all_of(tolower(statinput)), + names_to = "DPTVAL", + values_to = "CVALUE" ) |> mutate(DPTVALN = as.numeric(fct_inorder(.data[["DPTVAL"]]))) - + message("msum success") return(list(tsum = data_long, gsum = data_wide)) } diff --git a/R/occ_tier_summary.R b/R/occ_tier_summary.R index 383719c..4702ba0 100644 --- a/R/occ_tier_summary.R +++ b/R/occ_tier_summary.R @@ -11,22 +11,56 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +# #' Generic Occurrence Summary Tiered Table #' #' @inheritParams risk_stat #' @param datain Input dataset (generally the output from `mentry()`) -#' @param hterm High Level Event term variable, used for analysis +#' @param hterm High Level Event term variable, used for analysis (tilde-separated) #' @param lterm Low Level Event term variable, used for analysis +#' @param htermctyn To show count of high term rows or not. Should correspond to and be same number +#' of terms passed in `hterm` (tilde-separated). To suppress showing counts for any term pass "N" #' @param pctdisp Method to calculate denominator (for %) by. #' Possible values: `"TRT"`, `"VAR"`, `"COL"`, `"SUBGRP"`, `"CAT"`, `"NONE"`, `"NO"`, `"DPTVAR"`, #' `"BYVARxyN"` -#' @param apply_hrow_cutoff To apply cutoff value to high terms in addition to low term. +#' @param sum_row To show summary/any term row or not. 'Y'/'N' +#' @param sum_row_label Label for Summary Row to be displayed, if Y. +#' @param apply_hrow_cutoff To apply `cutoff_where` value to high terms in addition to low term. #' If set to "Y" same cutoff is applied to remove both high and low level terms that don't meet #' the criteria. #' If set to "N" (default), cutoff is applied only to Lower Level term. The terms that do not fit #' the criteria are then excluded from the counts for High Level term. This does not happen in case -#' of "N" - all counts are included in high term which is displayed as long as it meets the criteria -#' as well. +#' of "N" - all low terms are included in high term which is displayed as long as it meets the +#' criteria as well. +#' @param sort_col Which treatment column to sort by. (Depends on trt levels) eg: 1, 2, 3 +#' @param nolwrtierdispyn When`apply_hrow_cutoff` = Y, to display high level terms with zero low +#' level terms satisfying the cutoff threshold or not? If Y, high terms will be displayed even with +#' no corresponding lower levels in the table. +#' @param sigdec_cat Number of decimal places for % displayed in output +#' @param pctsyn Display Percentage Sign in table or not. Values: `"Y"/"N"` +#' @param stathead Label for sub-column header in output. eg. "n (%)" +#' +#' @details +#' \itemize{ +#' \item `cutoff_where` is applied to event lower term only, unless `apply_hrow_cutoff` is given. +#' \item If `apply_hrow_cutoff` is Y, cutoff_where is applied to higher terms as well. If it is N, +#' lower terms which do not meet criteria are removed from higher term count. eg: if `cutoff_where` +#' is set to "PCT >= 2" and `hterm` and `lterm` are AEBODSYS and AEDECOD: +#' +#' EYE DISORDERS 9 (3.1) +#' Dry eye 3 (1.4) +#' Wet eye 6 (2.4) +#' +#' Here if `apply_hrow_cutoff` is set to N then 'Dry eye' row will be excluded and the 3 excluded +#' from count of EYE DISORDERS as well (9). If Y, then 'Dry eye' will be excluded but EYE DISORDERS +#' not impacted as it is 4.4% and its PCT >= 2. +#' +#' \item If `cutoff_where` is PCT >= 3 and `nolwrtierdispyn` set to Y, then +#' neither Dry eye nor Wet eye will be shown, but EYE DISORDERS will still be displayed. +#' +#' If `nolwrtierdispyn` is N in this case, EYE DISORDERS will also be removed as no low terms meet +#' the criteria. +#' } #' #' @return Summarized data frame for Adverse Events based on high and lower terms. #' @export @@ -50,7 +84,7 @@ #' hterm = "AEBODSYS", #' lterm = "AEDECOD", #' pctdisp = "TRT", -#' cutoff = 2, +#' cutoff_where = "PCT > 2", #' apply_hrow_cutoff = "N", #' sort_opt = "Ascending", #' sort_var = "Count" @@ -58,72 +92,207 @@ #' output |> #' tbl_processor() |> #' tbl_display() +#' # Example 2: ADAE table with max sev/ctc grade: +#' ae_pre <- ae_pre_processor( +#' tornado_plot_data$adae, +#' subset = "TRTEMFL == 'Y'", +#' max_sevctc = "SEV", +#' sev_ctcvar = "ASEVN", +#' pt_total = "Y" +#' ) +#' ae_entry_max <- adsl_merge( +#' tornado_plot_data$adsl, +#' adsl_subset = 'SAFFL == "Y"', +#' ae_pre[["data"]] +#' ) |> +#' mentry( +#' subset = NA, +#' byvar = "AEBODSYS", +#' trtvar = "TRTA", +#' trtsort = "TRTAN", +#' trttotalyn = "N", +#' add_grpmiss = "N", +#' subgrpvar = "ASEV", +#' sgtotalyn = "N", +#' pop_fil = "Overall Population" +#' ) +#' rpt_data <- occ_tier_summary( +#' ae_entry_max, +#' a_subset = ae_pre[["a_subset"]], +#' summary_by = "Patients", +#' hterm = "AEBODSYS", +#' lterm = "AEDECOD", +#' cutoff_where = "FREQ > 2", +#' pctdisp = "TRT", +#' sum_row = "Y", +#' sum_row_label = "Any Adverse Event", +#' nolwrtierdispyn = "N", +#' sort_opt = "Alphabetical", +#' stathead = "n (%)" +#' ) +#' rpt_data |> +#' tbl_processor() |> +#' tbl_display(dpthead = "No. of Adverse Events_SOC and PT") |> +#' flextable::autofit() +#' ## ADPR Example: +#' \dontrun{ +#' pr_entry <- adsl |> +#' adsl_merge( +#' adsl_subset = "SAFFL == 'Y'", +#' dataset_add = adpr +#' ) |> +#' mentry( +#' subset = NA, +#' byvar = "PRSOC", +#' trtvar = "TRT01A", +#' trtsort = "TRT01AN", +#' trttotalyn = "N", +#' add_grpmiss = "N", +#' sgtotalyn = "N", +#' pop_fil = "Overall Population" +#' ) +#' output <- occ_tier_summary( +#' pr_entry, +#' a_subset = "ONPERFL == 'Y' & PRDECOD != ''", +#' summary_by = "Patients", +#' hterm = "PRSOC", +#' lterm = "PRDECOD", +#' pctdisp = "TRT", +#' apply_hrow_cutoff = "N", +#' sort_opt = "Ascending", +#' sort_var = "Count", +#' sum_row = "Y", +#' sum_row_label = "Participants with 1 term", +#' htermctyn = "N" +#' ) +#' output |> +#' display_bign_head( +#' mentry_data = pr_entry +#' ) |> +#' tbl_processor() |> +#' tbl_display() +#' } #' occ_tier_summary <- function(datain, a_subset = NA_character_, summary_by = "Patients", hterm = "AEBODSYS", lterm = "AEDECOD", + htermctyn = "Y", pctdisp = "TRT", - cutoff = 2, + cutoff_where = NA, + sum_row = "N", + sum_row_label = "Number of Participants with Any AE", apply_hrow_cutoff = "N", sort_opt = "Ascending", - sort_var = "Count") { - stopifnot("Input data is empty" = nrow(datain) > 0) - stopifnot("Invalid method to set denominator percentage" = pctdisp %in% c("TRT", "HT", "VAR")) + sort_var = "Count", + sort_col = 1, + nolwrtierdispyn = "N", + sigdec_cat = 2, + pctsyn = "Y", + stathead = "n (%)") { + if (nrow(datain) == 0) { + return(datain) + } stopifnot( "`byvar` in `mentry()` cannot be `NA` or ''" = identical(var_start(datain, "BYVAR"), "BYVAR1") ) - stopifnot( - "`byvar` in `mentry()` should be identical to `hterm` in `occurrence_summary()" = - identical(unique(datain[["BYVAR1"]]), unique(datain[[hterm]])) - ) - rows <- c(var_start(datain, "BYVAR"), "DPTVAL") + byvars <- var_start(datain, "BYVAR") + rows <- c(byvars, "DPTVAL") # Lowest Term Calculation lcat <- mcatstat( datain = datain, a_subset = a_subset, uniqid = ifelse(summary_by == "Events", "ALLCT", "USUBJID"), dptvar = lterm, - pctdisp = pctdisp - ) |> - mutate(across(all_of(c("FREQ", "PCT")), \(x) as.double(x))) + pctdisp = pctdisp, + sigdec = sigdec_cat, + pctsyn = pctsyn, + sparseyn = "N" + ) + if (!is.data.frame(lcat) || nrow(lcat) == 0) { + return(data.frame()) + } # Applying Cutoff to lower level term counts/pct - if (!cutoff %in% c("", NA, 0)) { - lcat_cut <- lcat |> filter(.data[["PCT"]] > as.numeric(cutoff)) + if (!is.na(cutoff_where) && str_detect(cutoff_where, "PCT|FREQ")) { + lcat_cut <- lcat |> + filter(!!!parse_exprs(cutoff_where)) |> + select(all_of(rows)) |> + mutate(CUTFL = "Y") lcat <- lcat |> semi_join(lcat_cut, by = rows) if (nrow(lcat) < 1) { - return(data.frame("Note" = "No low term data available under these conditions")) + return(data.frame()) } # Rows under cutoff to be excluded from higher level term counts if (apply_hrow_cutoff != "Y") { datain <- datain |> - semi_join(rename(lcat_cut, !!lterm := DPTVAL), by = gsub("DPTVAL", lterm, rows)) + left_join(rename(lcat_cut, !!lterm := DPTVAL), by = gsub("DPTVAL", lterm, rows)) + a_subset <- paste(na.omit(c(a_subset, "CUTFL == 'Y'")), collapse = "&") + if (nolwrtierdispyn == "N") { + datain <- semi_join(datain, lcat_cut, by = byvars[length(byvars)]) + } + } else { + if (nolwrtierdispyn == "N") { + datain <- datain |> + left_join(lcat_cut |> select(-all_of("DPTVAL")), by = byvars[length(byvars)]) + a_subset <- paste(na.omit(c(a_subset, "CUTFL == 'Y'")), collapse = "&") + } } } - + hterm <- str_to_vec(hterm) # Higher Terms calculation: hcat <- map(hterm, \(h_term) { h <- mcatstat( datain = datain, - a_subset = a_subset, + a_subset = ifelse("HT_FL" %in% names(datain), "HT_FL == 1", a_subset), uniqid = ifelse(summary_by == "Events", "ALLCT", "USUBJID"), dptvar = h_term, - pctdisp = pctdisp - ) |> - mutate(across(all_of(c("FREQ", "PCT")), \(x) as.double(x))) + pctdisp = pctdisp, + sigdec = sigdec_cat, + pctsyn = pctsyn, + sparseyn = "N" + ) # Apply CUTOFF based on parameter and lower term value - if (!cutoff %in% c("", NA, 0) && apply_hrow_cutoff == "Y") { + if (!is.na(cutoff_where) && str_detect(cutoff_where, "PCT|FREQ") && apply_hrow_cutoff == "Y") { h <- h |> - semi_join(h |> filter(.data[["PCT"]] > as.numeric(cutoff)), by = rows) + semi_join(h |> filter(!!!parse_exprs(cutoff_where)), by = rows) if (nrow(h) < 1) { - return(data.frame("Note" = "No high term data available under these conditions")) + return(data.frame()) } } h }) + + # Summary/Any Row Output + if (sum_row != "N") { + sum_data <- summary_row_cat( + datain, + sum_row_label, + byvaryn = "N", + a_subset, + pctdisp, + sigdec_cat, + pctsyn, + "ANY" + ) + } else { + sum_data <- NULL + } + + if ("PT_CNT" %in% names(datain)) { + pt_data <- datain |> + summary_row_cat( + a_subset = a_subset, + var = "PT_CNT", + pctdisp = "NONE", + sum_row_label = "Total preferred term events", + uniqid = c("AEDECOD", "USUBJID") + ) + } else { + pt_data <- NULL + } ### Combined Processing for lower and higher terms # Sort Values and Options if (sort_opt == "Alphabetical") { @@ -131,11 +300,11 @@ occ_tier_summary <- function(datain, } else { sort_var <- get_sort_var(sort_var) } - ctrlgrp <- get_ctrlgrp(datain) + ctrlgrp <- get_ctrlgrp(datain, sort_col) comb <- append(hcat, list(lcat)) # Mapping over each categorical dataset and applying post-processing function to concatenate # the dataframes into single output - map(seq_along(comb), \(i) { + outdata <- map(seq_along(comb), \(i) { xout <- comb[[i]] |> mutate( CTRL_N = ifelse(.data[["TRTVAR"]] == ctrlgrp, .data[["FREQ"]], NA), @@ -149,7 +318,22 @@ occ_tier_summary <- function(datain, xout |> ord_summ_df(sort_var, sort_opt) }) |> - post_occ_tier(ctrlgrp = ctrlgrp) + post_occ_tier(ctrlgrp = ctrlgrp, sum_row = sum_data, pt_row = pt_data, stathead = stathead) + + # To suppress any high term percentage counts, per variable htermctyn + if (any(str_to_vec(htermctyn) == "N")) { + htermctyn <- str_to_vec(htermctyn) + stopifnot(length(htermctyn) == length(hterm)) + blankterm <- hterm[which(htermctyn == "N")] + outdata <- outdata |> + mutate(CVALUE = ifelse( + toupper(.data[["DPTVAR"]]) %in% toupper(blankterm), + "", + .data[["CVALUE"]] + )) + } + outdata |> + mutate(DPTVAR = "TIER") } #' Prepare Occurrence summary for Tabular Display @@ -160,32 +344,49 @@ occ_tier_summary <- function(datain, #' @return Flextable object #' @noRd post_occ_tier <- - function(occ_summ, riskyn = "N", ctrlgrp, statistics = NULL) { + function(occ_summ, riskyn = "N", ctrlgrp, sum_row = NULL, risklabels = tbl_risk_labels(), + pt_row = NULL, stathead = "n (%)") { occ_summ <- occ_summ |> setNames(c("hterm_summ", "lterm_summ")) final_cts <- occ_summ |> bind_rows() |> select(-any_of(c("DPTVARN", "DPTVALN"))) |> - inner_join(ord_by_ht(occ_summ, ctrlgrp), by = c("BYVAR1", "DPTVAL")) |> - select(-any_of(c("BYVAR1", "BYVAR1N"))) |> - select(-starts_with("CTRL_")) |> + inner_join(ord_by_ht(occ_summ, ctrlgrp), by = c("DPTVAR", "BYVAR1", "DPTVAL")) |> + select(-any_of(c("BYVAR1", "BYVAR1N", "CUTFL"))) |> + select(-starts_with("CTRL_")) + if (is.data.frame(sum_row) && nrow(sum_row) > 0) { + final_cts <- bind_rows(final_cts, sum_row |> mutate(DPTVARN = 0, DPTVALN = 0)) + } + if (is.data.frame(pt_row) && nrow(pt_row) > 0) { + final_cts <- bind_rows( + final_cts, pt_row |> mutate(DPTVALN = 0, DPTVARN = max(final_cts$DPTVARN, na.rm = TRUE) + 1) + ) + } + SUBGRPN <- var_start(final_cts, "SUBGRPN") + if (length(SUBGRPN) > 0) { + repvar <- SUBGRPN[length(SUBGRPN)] + } else { + repvar <- "TRTVAR" + } + final_cts <- final_cts |> mutate( DPTVAL = ifelse(.data[["DPTVALN"]] == 0, - .data[["DPTVAL"]], paste0("\t\t\t", str_to_title(.data[["DPTVAL"]])) + .data[["DPTVAL"]], paste0("\t\t\t", .data[["DPTVAL"]]) ), - DPTVAR = "TIER", - SUBGRPVARX = paste0("n (%)", strrep(" ", as.numeric(.data[["TRTVAR"]]))), + SUBGRPVARX = paste0(stathead, strrep(" ", as.numeric(as.factor(.data[[repvar]])))), SUBGRPVARXN = 1 ) - if (riskyn == "Y") { - # Rename variable containing RISK_CI based on type of statistic + # Rename variable containing RISK_CI based user inputs final_cts <- final_cts |> filter(!is.nan(.data[["RISK"]]), !is.infinite(.data[["RISK"]])) |> mutate( - !!paste0(statistics, " (CI)") := .data[["RISK_CI"]], - !!paste0("P-", "value") := .data[["PVALUE"]], - CVALUE = paste0(.data[["FREQ"]], " (", .data[["PCT"]], "%)") + !!risklabels$riskci := .data[["RISK_CI"]], + !!risklabels$risk := .data[["RISK"]], + !!risklabels$p := .data[["PVALUE"]], + !!risklabels$low := .data[["RISKCIL"]], + !!risklabels$up := .data[["RISKCIU"]], + !!risklabels$lowup := paste0("(", .data[["RISKCIL"]], ",", .data[["RISKCIU"]], ")") ) } final_cts @@ -208,15 +409,15 @@ ord_by_ht <- function(df, ctrlgrp) { filter(df[["hterm_summ"]], .data[["TRTVAR"]] != ctrlgrp), "DPTVAL" )) ) - + map(names(df), \(x) { match_var <- recode(x, "hterm_summ" = "DPTVAL", "lterm_summ" = "BYVAR1") df_out <- df[[x]] |> - select(any_of(c("BYVAR1", "DPTVAL"))) |> + select(all_of(c("DPTVAR", "BYVAR1", "DPTVAL"))) |> distinct() |> mutate(DPTVARN = match(.data[[match_var]], uniqHT)) |> filter(!is.na(.data[["DPTVARN"]])) - + if (x == "hterm_summ") { df_out <- df_out |> mutate(DPTVALN = 0) @@ -237,6 +438,57 @@ ord_by_ht <- function(df, ctrlgrp) { #' #' @return Name of control group #' @noRd -get_ctrlgrp <- function(df) { - levels(df[["TRTVAR"]])[1] +get_ctrlgrp <- function(df, col = 1) { + levels(df[["TRTVAR"]])[as.numeric(col)] +} + +#' Insert Overall/Summary Row +#' +#' @param datain Input dataset `ADAM` or intermediate within summary function +#' @param sum_row_label Label for Summary Row to be displayed, if Y. +#' @param byvaryn Include by variable or not? For single overally row, "N" +#' @param var Flag Variable to identify Any/Summary Rows +#' @inheritParams mcatstat +#' +#' @return dataframe with single overall row count +#' +#' @export +#' +#' @examples +#' data("adae") +#' summary_row_cat( +#' adae, +#' a_subset = "TRTEMFL == 'Y'" +#' ) +#' +summary_row_cat <- function(datain, + sum_row_label = "Any Term", + byvaryn = "N", + a_subset = NA, + pctdisp = "TRT", + sigdec = 2, + pctsyn = "Y", + var = "ANY", + uniqid = "USUBJID") { + if (nrow(datain) < 1) { + return(datain) + } + if (!(var %in% names(datain))) { + datain <- datain |> mutate(!!var := 1) + } + if (byvaryn == "N") { + byvar <- var_start(datain, "BYVAR") + datain <- datain |> + select(-starts_with(byvar[length(byvar)])) + } + datain |> + mcatstat( + a_subset = paste(na.omit(c(a_subset, glue("{var} == 1"))), collapse = "&"), + dptvar = var, + pctdisp = pctdisp, + uniqid = uniqid, + sigdec = sigdec, + pctsyn = pctsyn + ) |> + mutate(DPTVAL = sum_row_label) } diff --git a/R/process_vx_bar_plot.R b/R/process_vx_bar_plot.R index 005b5cf..de6cbc2 100644 --- a/R/process_vx_bar_plot.R +++ b/R/process_vx_bar_plot.R @@ -79,17 +79,17 @@ process_vx_bar_plot <- function(dataset_adsl, stopifnot(nrow(dataset_analysis) > 0) stopifnot(trtvar %in% toupper(names(dataset_adsl))) stopifnot("AVAL" %in% toupper(names(dataset_analysis))) - + if (!is.na(split_by) && str_squish(split_by) != "") { stopifnot(all(str_to_vec(split_by) %in% toupper(names(dataset_adsl)))) } - + adsl_out <- adsl_merge( dataset_adsl, adsl_subset, dataset_analysis ) - + mentry_out <- mentry( datain = adsl_out, subset = overall_subset, @@ -99,19 +99,20 @@ process_vx_bar_plot <- function(dataset_adsl, trtsort = trtsort, add_grpmiss = "N" ) - + mcatstat_out <- mcatstat( datain = mentry_out, a_subset = analysis_subset, denom_subset = denom_subset, uniqid = "USUBJID", dptvar = xvar, - pctdisp = pctdisp + pctdisp = pctdisp, + sparseyn = "N" ) |> mutate( YVAR = as.numeric(.data[[yvar]]), XVAR = factor(XVAR, - levels = unique(XVAR[order(DPTVALN)]) + levels = unique(XVAR[order(DPTVALN)]) ) ) plotdata <- plot_title_nsubj( @@ -120,6 +121,6 @@ process_vx_bar_plot <- function(dataset_adsl, var_start(mcatstat_out, "SUBGRP") ) |> plot_display_bign(mentry_out, bignyn = legendbign) - + return(plotdata) } diff --git a/R/ptly_utils.R b/R/ptly_utils.R index 794ec0a..f62fa4d 100644 --- a/R/ptly_utils.R +++ b/R/ptly_utils.R @@ -49,10 +49,10 @@ plotly_legend <- function(fig, lg_pos %in% c("top", "bottom", "right", "left") ) opts <- switch(lg_pos, - "bottom" = list(0.3, -0.2, "left", "top", "right", "top"), - "top" = list(0.3, 1.1, "left", "bottom", "right", "bottom"), - "left" = list(-0.2, 0.8, "right", "top", "right", "bottom"), - "right" = list(1, 0.8, "left", "top", "left", "bottom") + "bottom" = list(0.3, -0.2, "left", "top", "right", "top"), + "top" = list(0.3, 1.1, "left", "bottom", "right", "bottom"), + "left" = list(-0.2, 0.8, "right", "top", "right", "bottom"), + "right" = list(1, 0.8, "left", "top", "left", "bottom") ) } # Legend title as annotation if exists diff --git a/R/risk_stat.R b/R/risk_stat.R index cd26c0e..caaf95b 100644 --- a/R/risk_stat.R +++ b/R/risk_stat.R @@ -14,10 +14,11 @@ # options(warn = -1) +options(warn = -1) + #' Calculate Risk Statistics for treatment pairs from pre-processed Adverse Events data #' -#' @param datain Input dataset after pre_processing and running `mentry()` to *ADAE* data -#' @param a_subset Analysis Subset condition specific to categorical analysis. +#' @inheritParams mcatstat #' @param summary_by Measure to construct the summary by. Values: `'Patients' or 'Events'`. #' @param eventvar Event Variable to review by. Example: `'AEDECOD', 'AEBODSYS'`. #' @param ctrlgrp Treatment Control value. @@ -25,13 +26,17 @@ options(warn = -1) #' for `forest_plot()`. #' @param statistics Statistic to be calculated. Values: `'Risk Ratio' or 'Risk Difference'`. #' @param alpha Alpha value to determine confidence interval for risk calculation. Default: `0.05` -#' @param cutoff Incidence Cutoff Value; consider only terms with `incidence percentage > cutoff`. +#' @param cutoff_where Filter condition for incidence/pct. Consider only terms with +#' eg: "FREQ > 5" or "PCT <3". Must contain FREQ or PCT (count or percent) #' @param sort_opt How to sort terms, only for table/forest plot. #' Values: `'Ascending','Descending','Alphabetical'`. #' @param sort_var Metric to sort by. Values: `'Count','Percent','RiskValue'`. #' @param g_sort_by_ht For Forest Plot only - include sorting by high term/*BYVAR1*? #' Values: "Y"/"N". In the output, terms will be sorted by group first, then term. To be used #' along with `ht_dispyn` = Y in `ae_forest_plot()` +#' @param riskdiff_pct To display risk and CI as % if `statistic` = risk difference (Y/N) +#' @param hoveryn Include hover information (for graphs) Y/N +#' #' @return A dataset containing risk statistic calculations for given treatment pair(s). #' @export #' @@ -61,7 +66,7 @@ options(warn = -1) #' trtgrp = "Xanomeline High Dose", #' statistics = "Risk Ratio", #' alpha = 0.05, -#' cutoff = 2, +#' cutoff_where = "PCT > 2", #' sort_opt = "Ascending", #' sort_var = "Count" #' ) @@ -74,24 +79,26 @@ risk_stat <- trtgrp, statistics = "Risk Ratio", alpha = 0.05, - cutoff = 2, + cutoff_where = NA, sort_opt, sort_var, - g_sort_by_ht = "N") { + g_sort_by_ht = "N", + riskdiff_pct = "N", + sigdec = 1, + pctsyn = "Y", + hoveryn = "Y") { trtgrp <- str_to_vec(trtgrp, "~~") - stopifnot("Invalid Control Group" = ctrlgrp %in% unique(datain[["TRTVAR"]])) - stopifnot("Invalid Treatment Group" = all(trtgrp %in% unique(datain[["TRTVAR"]]))) stopifnot( "Invalid Risk Statistics; specify any one of `Risk Ratio` or `Risk Difference`" = - statistics %in% c("Risk Ratio", "Risk Difference") + tolower(statistics) %in% c("risk ratio", "risk difference") ) trt_list <- levels(datain[["TRTVAR"]]) ## getting equivalent data variable for given summary by selection summ_var <- recode(tolower(summary_by), - "participants" = "USUBJID", - "patients" = "USUBJID", - "events" = eventvar + "participants" = "USUBJID", + "patients" = "USUBJID", + "events" = eventvar ) ## get sort variables to apply sorting post risk statistics calculation if (sort_opt == "Alphabetical") { @@ -99,64 +106,69 @@ risk_stat <- } else { sort_var <- get_sort_var(sort_var) } - + id_vars <- c("BYVAR1", "DPTVAL") value_vars <- c("FREQ", "PCT", "DENOMN") - distinct_vars <- c("TRTVAR", id_vars, value_vars) - + mcat_out <- mcatstat( + datain = datain, + a_subset = a_subset, + uniqid = ifelse(tolower(summary_by) == "events", "ALLCT", summ_var), + dptvar = eventvar, + pctdisp = "TRT", + sigdec = sigdec, + pctsyn = pctsyn + ) + if (nrow(mcat_out) == 0) { + return(mcat_out) + } ## calculating risk statistics mapping over each treatment risk_out <- map(set_names(trtgrp), \(trt) { - datain <- datain |> + mcatin <- mcat_out |> filter(.data[["TRTVAR"]] %in% c(ctrlgrp, trt)) - - mcat_out <- mcatstat( - datain = datain, - a_subset = a_subset, - uniqid = ifelse(tolower(summary_by) == "events", "ALLCT", summ_var), - dptvar = eventvar, - pctdisp = "TRT" - ) - + if (!is.na(cutoff_where) && str_detect(cutoff_where, "PCT|FREQ")) { + mcat_cut <- mcatin |> + filter(!!!parse_exprs(cutoff_where)) |> + distinct(across(all_of(id_vars))) |> + mutate(CUTFL = "Y") + mcatin <- mcatin |> + left_join(mcat_cut, by = id_vars) |> + filter(CUTFL == "Y") + } + if (nrow(mcatin) < 1) { + return(data.frame()) + } rout <- add_risk_stat( - mcatout = mcat_out, + mcatout = mcatin, ctrlgrp = ctrlgrp, trtgrp = trt, id_vars = id_vars, value_vars = value_vars, statistics = statistics, - alpha = alpha, - cutoff = cutoff + riskdiff_pct = riskdiff_pct, + alpha = alpha ) - if (nrow(rout) > 0) { - rout <- rout |> - left_join(distinct(select(mcat_out, any_of( - c(distinct_vars) - ))), by = c("TRTVAR", intersect(id_vars, names(mcat_out)))) |> - mutate(across(any_of(c(value_vars)), \(x) as.double(x))) |> - select(c( - any_of(c(id_vars)), - contains("PVAL"), - contains("RISK"), - contains("CTRL_"), - starts_with("TRT"), - any_of(c(value_vars)), - "TOTAL_N" = "DENOMN" - )) + rout <- mcat_out |> + filter(.data[["TRTVAR"]] %in% c(ctrlgrp, trt) | + str_detect(.data[["TRTVAR"]], "Total")) |> + left_join(rout, by = intersect(names(mcat_out), names(rout))) |> + mutate(across(any_of(c(value_vars)), \(x) as.double(x)), TOTAL_N = DENOMN) |> + filter(!.data[["RISK"]] %in% c(NA, Inf, NaN)) } rout }) |> bind_rows() - - if (nrow(risk_out) > 0) { + if (nrow(risk_out) > 0 && ncol(risk_out) > 1) { ## Add hover_text and order the final table risk_out <- risk_out |> - risk_hover_text(summary_by, eventvar) |> ord_summ_df(sort_var, sort_opt, g_sort_by_ht) risk_out[["TRTVAR"]] <- factor(risk_out[["TRTVAR"]], - levels = trt_list, ordered = TRUE + levels = trt_list, ordered = TRUE ) + if (hoveryn == "Y") { + risk_out <- risk_hover_text(risk_out, summary_by, eventvar) + } } risk_out } @@ -176,8 +188,8 @@ add_risk_stat <- function(mcatout, id_vars = c("BYVAR1", "DPTVAL"), value_vars = c("FREQ", "PCT", "DENOMN"), statistics = "Risk Ratio", - alpha = 0.05, - cutoff = 2) { + riskdiff_pct = "N", + alpha = 0.05) { if (nrow(mcatout) < 1 || !all(c(ctrlgrp, trtgrp) %in% unique(mcatout$TRTVAR))) { return(data.frame()) } @@ -192,18 +204,11 @@ add_risk_stat <- function(mcatout, PCT = as.double(.data[["PCT"]]) ) |> pivot_wider( - id_cols = any_of(c(id_vars)), + id_cols = any_of(c(id_vars, "CUTFL")), names_from = "TRTCD", values_from = any_of(c(value_vars)) ) |> - mutate(across(where(is.numeric), ~ replace_na(.x, 0))) |> - filter(.data[["PCT_CTRLGRP"]] > cutoff | - .data[["PCT_TRTGRP"]] > cutoff) - - if (nrow(risk_prep) < 1) { - message("`cutoff` value provided is too big, please specify a smaller `cutoff` value") - return(data.frame(NULL)) - } + mutate(across(where(is.numeric), ~ replace_na(.x, 0))) ## Calculate Risk Statistics in `risk_out` column using `calc_risk_stat` risk_prep <- risk_prep |> group_by(across(any_of(c(id_vars)))) |> @@ -216,13 +221,19 @@ add_risk_stat <- function(mcatout, calc_risk_stat )) |> ungroup() + # Show as percent if required + if (tolower(statistics) == "risk difference" && riskdiff_pct == "Y") { + ppct <- 100 + } else { + ppct <- 1 + } ## Extract Risk Statistics from the added `risk_out` column by each row rowwise(risk_prep) |> mutate( - PVALUE = flatten(.data[["risk_out"]])[["pval"]], - RISK = flatten(.data[["risk_out"]])[["risk"]], - RISKCIL = flatten(.data[["risk_out"]])[["low_ci"]], - RISKCIU = flatten(.data[["risk_out"]])[["upp_ci"]] + PVALUE = round(flatten(.data[["risk_out"]])[["pval"]], 4), + RISK = round(ppct * flatten(.data[["risk_out"]])[["risk"]], 3), + RISKCIL = round(ppct * flatten(.data[["risk_out"]])[["low_ci"]], 2), + RISKCIU = round(ppct * flatten(.data[["risk_out"]])[["upp_ci"]], 2) ) |> mutate( ADJPVALUE = p.adjust(.data[["PVALUE"]], method = "fdr"), @@ -235,8 +246,7 @@ add_risk_stat <- function(mcatout, ACTIVE = trtgrp ) |> rename(CTRL_N = "FREQ_CTRLGRP", CTRL_PCT = "PCT_CTRLGRP") |> - select(-c("risk_out", any_of(starts_with("DENOMN")), any_of(ends_with("TRTGRP")))) |> - pivot_longer(c("CTRL", "ACTIVE"), values_to = "TRTVAR", names_to = NULL) + select(-c("risk_out", any_of(starts_with("DENOMN")), any_of(ends_with("TRTGRP")))) } #' Calculate Risk Statistics @@ -264,21 +274,17 @@ calc_risk_stat <- ), nrow = 2 ) - + if (statistic == "Risk Difference") { risk_mat <- suppressWarnings(riskdiff_wald(risk_mat, conf.level = 1 - alpha)) } else { risk_mat <- - suppressWarnings(epitools::riskratio.wald(risk_mat, conf.level = 1 - alpha)) + suppressWarnings( + epitools::riskratio.wald(risk_mat, conf.level = 1 - alpha, correction = TRUE) + ) } - - list( - risk = round(risk_mat$measure[2, 1], 3), - pval = round(risk_mat$p.value[2, 3], 4), - low_ci = round(risk_mat$measure[2, 2], 2), - upp_ci = round(risk_mat$measure[2, 3], 2) - ) + extract_riskstats(risk_mat, statistic) } #' Calculate Risk difference @@ -290,12 +296,7 @@ calc_risk_stat <- #' input data can be one of the following: r x 2 table, vector of numbers from a #' contigency table (will be transformed into r x 2 table in row-wise order), #' or single factor or character vector that will be combined with y into a table. -#' @param y single factor or character vector that will be combined with x into a table -#' (default is NULL) #' @param conf.level confidence level (default is 0.95) -#' @param rev reverse order of "rows", "colums", "both", or "neither" (default) -#' @param correction Yate's continuity correction -#' @param verbose To return more detailed results #' #' @return a list containg a data,measure,p.value,correction #' @export @@ -306,22 +307,9 @@ calc_risk_stat <- #' conf.level = 0.95 #' ) riskdiff_wald <- - function(x, y = NULL, - conf.level = 0.95, - rev = "neither", - correction = FALSE, - verbose = FALSE) { - if (is.matrix(x) && !is.null(y)) { - stop("y argument should be NULL") - } - if (is.null(y)) { - x <- epitools::epitable(x, rev = rev) - } else { - x <- epitools::epitable(x, y, rev = rev) - } + function(x, conf.level = 0.95) { + x <- epitools::epitable(x, rev = "neither") tmx <- epitools::table.margins(x) - p.exposed <- sweep(tmx, 2, tmx["Total", ], "/") - p.outcome <- sweep(tmx, 1, tmx[, "Total"], "/") Z <- qnorm(0.5 * (1 + conf.level)) nr <- nrow(x) wald <- matrix(NA, nr, 3) @@ -331,16 +319,14 @@ riskdiff_wald <- b <- x[i, 1] c <- x[1, 2] d <- x[1, 1] - - # point estimate of risk difference - est <- (a / (a + b)) - (c / (c + d)) - # standard error of risk difference - se_RD <- sqrt((a * b / (a + b)^3) + (c * d / (c + d)^3)) - - ci <- est + c(-1, 1) * Z * se_RD + p2 <- a / (a + b) + p1 <- c / (c + d) + est <- p1 - p2 + se_RD <- sqrt((p1 * (1 - p1) / (c + d)) + (p2 * (1 - p2) / (a + b))) + ci <- (est + c(-1, 1) * Z * se_RD) wald[i, ] <- c(est, ci) } - pv <- epitools::tab2by2.test(x, correction = correction) + pv <- epitools::tab2by2.test(x, correction = FALSE) colnames(wald) <- c("estimate", "lower", "upper") rownames(wald) <- rownames(x) cn2 <- paste( @@ -349,32 +335,15 @@ riskdiff_wald <- "C.I." ) names(dimnames(wald)) <- c(names(dimnames(x))[1], cn2) - - rr <- list( - x = x, - data = tmx, - p.exposed = p.exposed, - p.outcome = p.outcome, - measure = wald, - conf.level = conf.level, - p.value = pv$p.value, - correction = pv$correction - ) + rrs <- list( data = tmx, measure = wald, p.value = pv$p.value, correction = pv$correction ) - - attr(rr, "method") <- "Unconditional MLE & normal approximation (Wald) CI" attr(rrs, "method") <- "Unconditional MLE & normal approximation (Wald) CI" - - if (verbose == FALSE) { - rrs - } else { - rr - } + rrs } #' Add Risk Statistics specific Hover Text to data @@ -424,3 +393,31 @@ risk_hover_text <- function(df, summary_by, eventvar) { ) ) } + +#' Extract Risk Statistics +#' +#' @return list of statistics +#' @noRd +extract_riskstats <- function(risk_mat, statistic) { + risk <- risk_mat$measure[2, 1] + pval <- risk_mat$p.value[2, 3] + low_ci <- risk_mat$measure[2, 2] + upp_ci <- risk_mat$measure[2, 3] + + if (statistic == "Risk Difference") { + out <- list( + risk = 0 - risk, + pval = pval, + upp_ci = 0 - low_ci, + low_ci = 0 - upp_ci + ) + } else { + out <- list( + risk = risk, + pval = pval, + upp_ci = upp_ci, + low_ci = low_ci + ) + } + out +} diff --git a/R/riskdiff_wald.R b/R/riskdiff_wald.R deleted file mode 100644 index 7f2f90d..0000000 --- a/R/riskdiff_wald.R +++ /dev/null @@ -1,117 +0,0 @@ -# Copyright 2024 Pfizer Inc -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -#' Calculate Risk difference -#' -#' Function to calculate risk difference by unconditional maximum likelihood estimation (Wald) -#' for any given treatment pairs. -#' -#' @param x input data -#' input data can be one of the following: r x 2 table, vector of numbers from a -#' contigency table (will be transformed into r x 2 table in row-wise order), -#' or single factor or character vector that will be combined with y into a table. -#' -#' @param y single factor or character vector that will be combined with x into a table -#' (default is NULL) -#' -#' @param conf.level confidence level (default is 0.95) -#' -#' @param rev reverse order of "rows", "colums", "both", or "neither" (default) -#' -#' @param correction Yate's continuity correction -#' -#' @param verbose To return more detailed results -#' -#' @return a list containg a data,measure,p.value,correction -#' @export -#' -#' @examples -#' riskdiff_wald( -#' x = matrix(c(178, 79, 1411, 1486), 2, 2), -#' conf.level = 0.95, -#' rev = c("neither", "rows", "columns", "both"), -#' correction = FALSE, -#' verbose = FALSE -#' ) -riskdiff_wald <- - function(x, y = NULL, - conf.level = 0.95, - rev = c("neither", "rows", "columns", "both"), - correction = FALSE, - verbose = FALSE) { - if (is.matrix(x) && !is.null(y)) { - stop("y argument should be NULL") - } - if (is.null(y)) { - x <- epitable(x, rev = rev) - } else { - x <- epitable(x, y, rev = rev) - } - tmx <- table.margins(x) - p.exposed <- sweep(tmx, 2, tmx["Total", ], "/") - p.outcome <- sweep(tmx, 1, tmx[, "Total"], "/") - Z <- qnorm(0.5 * (1 + conf.level)) - nr <- nrow(x) - wald <- matrix(NA, nr, 3) - wald[1, 1] <- 1 - for (i in 2:nr) { - a <- x[i, 2] - b <- x[i, 1] - c <- x[1, 2] - d <- x[1, 1] - - # point estimate of risk difference - est <- (a / (a + b)) - (c / (c + d)) - # standard error of risk difference - se_RD <- sqrt((a * b / (a + b)^3) + (c * d / (c + d)^3)) - - ci <- est + c(-1, 1) * Z * se_RD - wald[i, ] <- c(est, ci) - } - pv <- tab2by2.test(x, correction = correction) - colnames(wald) <- c("estimate", "lower", "upper") - rownames(wald) <- rownames(x) - cn2 <- paste( - "risk difference with", - paste(100 * conf.level, "%", sep = ""), - "C.I." - ) - names(dimnames(wald)) <- c(names(dimnames(x))[1], cn2) - - rr <- list( - x = x, - data = tmx, - p.exposed = p.exposed, - p.outcome = p.outcome, - measure = wald, - conf.level = conf.level, - p.value = pv$p.value, - correction = pv$correction - ) - rrs <- list( - data = tmx, - measure = wald, - p.value = pv$p.value, - correction = pv$correction - ) - - attr(rr, "method") <- "Unconditional MLE & normal approximation (Wald) CI" - attr(rrs, "method") <- "Unconditional MLE & normal approximation (Wald) CI" - - if (verbose == FALSE) { - rrs - } else { - rr - } - } diff --git a/R/scatter_plot.R b/R/scatter_plot.R index 9cbb720..2bb8244 100644 --- a/R/scatter_plot.R +++ b/R/scatter_plot.R @@ -1,3 +1,17 @@ +# Copyright 2024 Pfizer Inc +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# #' Create Scatter Plot #' #' @param datain `data.frame` retrieved from `process_vx_scatter_data()`. @@ -126,10 +140,10 @@ scatter_plot <- stopifnot(series_var %in% names(datain)) stopifnot(length(series_opts$shape) == length(series_opts$color)) stopifnot(length(series_opts$size) == length(series_opts$color)) - + legend_label <- legend_opts$label series_labels <- series_leg_lab(datain, series_var, series_labelvar) - + g <- ggplot( datain, aes( @@ -232,15 +246,15 @@ process_vx_scatter_data <- if (!is.na(split_by) && str_squish(split_by) != "") { stopifnot(all(str_to_vec(split_by) %in% toupper(names(dataset_adsl)))) } - + adsl_sub <- adsl_merge( dataset_adsl, adsl_subset, dataset_analysis ) - + stopifnot(nrow(adsl_sub) > 0) - + mentry_df <- adsl_sub |> mentry( subset = analysis_subset, @@ -250,19 +264,19 @@ process_vx_scatter_data <- add_grpmiss = "N", pop_fil = "Overall Population" ) - + stopifnot(nrow(mentry_df) > 0) - + a_dsin <- mentry_df |> mutate(Vars = case_when( !!!parse_exprs(xvar) ~ "XVAR", !!!parse_exprs(yvar) ~ "YVAR" )) - + if (all(is.na(a_dsin[["Vars"]]))) { stop("`xvar/yvar` are invalid") } - + a_dsin_ <- pivot_wider( a_dsin, id_cols = c(SUBJID, TRTVAR, starts_with("SUBGRPVAR")), @@ -270,6 +284,6 @@ process_vx_scatter_data <- values_from = AVAL ) |> plot_display_bign(mentry_df, bignyn = legendbign) - + return(a_dsin_) } diff --git a/R/surv_utils.R b/R/surv_utils.R index c73815f..df78992 100644 --- a/R/surv_utils.R +++ b/R/surv_utils.R @@ -1,3 +1,17 @@ +# Copyright 2024 Pfizer Inc +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# #' Process data for Survival Analysis #' #' @inheritParams process_vx_scatter_data @@ -46,7 +60,7 @@ surv_pre_processor <- function(dataset_adsl, stopifnot( "Please provide a valid Censoring variable" = censor_var %in% toupper(names(dataset_analysis)) ) - + if (!is.na(split_by) && str_squish(split_by) != "") { stopifnot(all(str_to_vec(split_by) %in% toupper(names(dataset_adsl)))) } @@ -66,8 +80,8 @@ surv_pre_processor <- function(dataset_adsl, subgrpvar = str_remove_all(split_by, " ") ) plot_display_bign(mentry_out, - mentry_data = mentry_out, - bignyn = "N" + mentry_data = mentry_out, + bignyn = "N" ) } @@ -79,8 +93,8 @@ surv_pre_processor <- function(dataset_adsl, #' @noRd #' pairwise_surv_stats <- function(datain) { - pairs <- utils::combn(sort(unique(datain[["TRTSORT"]])), 2) - + pairs <- combn(sort(unique(datain[["TRTSORT"]])), 2) + pair_stat <- map_chr(seq_len(ncol(pairs)), \(i) { trt_index <- pairs[, i] pair_data <- datain |> @@ -106,6 +120,6 @@ pairwise_surv_stats <- function(datain) { "HR ({trt_pair[1]} vs {trt_pair[2]}) = {HR}, 95% CI ({cil}, {ciu}), 2-sided p = {round_f(pval_2s, 4)}, 1-sided p = {pval_1s}" # nolint ) }) - + paste0(pair_stat, collapse = "\n") } diff --git a/R/tbl_display.R b/R/tbl_display.R index 173c733..fafc907 100644 --- a/R/tbl_display.R +++ b/R/tbl_display.R @@ -80,29 +80,32 @@ tbl_processor <- function(datain, if (any(keepvars == "")) { rep <- rep |> select( - -any_of(c("DENOMN", "TRTTXT", "FREQ", "PCT", "XVAR", "TOTAL_N", "TRTPAIR", dropvars)), + -any_of(c( + "DENOMN", "TRTTXT", "FREQ", "PCT", "CPCT", "XVAR", + "TOTAL_N", "TRTPAIR", dropvars + )), -starts_with("HOVER") ) } else { rep <- rep |> select( any_of(c(BYVAR, "DPTVAR", "DPTVAL", - "DPTVAL" = "STAT", "TRTVAR", SUBGRP, BYVARN, SUBGRPN, - "DPTVARN", "DPTVALN", "DPTVALN" = "STATN", "CVALUE", "CN", keepvars + "DPTVAL" = "STAT", "TRTVAR", SUBGRP, BYVARN, SUBGRPN, + "DPTVARN", "DPTVALN", "DPTVALN" = "STATN", "CVALUE", "CN", keepvars )) ) } - + # IF treatment, subgroup exists, pivot and perform operations if (any(c("TRTVAR", SUBGRP) %in% names(rep))) { # Workaround for pivot_wider to accept "duplicate" spanned column names (Total) rep <- rep |> mutate(across(any_of(SUBGRP), ~ - ifelse( - get(paste0(cur_column(), "N")) == 9999, - paste0(.x, paste(rep(" ", which(SUBGRP == cur_column())), collapse = "")), - .x - ))) |> + ifelse( + get(paste0(cur_column(), "N")) == 9999, + paste0(.x, paste(rep(" ", which(SUBGRP == cur_column())), collapse = "")), + .x + ))) |> arrange(across(any_of(c("TRTVAR", SUBGRPN)))) |> select(-any_of(SUBGRPN)) |> pivot_wider( @@ -117,6 +120,15 @@ tbl_processor <- function(datain, ~ ifelse(.data[["CN"]] == "C", gsub("^-$", "0", .x), .x) )) } + # Only N and uniqN should be 0 instead of - + if ("DPTVAL" %in% names(rep) && any(c("n", "nmiss", "nobs") %in% unique(rep$DPTVAL))) { + rep <- rep |> mutate(across( + -any_of(c(BYVAR, "DPTVAR", "DPTVAL")) & where(is.character), + ~ if_else(.data[["DPTVAL"]] %in% c("n", "nmiss", "nobs"), + gsub("^-$", "0", .x), .x + ) + )) + } # If additional dataset is given: if (is.data.frame(extra_df)) { rep <- rep |> inner_join(extra_df, by = extra_mergeby) @@ -134,8 +146,8 @@ tbl_processor <- function(datain, rep <- rep |> mutate( DPTVAL = ifelse(.data[["CN"]] == "N", - recode(.data[["DPTVAL"]], !!!statn), - .data[["DPTVAL"]] + recode(.data[["DPTVAL"]], !!!statn), + .data[["DPTVAL"]] ) ) } @@ -158,7 +170,7 @@ tbl_processor <- function(datain, # arrange rows and then proceed; combine groups if dptlabel is suitably passed rep |> arrange(across(any_of(c(BYVARN, "DPTVARN", "DPTVALN")))) |> - filter(if_all(any_of("DPTVAL"), ~ !grepl("NONE$|_NONE_$|JOIN$", toupper(.x)))) |> + filter(if_all(any_of("DPTVAL"), ~ !grepl("_NONE_$|_JOIN_$", toupper(.x)))) |> select(any_of(c(BYVAR, "DPTVAR", "DPTVAL")), everything(), -any_of(BYVARN)) } @@ -175,18 +187,25 @@ set_cat_labels <- function(data, dptlabel) { vals <- data |> arrange(.data[["DPTVARN"]]) |> - pull(.data[["DPTVAR"]]) |> + select(all_of(c("DPTVAR", "DPTVARN"))) |> unique() # # Labels for categories if (all(is.na(dptlabel))) { - dptlabel <- setNames(str_to_title(vals), vals) + dptlabel <- setNames(str_to_title(vals[["DPTVAR"]]), vals[["DPTVAR"]]) + label_df <- data |> + mutate(DPTVAR = recode(.data[["DPTVAR"]], !!!dptlabel)) } else { cats <- str_to_vec(dptlabel) - stopifnot("Supply same number of labels as variables" = length(cats) == length(vals)) - dptlabel <- setNames(str_to_vec(dptlabel), vals) + stopifnot( + "Supply same number of labels as variables" = length(cats) == length(vals[["DPTVARN"]]) + ) + label_df <- bind_cols(vals, new_var = cats) |> + select(-DPTVAR) |> + right_join(data, by = "DPTVARN") |> + mutate(DPTVAR = new_var) |> + select(-new_var) } - data |> - mutate(DPTVAR = recode(.data[["DPTVAR"]], !!!dptlabel)) + label_df } #' Add empty row for dptvar @@ -205,18 +224,25 @@ add_row_var <- function(datain, len <- length(addrowvar) dptvaln <- seq(0, 0.9, length.out = len) pad <- strrep("\t\t", seq_len(len + 1) - 1) + if ("DPTVAL" %in% names(datain)) { + val_var <- "DPTVAL" + } else { + val_var <- "DPTVAR" + } + val_varn <- paste0(val_var, "N") purrr::map(seq_along(addrowvar), \(i) { + varn <- paste0(addrowvar[[i]], "N") var <- addrowvar[[i]] datain |> - distinct(across(any_of(unique(c(byvar, byvarn, var, "DPTVARN"))))) |> + distinct(across(any_of(unique(c(byvar, byvarn, var, varn))))) |> mutate( - DPTVALN = dptvaln[i], - DPTVAL = paste0(pad[i], .data[[var]]) + !!val_varn := dptvaln[i], + !!val_var := paste0(pad[i], .data[[var]]) ) }) |> bind_rows( datain |> - mutate(DPTVAL = paste0(pad[len + 1], .data[["DPTVAL"]])) + mutate(!!val_var := paste0(pad[len + 1], .data[[val_var]])) ) } @@ -263,6 +289,7 @@ clear_dup_rows <- function(col, target) { #' @param dpthead String to become name of the column containing categories (`DPTVAL`) in output. #' @param font Font face for text inside table #' @param fontsize Font size for text inside table +#' @param boldheadyn Y/N to determine if table header should be bold #' #' @return flextable object #' @export @@ -296,10 +323,11 @@ clear_dup_rows <- function(col, target) { #' dpthead = " " #' ) tbl_display <- function(datain, - bylabel, + bylabel = NA, dpthead = " ", font = "Arial", - fontsize = 10) { + fontsize = 10, + boldheadyn = "N") { BYVAR <- var_start(datain, "BYVAR") # If by variables exist, process for aptly merging the columns in output lenby <- length(BYVAR) @@ -318,8 +346,8 @@ tbl_display <- function(datain, ))) tout <- rep |> flextable(col_keys = grep("DPTVARN|^CN$|DPTVALN", - names(rep), - invert = TRUE, value = TRUE + names(rep), + invert = TRUE, value = TRUE )) |> ftExtra::span_header("_") |> font(fontname = font, part = "all") |> @@ -337,6 +365,10 @@ tbl_display <- function(datain, hline(i = rowh[rowh != 0], j = b:lenby, border = small_border) } } + if (boldheadyn != "N") { + tout <- tout |> + bold(part = "header") + } tout |> hline(j = (lenby + 1):last, border = small_border) |> border_outer(part = "all", border = big_border) |> @@ -344,3 +376,23 @@ tbl_display <- function(datain, vline(part = "all", border = small_border) |> fix_border_issues() } + +#' Return table if output is empty +#' +#' @param text Text to display under table creation +#' +#' @return flextable output +#' @export +#' +#' @examples +#' empty_tbl() +empty_tbl <- function(text = "No participant meets the reporting criteria") { + flextable(data.frame("X" = text)) |> + set_header_labels(X = "Table not created") |> + bold(part = "header") |> + font(fontname = "Arial", part = "all") |> + theme_box() |> + align(align = "center", part = "all") |> + autofit() +} + diff --git a/R/tornado_plot.R b/R/tornado_plot.R index 1379196..6a153c1 100644 --- a/R/tornado_plot.R +++ b/R/tornado_plot.R @@ -1,3 +1,17 @@ +# Copyright 2024 Pfizer Inc +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# #' Tornado Plot #' #' @param datain An input dataframe retrieved from `process_tornado_data`()`. @@ -15,15 +29,13 @@ #' dataset_adsl = tornado_plot_data[["adsl"]], #' dataset_analysis = tornado_plot_data[["adae"]], #' adsl_subset = "SAFFL == 'Y'", -#' analysis_subset = NA_character_, -#' ae_filter = "Treatment emergent", +#' analysis_subset = "TRTEMFL == 'Y'", #' obs_residual = "30", #' fmq_data = NA, -#' ae_catvar = "AESEV", +#' ae_catvar = "AESEV/AESEVN", #' trtvar = "ARMCD", #' trt_left = "A", #' trt_right = "A", -#' pop_fil = "Overall Population", #' pctdisp = "TRT", #' denom_subset = NA_character_, #' legendbign = "N", @@ -77,7 +89,7 @@ tornado_plot <- function(datain, "XVAR Treatment values not in data" = all(c("XVAR", "trt_left", "trt_right") %in% names(datain)) ) - + # Tornado plot - Flipped left and right Bar plots to ref line at 0 g_plot <- datain |> ggplot(aes(x = XVAR)) + @@ -93,7 +105,7 @@ tornado_plot <- function(datain, width = bar_width ) + coord_flip() - + names(series_opts) <- rev(names(series_opts)) # Adding Labels, Breaks, Colors, Ticks, Themes g_plot + @@ -120,8 +132,8 @@ tornado_plot <- function(datain, #' @param dataset_adsl (`data.frame`) ADSL dataset. #' @param dataset_analysis (`data.frame`) ADAE dataset. #' @param adsl_subset (`string`) Subset condition to be applied on `dataset_adsl`. -#' @param analysis_subset Subset conditions for overall data. -#' @param ae_catvar Categorical variable for severity analysis. +#' @param analysis_subset Subset conditions for `dataset_analysis` +#' @param ae_catvar Categorical variable for severity analysis and order variable. eg; "ASEV/ASEVN" #' @param denom_subset Subset condition to be applied to data set for #' calculating denominator. #' @param split_by (`string`) By variable for stratification. @@ -132,7 +144,7 @@ tornado_plot <- function(datain, #' @param yvar Categorical Analysis variable for Y axis #' @param pctdisp Method to calculate denominator (for %) by #' Possible values: "TRT","VAR","COL","SUBGRP","CAT","NONE","NO","DPTVAR" -#' @param pop_fil Population Filter for data set: Name of flag variable. +#' @param subset Overall subset for data set. eg: "EFFFL == 'Y'" #' eg: `"SAFFL"`, `"EFFFL"` or `NA` for Overall Population. #' @param legendbign (`string`) Display BIGN in Legend (`Y/N`). #' @inheritParams ae_pre_processor @@ -140,7 +152,8 @@ tornado_plot <- function(datain, #' @details #' \itemize{ #' \item ae_catvar grouping variable for severity like AESEV(MILD, MODERATE, -#' SEVERE). It must also have it's numeric variable in the dataset. +#' SEVERE). It must be passed "/" separated with its numeric variable. +#' eg: ASEV/ASEVN; ATOXGR/ATOXGRN #' \item yvar(dptvar) Adverse Event category, derived term from AE. #' Possible Values: AEBODSYS, AEDECOD, AEHLT, AEHLGT. #' } @@ -154,15 +167,13 @@ tornado_plot <- function(datain, #' dataset_adsl = tornado_plot_data[["adsl"]], #' dataset_analysis = tornado_plot_data[["adae"]], #' adsl_subset = "SAFFL == 'Y'", -#' analysis_subset = NA_character_, -#' ae_filter = "Treatment emergent", +#' analysis_subset = "TRTEMFL == 'Y'", #' obs_residual = "30", #' fmq_data = NA, -#' ae_catvar = "AESEV", +#' ae_catvar = "AESEV/AESEVN", #' trtvar = "ARMCD", #' trt_left = "A", #' trt_right = "A", -#' pop_fil = "Overall Population", #' pctdisp = "TRT", #' denom_subset = NA_character_, #' legendbign = "N", @@ -174,7 +185,6 @@ process_tornado_data <- dataset_analysis, adsl_subset = NA_character_, analysis_subset = NA_character_, - ae_filter = "Any Event", obs_residual = NA_real_, fmq_data = NULL, split_by = NA_character_, @@ -183,50 +193,48 @@ process_tornado_data <- trt_left, trt_right, trtsort = NA_character_, - pop_fil = "Overall Population", + subset = NA_character_, pctdisp = "TRT", denom_subset = NA_character_, legendbign = "N", - yvar) { + yvar = "AESOC") { # Check data sets are not empty - # stopifnot("ADSL data is empty" = nrow(dataset_adsl) != 0) + stopifnot("ADSL data is empty" = nrow(dataset_adsl) != 0) stopifnot("Analysis data is empty" = nrow(dataset_analysis) != 0) - - # stopifnot(all(c(ae_catvar, paste0(ae_catvar, "N")) %in% - # toupper(names(dataset_analysis)))) - - # Merge with adsl - # adsl_merged <- adsl_merge( - # adsl = dataset_adsl, - # adsl_subset = adsl_subset, - # dataset_add = dataset_analysis - # ) - + maxvar <- str_to_vec(ae_catvar, "/") + numvar <- ifelse(length(maxvar) > 1, maxvar[2], paste0(maxvar[1], "N")) + stopifnot(all(c(numvar, maxvar[1]) %in% toupper(names(dataset_analysis)))) # Pre-Processing data for Adverse Event data_pre <- ae_pre_processor( datain = dataset_analysis, - ae_filter = ae_filter, + subset = analysis_subset, obs_residual = obs_residual, - fmq_data = fmq_data + fmq_data = fmq_data, + max_sevctc = "SEV", + sev_ctcvar = numvar, + hterm = character(0), + lterm = yvar + ) + # Merge with adsl + adsl_merged <- adsl_merge( + adsl = dataset_adsl, + adsl_subset = adsl_subset, + dataset_add = data_pre$data ) - # Data mentry processing mentry_out <- mentry( - data_pre$data, - subset = analysis_subset, - byvar = ae_catvar, + adsl_merged, + subset = subset, + byvar = maxvar, subgrpvar = str_remove_all(split_by, " "), trtvar = trtvar, - trtsort = trtsort, - pop_fil = pop_fil - ) |> - group_by(!!sym(trtvar), SUBJID, !!sym(yvar)) |> - filter(!!sym(paste0(ae_catvar, "N")) == max(!!sym(paste0(ae_catvar, "N")))) + trtsort = trtsort + ) stopifnot( "Given Subsets not present in Analysis Data" = nrow(mentry_out) != 0 ) - + # Summary analysis for tornado plot dataset mcatstat_out <- mcatstat( datain = mentry_out, @@ -234,24 +242,25 @@ process_tornado_data <- denom_subset = denom_subset, uniqid = "USUBJID", dptvar = yvar, - pctdisp = pctdisp + pctdisp = pctdisp, + sparseyn = "N" ) |> (\(x) { mutate(x, - YVAR = as.numeric(PCT), - XVAR = factor(x$XVAR, - levels = rev(x |> - group_by(XVAR) |> - mutate(XVARPCTS = sum(as.numeric(PCT))) |> - arrange(desc(.data$XVARPCTS)) |> - distinct(XVAR) |> - pull(XVAR)) - ), - BYVAR1 = factor(x$BYVAR1, levels = rev(unique(x$BYVAR1))) + YVAR = as.numeric(PCT), + XVAR = factor(x$XVAR, + levels = rev(x |> + group_by(XVAR) |> + mutate(XVARPCTS = sum(as.numeric(PCT))) |> + arrange(desc(.data$XVARPCTS)) |> + distinct(XVAR) |> + pull(XVAR)) + ), + BYVAR1 = factor(x$BYVAR1, levels = rev(unique(x$BYVAR1))) ) |> pivot_wider(names_from = "TRTVAR", values_from = "YVAR") |> mutate(trt_left = !!sym(trt_left), trt_right = !!sym(trt_right)) })() - + # Dataset for tornado plot plot_title_nsubj( mentry_out, diff --git a/R/utils.R b/R/utils.R index 99dc654..5ef98c6 100644 --- a/R/utils.R +++ b/R/utils.R @@ -34,17 +34,16 @@ data_attrib <- function(datain) { names(datain), function(x) { ifelse(is.null(attr(datain[[x]], "label")), - x, attr(datain[[x]], "label") + x, attr(datain[[x]], "label") ) } )) ), c("VAR_NAMES", "VAR_LABEL") ) - data_attr + return(data_attr) } - ## Identify any variable names starting with given string. Useful for Byvar,subgrp identification ## #' Find column names with starting pattern #' @@ -122,7 +121,7 @@ round_f <- function(x, digits = 2) { fmtrd <- function(f, d = 2, ...) { function(x) { dc <- do.call(f, args = list(x, na.rm = TRUE, ...)) - ifelse(is.na(dc), "-", round_f(dc, d)) + return(ifelse(is.na(dc), "-", round_f(dc, d))) } } @@ -280,7 +279,7 @@ split_data_by_var <- function(datain, split_section_headers <- function(datain, split_by = "", split_by_prefix = "", - split_lab = "", + split_lab = " ", sep = "~") { stopifnot(is.data.frame(datain)) if (split_by == "" && split_by_prefix == "") { @@ -292,17 +291,17 @@ split_section_headers <- function(datain, split_by <- var_start(datain, split_by_prefix) stopifnot("No variables with split_by_prefix" = length(split_by) > 0) } - + if (split_lab != "") { split_lab <- str_to_vec(split_lab) } else { split_lab <- NA_character_ } - + header_list <- datain |> group_by(!!!syms(split_by)) |> group_keys() - + map(seq_along(split_by), \(x) { header_list |> mutate( @@ -365,14 +364,14 @@ dataset_vignette <- function(df = NULL, disp_vars = NULL, subset = NA_character_ out <- out |> filter(!!!parse_exprs(subset)) } - + if (!is.null(disp_vars)) { hide_columns <- which(!(colnames(out) %in% str_to_vec(disp_vars))) cols_to_hide <- list(list(targets = hide_columns - 1, visible = FALSE)) } else { cols_to_hide <- list() } - + DT::datatable( out, rownames = FALSE, @@ -411,20 +410,18 @@ dataset_vignette <- function(df = NULL, disp_vars = NULL, subset = NA_character_ #' @return Data frame with `Big N`. #' @noRd add_bigN <- function(data, dsin, grpvar, modvar, subjid = "USUBJID") { + newvar <- paste0(modvar, "_BIGN") data <- dsin |> group_by(!!!syms(grpvar)) |> distinct(!!!syms(subjid)) |> summarise(BIGN = n()) |> (\(.) left_join(data, ., by = grpvar))() |> - mutate(across(any_of(modvar), - ~ paste0(.x, " (N=", .data[["BIGN"]], ")"), - .names = "{.col}_BIGN" - )) |> + mutate(!!newvar := paste0(.data[[modvar]], " (N=", .data[["BIGN"]], ")")) |> select(-all_of("BIGN")) if (length(modvar) == 1 && is.factor(data[[modvar]])) { newvar <- paste0(modvar, "_BIGN") data[[newvar]] <- factor(data[[newvar]], - levels = unique(data[[newvar]][order(data[[modvar]])]) + levels = unique(data[[newvar]][order(data[[modvar]])]) ) } data @@ -454,8 +451,7 @@ add_bigN <- function(data, dsin, grpvar, modvar, subjid = "USUBJID") { #' msumstat( #' adsl_entry, #' dptvar = "AGE", -#' statvar = "meansd", -#' sigdec = 2, +#' statvar = "mean", #' dptvarn = 2 #' )$tsum |> #' display_bign_head(adsl_entry) @@ -496,13 +492,13 @@ display_bign_head <- function(datain, mutate(across(any_of(lastvar), ~ paste0(.x, colformat))) if (lastvar == "TRTVAR") { datain[["TRTVAR"]] <- factor(datain[["TRTVAR"]], - levels = unique(datain[["TRTVAR"]][ord]) + levels = unique(datain[["TRTVAR"]][ord]) ) } } } else { notrthead <- ifelse(any(c(trtbignyn, subbignyn) == "Y"), - paste0(notrthead, " (N = ", length(unique(mentry_data[["USUBJID"]])), ")"), notrthead + paste0(notrthead, " (N = ", length(unique(mentry_data[["USUBJID"]])), ")"), notrthead ) datain <- datain |> mutate(!!notrthead := as.character(.data[["CVALUE"]])) |> @@ -511,6 +507,101 @@ display_bign_head <- function(datain, return(datain) } +#' Sparse empty categories/treatments with 0 +#' +#' @param datain Input data to be sparsed for missing categories/treatments/by vars +#' @param data_sparse Initial data to sparse with +#' @param sparseyn Sparse categories within by groups. (Y/N) +#' @param sparsebyvalyn Sparse by groups in data - takes precedence over `sparseyn` (Y/N) +#' @param BYVAR By Variables in data +#' @param BYVARN By Variables N equivalent +#' @param SUBGRP Subgroup Variables in data +#' @param SUBGRPN Subgroup Variables N equivalent +#' @param fillvar Variables to fill with `fill_with` +#' @param fill_with Value to fill empty `fillvar` with +#' +#' @return dataframe sparsed with values for empty categories +#' +#' @examples +#' data(adsl) +#' library(dplyr) +#' adsl_entry <- mentry(adsl, +#' byvar = "SEX", +#' trtvar = "TRT01A", +#' trtsort = "TRT01AN", +#' subset = "SAFFL == 'Y'" +#' ) +#' count <- adsl_entry |> +#' filter(SEX == "F") |> +#' group_by(BYVAR1, TRTVAR) |> +#' summarise(FREQ = length(unique(USUBJID))) +#' sparse_vals(count, +#' data_sparse = adsl_entry, +#' sparseyn = "N", +#' sparsebyvalyn = "Y", +#' "BYVAR1", +#' character(0), +#' "BYVAR1N", +#' character(0) +#' ) +#' +#' @noRd +sparse_vals <- function(datain, + data_sparse, + sparseyn = "Y", + sparsebyvalyn = "N", + BYVAR, + SUBGRP, + BYVARN, + SUBGRPN, + fillvar = "FREQ", + fill_with = 0) { + # Exit if neither are Y + if (!(sparseyn == "Y" || sparsebyvalyn == "Y")) { + return(datain) + } + TRTVAR <- var_start(datain, "TRTVAR") + if (sparsebyvalyn == "Y") { + byn <- c(BYVAR, SUBGRP) + if ("DPTVAL" %in% names(datain)) { + df_exp <- data_sparse |> + tidyr::expand(!!!rlang::syms(c(BYVAR, TRTVAR, SUBGRP)), tidyr::nesting(DPTVAL, DPTVALN)) + dptn <- "DPTVALN" + } else { + if (!any(c(SUBGRP, "TRTVAR") %in% names(datain))) { + return(datain) + } + # Processing if msumstat output/without DPTVAL column + df_exp <- data_sparse |> + tidyr::expand(!!!rlang::syms(c(BYVAR, TRTVAR, SUBGRP))) + dptn <- character() + } + } else if (sparseyn == "Y") { + # Sparse only category columns + byn <- SUBGRP + dptn <- "DPTVALN" + df_exp <- data_sparse |> + tidyr::expand(!!!rlang::syms(c(TRTVAR, SUBGRP)), tidyr::nesting(DPTVAL, DPTVALN)) |> + left_join(distinct(data_sparse, across(any_of(starts_with(c("DPTVAL", "BYVAR"))))), + by = c("DPTVAL", "DPTVALN") + ) + } + data_sparse <- ungroup(data_sparse) + if (length(byn) > 0) { + for (b in byn) { + df_exp <- df_exp |> + left_join(distinct(data_sparse, across(all_of(starts_with(b)))), by = b) + } + } + df_exp <- distinct(df_exp) + datain |> + select(-any_of(c(SUBGRPN, BYVARN, dptn))) |> + (\(.) full_join(., df_exp, by = intersect(names(.), names(df_exp))))() |> + mutate(across(any_of(fillvar), ~ replace_na(.x, fill_with))) |> + ungroup() |> + distinct() +} + #' Report Metadata #' #' @return `data.frame` containing report metadata diff --git a/R/vx_boxplot.R b/R/vx_boxplot.R deleted file mode 100644 index 140b3ed..0000000 --- a/R/vx_boxplot.R +++ /dev/null @@ -1,221 +0,0 @@ -# Copyright 2024 Pfizer Inc -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -#' Process data for vaccine boxplot -#' -#' @inheritParams process_vx_scatter_data -#' @param ystat Additional Statistic to be calculated and plotted as markers. -#' Values: 'mean', 'sum', 'sd' etc -#' @param ada_nab_opts List of values of : *PARAMN*, Y axis Label, Reference -#' line value and Dilution (for footnote) corresponding to ADA and NAb titers -#' respectively. Format: list(N = "1~2", -#' LAB = "ADA Titer (log2)~NAb Titer (log2)", REF = "6.23~1.58", DIL = "75~3") -#' -#' @return Dataframe containing analysis values for requisite box plot statistics -#' @export -#' -#' @examples -#' data(vx_box_data) -#' process_vx_box_data( -#' dataset_adsl = vx_box_data$adsl, -#' dataset_analysis = vx_box_data$adisda, -#' adsl_subset = "RANDFL == 'Y'", -#' analysis_subset = "((ANL08FL == 'Y')|(ANL09FL=='Y'))&(ARMCD!='')&(PARAMN %in% c(1, 2))", -#' trtvar = "TRTA", -#' trtsort = "TRTAN", -#' xvar = "AVISIT", -#' ystat = "mean" -#' ) -process_vx_box_data <- - function(dataset_adsl, - dataset_analysis, - adsl_subset = "SAFFL == 'Y'", - analysis_subset = "((ANL08FL == 'Y')|(ANL09FL=='Y'))&(ARMCD!='')", - split_by = NA_character_, - trtvar = "TRT01A", - trtsort = "TRT01AN", - xvar = "AVISIT", - yvar = "AVAL", - ystat = "mean", - legendbign = "Y", - ada_nab_opts = - list( - N = "1~2", - LAB = "ADA Titer (log2)~NAb Titer (log2)", - REF = "6.23~1.58", - DIL = "75~3" - )) { - # Check data sets are not empty - stopifnot("ADSL data is empty" = nrow(dataset_adsl) != 0) - stopifnot("Analysis data is empty" = nrow(dataset_analysis) != 0) - # Set subgroup variables (page/split variables) - subgrps <- ifelse(all(is.na(split_by)), "PARAM", - paste(c(split_by, "PARAM"), collapse = "~") - ) - # Merge with adsl - adsin_entry <- adsl_merge( - dataset_adsl, - adsl_subset, - dataset_analysis - ) |> - mentry( - subset = analysis_subset, - byvar = xvar, - subgrpvar = subgrps, - trtvar = trtvar, - trtsort = trtsort, - trttotalyn = "N", - sgtotalyn = "N", - add_grpmiss = "N", - pop_fil = NA - ) - stopifnot( - "Given Subsets not present in Analysis Data" = - nrow(adsin_entry) != 0 - ) - stats_data <- msumstat( - datain = adsin_entry, - dptvar = yvar, - statvar = c(ystat, "box"), - sigdec = 3 - ) - # Paramn subgrp var name: - psub <- max(var_start(stats_data$gsum, "SUBGRPVARN")) - # ADa_NaB data combined: - ada_nab_data <- - data.frame( - as.numeric(str_to_vec(ada_nab_opts$N)), - as.numeric(str_to_vec(ada_nab_opts$REF)), - str_to_vec(ada_nab_opts$LAB), - as.numeric(str_to_vec(ada_nab_opts$DIL)), - c("ADA", "NAb") - ) |> setNames(c(psub, "REF", "YLAB", "DIL", "TITER")) - # Merge with ada opts and create X variable; spltN var for plot title - stats <- plot_title_nsubj( - adsin_entry, - stats_data$gsum, - var_start(stats_data$gsum, "SUBGRP") - ) |> - mutate(XVAR = fct_reorder(str_to_title(.data[["BYVAR1"]]), .data[["BYVAR1N"]])) |> - dplyr::inner_join(ada_nab_data, by = psub) |> - plot_display_bign(adsin_entry, bignyn = legendbign) - return(stats) - } - - -#' Generate Vaccine Boxplots for antibody titer using analysed data -#' -#' Creates 2 similar plots with slightly different specifications according to -#' parameter i.e., ADA or NaB titer values are plotted in 2 separate graphs -#' -#' @param datalist List of Input datasets, retrieved from -#' `process_vx_box_data()` and `split_data_by_var()` -#' @inheritParams box_plot -#' -#' @details Input data should come from output of `process_vx_box_data()` and -#' is expected to have the standardised variable XVAR and ada_nab_opts -#' @return a list of lists, each of 2 elements: -#' \itemize{ -#' \item `plot` Plot output -#' \item `footnote` Text to be considered as first line of footnote in report -#' } -#' @export -#' -#' @examples -#' data(vx_box_data) -#' plot_data <- process_vx_box_data( -#' dataset_adsl = vx_box_data$adsl, -#' dataset_analysis = vx_box_data$adisda, -#' adsl_subset = "RANDFL == 'Y'", -#' analysis_subset = "((ANL08FL == 'Y')|(ANL09FL=='Y'))&(ARMCD!='')&(PARAMN %in% c(1, 2))", -#' trtvar = "TRTA", -#' trtsort = "TRTAN", -#' xvar = "AVISIT", -#' ystat = "mean", -#' legendbign = "Y" -#' ) -#' series_opts <- plot_aes_opts( -#' datain = plot_data, -#' series_color = c("red", "blue", "green"), -#' series_shape = c("circlefilled", "trianglefilled", "squarefilled"), -#' series_size = c(2, 2, 2) -#' ) -#' -#' # Splitting data to generate separate plots by `split_by` variable -#' data_list <- split_data_by_var( -#' datain = plot_data, -#' split_by_prefix = "SUBGRPVAR" -#' ) -#' -#' vx_box_plot( -#' datalist = data_list, -#' axis_opts = plot_axis_opts( -#' xaxis_label = "Visits" -#' ), -#' series_opts = series_opts, -#' legend_opts = list( -#' lab = "Treatment", -#' pos = "bottom", -#' dir = "horizontal" -#' ), -#' ystat = "mean" -#' )[[1]][[1]] -vx_box_plot <- function(datalist, - axis_opts, - series_opts, - legend_opts = list( - lab = "", pos = "bottom", - dir = "horizontal" - ), - box_opts = c(0.7, 0.9), - ystat = "mean", - griddisplay = "N") { - datalist |> - map(function(df) { - stopifnot(is.data.frame(df)) - axis_opts$yaxis_label <- unique(df$YLAB) - titerp <- box_plot( - datain = df, - legend_opts = legend_opts, - series_opts = series_opts, - axis_opts = axis_opts, - series_var = "TRTVAR", - series_labelvar = ifelse("TRTTXT" %in% names(df), "TRTTXT", "TRTVAR"), - box_opts = box_opts, - ystat = ystat, - griddisplay = griddisplay, - plot_title = glue("Number of Participants N = {unique(df$splitN)}") - ) + - geom_line( - aes( - x = XVAR, - y = .data[[ystat]], - group = TRTVAR, - color = TRTVAR - ), - position = position_dodge(box_opts[2]) - ) + - geom_hline( - yintercept = unique(df$REF), - linetype = 2, - color = "black" - ) - ftnote <- glue( - "Reference line at Y = {unique(df$REF)} represents the detection limit\\ - in the {unique(df$TITER)} assay (minimum required dilution \\ - {unique(df$DIL)})" - ) - list(plot = titerp, footnote = ftnote) - }) -} From f21ab6cca5ba0c34f7a807980ec27a1452aa5a3f Mon Sep 17 00:00:00 2001 From: kallea03 Date: Mon, 6 Jan 2025 06:37:22 +0000 Subject: [PATCH 02/48] added stat_utils --- R/stat_utils.R | 194 +++++++++++++++++++++++++++++++++++++++++++++++++ R/utils.R | 97 ------------------------- 2 files changed, 194 insertions(+), 97 deletions(-) create mode 100644 R/stat_utils.R diff --git a/R/stat_utils.R b/R/stat_utils.R new file mode 100644 index 0000000..12d1646 --- /dev/null +++ b/R/stat_utils.R @@ -0,0 +1,194 @@ +#' Update functions to round values +#' +#' @param f List of names of summary statistics. +#' @param d Numeric values +#' @param ... +#' +#' @return Rounded numeric values +#' @noRd +#' +fmtrd <- function(f, d = 2, ...) { + function(x) { + dc <- do.call(f, args = list(x, na.rm = TRUE, ...)) + return(ifelse(is.na(dc), "-", round_f(dc, d))) + } +} + +#' Std Error function definition +#' +#' @param x Input vector +#' +#' @return Numeric value containing standard error +#' +#' @noRd +stderr <- function(x, ...) { + sd(x, ...) / sqrt(length((x))) +} + +#' Dataframe of statistics, labels and derivations +#' +#' @return tibble of 4 columns +#' @noRd +stat_lookup <- function() { + tibble::tribble( + ~Stat, ~base, ~label, ~derv, + "mean(sd)", "mean/sd", "Mean(SD)", "{mean} ({sd})", + "mean(stderr)", "mean/stderr", "Mean(SE)", "{mean} ({stderr})", + "minmax", "min/max", "(Min-Max)", "({min}-{max})", + "minmaxc", "min/max", "(Min,Max)", "({min},{max})", + "median(minmax)", "median/min/max", "Median (Min-Max)", "{median} ({min}-{max})", + "median(minmaxc)", "median/min/max", "Median (Min,Max)", "{median} ({min},{max})", + "q1q3", "q1/q3", "(Q1,Q3)", "({q1}, {q3})", + "median(q1q3)", "median/q1/q3", "Median (Q1,Q3)", "{median} ({q1},{q3})", + "geomean(geosd)", "geomean/geosd", "Geomean(Geomean SD)", "{geomean} ({geosd})", + "q1", "q1", "1st Quantile", "", + "q3", "q3", "3rd Quantile", "", + "stderr", "stderr", "s.e.", "", + "sd", "sd", "s.d.", "" + ) +} + +#' Parse Statistics for msumstat +#' +#' @param statvar Input statistics to msumstat +#' @param statdec Input statistics decimal levels to msumstat +#' +#' @return named vector +#' @noRd +parse_stats <- function(statvar, statdec) { + if (all(is.na(statdec)) || all(statdec == "")) statdec <- rep(2, length(statvar)) + if (length(statdec) == 1) statdec <- rep(statdec, length(statvar)) + stopifnot(length(statvar) == length(statdec)) + lookup <- stat_lookup() |> filter(derv != "") + stats <- map(seq_along(statvar), \(s) { + if (statvar[s] %in% lookup$Stat) { + st <- lookup |> + filter(Stat == statvar[s]) |> + pull(base) |> + str_to_vec("/") + d <- unlist(str_extract_all(statdec[s], "[0-9]+")) + last <- d[length(d)] + length(d) <- length(st) + d[is.na(d)] <- last + setNames(d, st) + } else { + setNames(statdec[s], statvar[s]) + } + }) |> + unlist() + if (any(duplicated(names(stats)))) { + stats[unique(names(stats))] + } else { + stats + } +} + +#' List of Summary Functions +#' +#' @param statvar Input statistics +#' @param statdec Corresponding number of decimal places for each statistic +#' +#' @return A named list containing function definition for all defined summary +#' statistics - mean, min, max, median, mode iqr, var, sum, sd, q25, q75, p1, p5, +#' p10, p90, p95, p99 (where last digits represent % of quantile), whiskerlow, +#' whiskerup, outliers in the Tukey method for box statistics, geometric mean/sd/CI +#' @export +#' +#' @examples +#' summary_functions(c("mean", "mode"), c(2, 1)) +summary_functions <- function(statvar, statdec) { + base_fns <- c( + "mean", "min", "max", "median", "IQR", "var", "sum", "sd", "stderr", "mode", + "whiskerlow", "whiskerup" + ) + map(seq_along(statvar), \(s) { + d <- as.numeric(statdec[s]) + f <- statvar[s] + f <- recode(f, "q1" = "q25", "q3" = "q75", "iqr" = "IQR") + if (f %in% base_fns) { + fmtrd(f, d) + } else if (str_detect(f, "^(q\\d+)$|^(p\\d+)$")) { + fmtrd(f = "quantile", d = d, as.numeric(gsub("\\D", "", f)) / 100, type = 2) + } else if (f == "geomean") { + function(x) round_f(exp(mean(log(x), na.rm = TRUE)), d) + } else if (f == "geosd") { + function(x) round_f(exp(sd(log(x), na.rm = TRUE)), d) + } else if (f == "geomean_lowci") { + function(x) { + x <- log(x) + margin_error <- qt(0.975, df = length(x) - 1) * sd(x, na.rm = TRUE) / sqrt(length(x)) + round_f(exp(mean(x, na.rm = TRUE) - margin_error), d) + } + } else if (f == "geomean_upci") { + function(x) { + x <- log(x) + margin_error <- qt(0.975, df = length(x) - 1) * sd(x, na.rm = TRUE) / sqrt(length(x)) + round_f(exp(mean(x, na.rm = TRUE) + margin_error), d) + } + } else if (f == "outliers") { + function(x) { + x <- x[!is.na(x)] + paste(unique(x[x < whiskerlow(x) | x > whiskerup(x)]), collapse = "") + } + } else if (f == "nobs") { + function(x) paste(n()) + } else if (f == "n") { + function(x) as.character(sum(!is.na(x))) + } else if (f == "nmiss") { + function(x) as.character(sum(is.na(x))) + } else { + function(x) "_NO_STAT" + } + }) |> + setNames(statvar) +} + + +#' Lower Box Whiskers +#' +#' @param x Input data +#' @param na.rm Remove NA +#' +#' @return Lower Whisker Value for box plot data +#' @noRd +whiskerlow <- function(x, na.rm = TRUE) { + min(x[(x >= (quantile(x, 0.25, na.rm = TRUE) - 1.5 * IQR(x, na.rm = TRUE))) & + (x <= quantile(x, 0.25, na.rm = TRUE))], na.rm = na.rm) +} + +#' Upper Box Whiskers +#' +#' @param x Input data +#' @param na.rm Remove NA +#' +#' @return Upper Whisker Value for box plot data +#' @noRd +whiskerup <- function(x, na.rm = TRUE) { + max(x[(x <= (quantile(x, 0.75, na.rm = TRUE) + 1.5 * IQR(x, na.rm = TRUE))) & + (x >= quantile(x, 0.75, na.rm = TRUE))], na.rm = na.rm) +} + +#' Concatenate to create complex statistics +#' +#' @param data Input dataset +#' @param stats Statistics +#' +#' @return Dataframe with mutated columns +#' @noRd +derv_stats <- function(data, stats, lookup = stat_lookup()) { + lookup <- lookup |> filter(derv != "") + if (!any(stats %in% lookup[[1]])) { + return(data) + } else { + stats <- stats[stats %in% lookup[[1]]] + map(stats, \(s) { + derv <- lookup |> + filter(if_all(1) == s) |> + pull(derv) + data |> + mutate({{ s }} := glue::glue(derv)) |> + select(all_of(s)) + }) |> + (\(.) bind_cols(data, .))() + } +} diff --git a/R/utils.R b/R/utils.R index 5ef98c6..731362b 100644 --- a/R/utils.R +++ b/R/utils.R @@ -109,103 +109,6 @@ round_f <- function(x, digits = 2) { format(round(x, digits), nsmall = digits) } -#' Update functions to round values -#' -#' @param f List of names of summary statistics. -#' @param d Numeric values -#' @param ... -#' -#' @return Rounded numeric values -#' @noRd -#' -fmtrd <- function(f, d = 2, ...) { - function(x) { - dc <- do.call(f, args = list(x, na.rm = TRUE, ...)) - return(ifelse(is.na(dc), "-", round_f(dc, d))) - } -} - -#' Create summary stats function for use within `msumstat()` -#' -#' @param sigdec Number of significant decimal places (base) -#' -#' @return a named list containing function definition for all defined summary -#' statistics - mean, min, max, median, iqr, var, sum, sd, q25, q75, p1, p5, -#' p10, p90, p95, p99 (where last digits represent % of quantile), meansd, -#' range, q1q3, medianrange (concatenation of indicated names), whiskerlow, -#' whiskerup, outliers in the Tukey method for box statistics -#' @export -summary_functions <- function(sigdec = 2) { - # Basic functions for summary - base_fns <- c("mean", "min", "max", "median", "IQR", "var", "sum", "sd") - d <- c(rep(sigdec, 7), sigdec + 1) - fns_list <- map(seq_along(base_fns), function(f) { - fmtrd(f = base_fns[[f]], d = d[[f]]) - }) |> set_names(tolower(base_fns)) - # Quantiles for data: - q_fns <- c("q25", "q75", "p1", "p10", "p5", "p90", "p95", "p99") - q_pct <- as.numeric(gsub("\\D", "", q_fns)) / 100 - q_list <- seq_along(q_fns) |> - map(function(f) fmtrd(f = "quantile", d = sigdec, q_pct[[f]])) |> - set_names(q_fns) - # Combined list - fns_list <- append(fns_list, q_list) - # Concatenated and Compound functions - fns_list <- append( - fns_list, - list( - meansd = - function(x) paste0(fns_list[["mean"]](x), " (", fns_list[["sd"]](x), ")"), - range = function(x) { - paste0( - "(", fns_list[["min"]](x), ", ", - fns_list[["max"]](x), ")" - ) - }, - q1q3 = function(x) { - paste0( - "(", fns_list[["q25"]](x), ", ", - fns_list[["q75"]](x), ")" - ) - }, - medianrange = - function(x) paste(fns_list[["median"]](x), fns_list[["range"]](x)), - whiskerlow = function(x) { - fns_list[["min"]]( - x[(x >= (quantile(x, 0.25, na.rm = TRUE) - 1.5 * IQR(x, na.rm = TRUE))) & - (x <= quantile(x, 0.25, na.rm = TRUE))]) - }, - whiskerup = function(x) { - fns_list[["max"]]( - x[(x <= (quantile(x, 0.75, na.rm = TRUE) + 1.5 * IQR(x, na.rm = TRUE))) & - (x >= quantile(x, 0.75, na.rm = TRUE))]) - }, - outliers = function(x) { - x <- x[!is.na(x)] - paste(unique(x[x < as.numeric(fns_list[["whiskerlow"]](x)) | - x > as.numeric(fns_list[["whiskerup"]](x))]), collapse = "~") - }, - geom_lowci = function(x) { - ci <- 0.95 - x <- log(x) - margin_error <- qt(ci + (1 - ci) / 2, df = length(x) - 1) * sd(x) / sqrt(length(x)) - paste(exp(mean(x, na.rm = TRUE) - margin_error)) - }, - geom_upci = function(x) { - ci <- 0.95 - x <- log(x) - margin_error <- qt(ci + (1 - ci) / 2, df = length(x) - 1) * sd(x) / sqrt(length(x)) - paste(exp(mean(x, na.rm = TRUE) + margin_error)) - }, - geommean = function(x) { - x <- log(x) - paste(exp(mean(x, na.rm = TRUE))) - }, - n = function(x) paste(n()) - ) - ) - return(fns_list) -} #' Convert string to vector #' From 7f0f4b76b32113c058b80f16e23133565affe7bb Mon Sep 17 00:00:00 2001 From: kallea03 Date: Mon, 6 Jan 2025 07:12:06 +0000 Subject: [PATCH 03/48] updated stat_utils --- R/stat_utils.R | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/R/stat_utils.R b/R/stat_utils.R index 12d1646..3e38400 100644 --- a/R/stat_utils.R +++ b/R/stat_utils.R @@ -1,3 +1,17 @@ +# Copyright 2024 Pfizer Inc +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# #' Update functions to round values #' #' @param f List of names of summary statistics. From d69d40bd8251c3f50afb6f140b8f433c88b273e6 Mon Sep 17 00:00:00 2001 From: kallea03 Date: Tue, 7 Jan 2025 06:44:29 +0000 Subject: [PATCH 04/48] updated & added test cases --- R/dataset_merge.R | 47 +- tests/testthat/_snaps/adae_risk_summary.md | 124 ++- tests/testthat/_snaps/adlb_r301.md | 267 ++++++ tests/testthat/_snaps/ae_forestplot.md | 90 ++ tests/testthat/_snaps/ae_volcano_plot.md | 86 ++ tests/testthat/_snaps/bar_plot.md | 32 + tests/testthat/_snaps/box_plot.md | 168 ++++ tests/testthat/_snaps/event_analysis.md | 396 +++++++++ tests/testthat/_snaps/forest_plot.md | 297 +++++++ tests/testthat/_snaps/graph_utils.md | 161 ++++ tests/testthat/_snaps/line_plot.md | 34 + tests/testthat/_snaps/occ_tier_summary.md | 983 +++++++++++++++++++-- tests/testthat/_snaps/ptly_utils.md | 108 +++ tests/testthat/test-adae_risk_summary.R | 95 +- tests/testthat/test-adlb_r301.R | 245 +++-- tests/testthat/test-adsl_r001.R | 173 ++++ tests/testthat/test-ae_forestplot.R | 178 ++++ tests/testthat/test-ae_pre_processor.R | 252 +++--- tests/testthat/test-ae_volcano_plot.R | 39 +- tests/testthat/test-bar_plot.R | 78 ++ tests/testthat/test-box_plot.R | 115 +++ tests/testthat/test-dataset_merge.R | 21 +- tests/testthat/test-edish_plot.R | 239 +++-- tests/testthat/test-event_analysis.R | 129 +-- tests/testthat/test-graph_utils.R | 328 +++++++ tests/testthat/test-km_plot.R | 68 ++ tests/testthat/test-line_plot.R | 55 ++ tests/testthat/test-mcatstat.R | 76 +- tests/testthat/test-mentry.R | 151 ++-- tests/testthat/test-msumstat.R | 158 ++++ tests/testthat/test-occ_tier_summary.R | 127 +-- tests/testthat/test-process_vx_bar_plot.R | 28 + tests/testthat/test-risk_stat.R | 89 +- tests/testthat/test-riskdiff_wald.R | 36 - tests/testthat/test-stat_utils.R | 161 ++++ tests/testthat/test-surv_utils.R | 68 ++ tests/testthat/test-tbl_display.R | 27 +- tests/testthat/test-tornado_plot.R | 14 +- tests/testthat/test-utils.R | 196 ++-- 39 files changed, 4928 insertions(+), 1011 deletions(-) create mode 100644 tests/testthat/_snaps/adlb_r301.md create mode 100644 tests/testthat/_snaps/ae_forestplot.md create mode 100644 tests/testthat/_snaps/ae_volcano_plot.md create mode 100644 tests/testthat/_snaps/bar_plot.md create mode 100644 tests/testthat/_snaps/box_plot.md create mode 100644 tests/testthat/_snaps/event_analysis.md create mode 100644 tests/testthat/_snaps/forest_plot.md create mode 100644 tests/testthat/_snaps/graph_utils.md create mode 100644 tests/testthat/_snaps/line_plot.md create mode 100644 tests/testthat/_snaps/ptly_utils.md create mode 100644 tests/testthat/test-adsl_r001.R create mode 100644 tests/testthat/test-ae_forestplot.R create mode 100644 tests/testthat/test-bar_plot.R create mode 100644 tests/testthat/test-box_plot.R create mode 100644 tests/testthat/test-graph_utils.R create mode 100644 tests/testthat/test-km_plot.R create mode 100644 tests/testthat/test-line_plot.R create mode 100644 tests/testthat/test-msumstat.R create mode 100644 tests/testthat/test-process_vx_bar_plot.R delete mode 100644 tests/testthat/test-riskdiff_wald.R create mode 100644 tests/testthat/test-stat_utils.R create mode 100644 tests/testthat/test-surv_utils.R diff --git a/R/dataset_merge.R b/R/dataset_merge.R index 8eba0b5..0d27b70 100644 --- a/R/dataset_merge.R +++ b/R/dataset_merge.R @@ -18,6 +18,7 @@ #' @param byvars By variables required to perform merge. #' @param subset Dataset specific subset conditions as `list`, default is `NULL`. #' Has to be specified in the same order of datasets to be merged +#' @param type Type of join to perform. Values: "left", "right", "inner", "full", "semi", "anti" #' #' @return A `data.frame` #' @export @@ -67,12 +68,20 @@ #' byvars = "STUDYID~USUBJID~SUBJID" #' ) #' -dataset_merge <- function(..., byvars, subset = NULL) { - dfs <- list2(...) +dataset_merge <- function(..., byvars, subset = NULL, type = "left") { + dfs <- rlang::list2(...) stopifnot("At least two datasets required for merging" = length(dfs) >= 2) + stopifnot( + "Type should be one of left, right, inner, full" = + type %in% c("left", "right", "inner", "full") + ) + + if (type == "full") { + warning("For full join, subsets will not work as expected. Consider using adsl_merge() instead") + } byvars <- str_to_vec(byvars) if (!every(dfs, \(x) all(byvars %in% names(x)))) stop("`byvars` not present") - + if (length(subset) > 0) { stopifnot("Length of subsets and datasets should be equal" = length(dfs) == length(subset)) if (every(subset, is.na)) stop("All subsets cannot be `NA`, use `subset = NULL` instead") @@ -85,7 +94,7 @@ dataset_merge <- function(..., byvars, subset = NULL) { df_sub }) } - + df_list <- map(seq_along(dfs), \(x) { out <- dfs[[x]] if (x < length(dfs)) { @@ -96,7 +105,8 @@ dataset_merge <- function(..., byvars, subset = NULL) { } out }) - reduce(df_list, left_join, byvars) + type <- paste0(type, "_join") + reduce(df_list, get(type), byvars) } #' Merge adsl dataset with the analysis dataset @@ -104,6 +114,7 @@ dataset_merge <- function(..., byvars, subset = NULL) { #' @param adsl adsl dataset #' @param adsl_subset population variable subset condition #' @param dataset_add analysis dataset +#' @param byvars Variables to merge the datasets by #' #' @return merged dataset #' @export @@ -117,19 +128,19 @@ dataset_merge <- function(..., byvars, subset = NULL) { #' dataset_add = lab_data$adlb #' ) #' -adsl_merge <- function(adsl = NULL, adsl_subset = "", dataset_add = NULL) { - stopifnot(length(adsl) > 0) - stopifnot(nrow(adsl) > 0) - stopifnot(length(dataset_add) > 0) - +adsl_merge <- function(adsl = NULL, adsl_subset = "", dataset_add = NULL, byvars = NULL) { + stopifnot("Pass an ADSL dataset" = length(adsl) > 0) + stopifnot("Pass an Analysis Dataset" = length(dataset_add) > 0) + if (nrow(adsl) == 0 || nrow(dataset_add) == 0) { + return(data.frame()) + } + if (is.null(byvars)) { + byvars <- intersect(colnames(adsl), colnames(dataset_add)) + } + + outdata <- full_join(adsl, dataset_add, by = byvars) if (adsl_subset != "" && !is.na(adsl_subset)) { - adsl <- adsl |> - filter(!!!parse_exprs(adsl_subset)) + outdata <- filter(outdata, !!!parse_exprs(adsl_subset)) } - - byvars <- grep("STUDYID|USUBJID|SUBJID", names(dataset_add), value = TRUE) - - adsl |> - select(all_of(c(byvars, setdiff(names(adsl), names(dataset_add))))) |> - left_join(dataset_add, by = byvars) + outdata } diff --git a/tests/testthat/_snaps/adae_risk_summary.md b/tests/testthat/_snaps/adae_risk_summary.md index b6ac995..79885a4 100644 --- a/tests/testthat/_snaps/adae_risk_summary.md +++ b/tests/testthat/_snaps/adae_risk_summary.md @@ -1,46 +1,98 @@ -# Standard Inputs work +# Test Case 1: adae_summary with standard inputs works + + Code + ae_risk + Output + # A tibble: 48 x 30 + TRTVAR DPTVAR DPTVAL CVALUE DENOMN FREQ PCT CPCT XVAR CN PVALUE + + 1 Placebo TIER CARDI~ 1 ( 1~ 62 1 1.61 " 1.~ CARD~ C 0.236 + 2 Placebo TIER RESPI~ 1 ( 1~ 62 1 1.61 " 1.~ RESP~ C 0.141 + 3 Xanomeline ~ TIER CARDI~ 4 ( 5~ 73 4 5.48 " 5.~ CARD~ C 0.236 + 4 Xanomeline ~ TIER RESPI~ 5 ( 6~ 73 5 6.85 " 6.~ RESP~ C 0.141 + 5 Placebo TIER NERVO~ 2 ( 3~ 62 2 3.23 " 3.~ NERV~ C 0.221 + 6 Xanomeline ~ TIER NERVO~ 6 ( 8~ 73 6 8.22 " 8.~ NERV~ C 0.221 + 7 Placebo TIER INFEC~ 8 (12~ 62 8 12.9 "12.~ INFE~ C 0.235 + 8 Xanomeline ~ TIER INFEC~ 5 ( 6~ 73 5 6.85 " 6.~ INFE~ C 0.235 + 9 Placebo TIER GASTR~ 9 (14~ 62 9 14.5 "14.~ GAST~ C 0.0342 + 10 Xanomeline ~ TIER GASTR~ 3 ( 4~ 73 3 4.11 " 4.~ GAST~ C 0.0342 + # i 38 more rows + # i 19 more variables: RISK , RISKCIL , RISKCIU , + # ADJPVALUE , RISK_CI , TRTPAIR , CTRL , ACTIVE , + # TOTAL_N , DPTVARN , DPTVALN , SUBGRPVARX , + # SUBGRPVARXN , `Risk Ratio (CI)` , `Risk Ratio` , + # `P-value` , `Lower Limit` , `Upper Limit` , + # `(Lower-Upper)` + +--- Code output Output - # A tibble: 58 x 24 - DPTVAL PVALUE ADJPVALUE RISK RISKCIL RISKCIU RISK_CI TRTPAIR TRTVAR FREQ - - 1 INJURY, ~ 0.591 0.591 0.672 0.16 2.9 0.672 ~ Placeb~ Place~ 4 - 2 INJURY, ~ 0.591 0.591 0.672 0.16 2.9 0.672 ~ Placeb~ Xanom~ 3 - 3 MUSCULOS~ 0.452 0.452 1.57 0.48 5.13 1.568 ~ Placeb~ Place~ 4 - 4 MUSCULOS~ 0.452 0.452 1.57 0.48 5.13 1.568 ~ Placeb~ Xanom~ 7 - 5 RENAL AN~ 0.331 0.331 0.448 0.08 2.37 0.448 ~ Placeb~ Place~ 4 - 6 RENAL AN~ 0.331 0.331 0.448 0.08 2.37 0.448 ~ Placeb~ Xanom~ 2 - 7 METABOLI~ 0.0707 0.0707 0.179 0.02 1.5 0.179 ~ Placeb~ Place~ 5 - 8 METABOLI~ 0.0707 0.0707 0.179 0.02 1.5 0.179 ~ Placeb~ Xanom~ 1 - 9 NERVOUS ~ 0.0521 0.0521 2.18 0.96 4.93 2.176 ~ Placeb~ Place~ 7 - 10 NERVOUS ~ 0.0521 0.0521 2.18 0.96 4.93 2.176 ~ Placeb~ Xanom~ 17 - # i 48 more rows - # i 14 more variables: PCT , TOTAL_N , HOVER_PCT , - # HOVER_RISK , HOVER_TEXT , DPTVAR , CN , DPTVARN , - # DPTVALN , SUBGRPVARX , SUBGRPVARXN , - # `Risk Ratio (CI)` , `P-value` , CVALUE + a flextable object. + col_keys: ` `, `Placebo_n (%) `, `Xanomeline Low Dose_n (%) `, `Risk Ratio (CI)` + header has 2 row(s) + body has 24 row(s) + original dataset sample: + DPTVARN DPTVALN CN + 1 CARDIAC DISORDERS 1 0 C + 2 \t\t\tSINUS BRADYCARDIA 1 1 C + 3 RESPIRATORY, THORACIC AND MEDIASTINAL DISORDERS 2 0 C + 4 \t\t\tCOUGH 2 1 C + 5 NERVOUS SYSTEM DISORDERS 3 0 C + Placebo_n (%) Xanomeline Low Dose_n (%) Risk Ratio (CI) + 1 1 ( 1.6%) 4 ( 5.5%) 0.039 (-0.02, 0.1) + 2 1 ( 1.6%) 4 ( 5.5%) 0.039 (-0.02, 0.1) + 3 1 ( 1.6%) 5 ( 6.8%) 0.052 (-0.01, 0.12) + 4 1 ( 1.6%) 5 ( 6.8%) 0.052 (-0.01, 0.12) + 5 2 ( 3.2%) 6 ( 8.2%) 0.05 (-0.03, 0.13) + +# Test Case 2: adae_summary with summary row + + Code + ae_risk + Output + # A tibble: 50 x 32 + TRTVAR DPTVAR DPTVAL CVALUE DENOMN FREQ PCT CPCT XVAR CN PVALUE + + 1 Placebo TIER CARDI~ 1 ( 1~ 62 1 1.61 " 1.~ CARD~ C 0.236 + 2 Xanomeline ~ TIER CARDI~ 4 ( 5~ 73 4 5.48 " 5.~ CARD~ C 0.236 + 3 Placebo TIER GASTR~ 9 (14~ 62 9 14.5 "14.~ GAST~ C 0.0342 + 4 Xanomeline ~ TIER GASTR~ 3 ( 4~ 73 3 4.11 " 4.~ GAST~ C 0.0342 + 5 Placebo TIER GENER~ 16 (2~ 62 16 25.8 "25.~ GENE~ C 0.0019 + 6 Xanomeline ~ TIER GENER~ 38 (5~ 73 38 52.1 "52.~ GENE~ C 0.0019 + 7 Placebo TIER INFEC~ 8 (12~ 62 8 12.9 "12.~ INFE~ C 0.235 + 8 Xanomeline ~ TIER INFEC~ 5 ( 6~ 73 5 6.85 " 6.~ INFE~ C 0.235 + 9 Placebo TIER NERVO~ 2 ( 3~ 62 2 3.23 " 3.~ NERV~ C 0.221 + 10 Xanomeline ~ TIER NERVO~ 6 ( 8~ 73 6 8.22 " 8.~ NERV~ C 0.221 + # i 40 more rows + # i 21 more variables: RISK , RISKCIL , RISKCIU , + # ADJPVALUE , RISK_CI , TRTPAIR , CTRL , ACTIVE , + # TOTAL_N , DPTVARN , DPTVALN , CTRL_N , CTRL_PCT , + # SUBGRPVARX , SUBGRPVARXN , `Risk Ratio (CI)` , + # `Risk Ratio` , `P-value` , `Lower Limit` , + # `Upper Limit` , `(Lower-Upper)` --- Code - out_table + output Output - # A tibble: 29 x 8 - DPTVAL DPTVARN DPTVALN CN `Placebo_n (%) ` Xanomeline Low Dose_~1 - - 1 "INJURY, POISO~ 1 0 C 4 (5.8%) 3 (3.9%) - 2 "MUSCULOSKELET~ 2 0 C 4 (5.8%) 7 (9.09%) - 3 "RENAL AND URI~ 3 0 C 4 (5.8%) 2 (2.6%) - 4 "METABOLISM AN~ 4 0 C 5 (7.25%) 1 (1.3%) - 5 "NERVOUS SYSTE~ 5 0 C 7 (10.14%) 17 (22.08%) - 6 "\t\t\tDizzine~ 5 1 C 2 (2.9%) 6 (7.79%) - 7 "RESPIRATORY, ~ 6 0 C 7 (10.14%) 9 (11.69%) - 8 "\t\t\tCough" 6 1 C 1 (1.45%) 5 (6.49%) - 9 "INVESTIGATION~ 7 0 C 8 (11.59%) 5 (6.49%) - 10 "CARDIAC DISOR~ 8 0 C 9 (13.04%) 10 (12.99%) - # i 19 more rows - # i abbreviated name: 1: `Xanomeline Low Dose_n (%) ` - # i 2 more variables: `Risk Ratio (CI)` , `P-value` + a flextable object. + col_keys: ` `, `Placebo_n (%) `, `Xanomeline Low Dose_n (%) `, `Risk Ratio (CI)` + header has 2 row(s) + body has 25 row(s) + original dataset sample: + DPTVARN DPTVALN CN Placebo_n (%) + 1 Any AE 0 0 C 42 (67.7%) + 2 CARDIAC DISORDERS 1 0 C 1 ( 1.6%) + 3 \t\t\tSINUS BRADYCARDIA 1 1 C 1 ( 1.6%) + 4 GASTROINTESTINAL DISORDERS 2 0 C 9 (14.5%) + 5 \t\t\tDIARRHOEA 2 1 C 9 (14.5%) + Xanomeline Low Dose_n (%) Risk Ratio (CI) + 1 64 (87.7%) 0.199 (0.06, 0.34) + 2 4 ( 5.5%) 0.039 (-0.02, 0.1) + 3 4 ( 5.5%) 0.039 (-0.02, 0.1) + 4 3 ( 4.1%) -0.104 (-0.2, -0.01) + 5 3 ( 4.1%) -0.104 (-0.2, -0.01) diff --git a/tests/testthat/_snaps/adlb_r301.md b/tests/testthat/_snaps/adlb_r301.md new file mode 100644 index 0000000..5a0422f --- /dev/null +++ b/tests/testthat/_snaps/adlb_r301.md @@ -0,0 +1,267 @@ +# lab_abnormality_summary works as expected with different options + + Code + x + Output + [1] "CLINICAL CHEMISTRY" "CLINICAL CHEMISTRY" "CLINICAL CHEMISTRY" + [4] "CLINICAL CHEMISTRY" "CLINICAL CHEMISTRY" + +--- + + Code + x + Output + [1] "Bilirubin (micromol/L)" + [2] "Aspartate Aminotransferase (microkat/L)" + [3] "Alanine Aminotransferase (microkat/L)" + [4] "Sodium (mmol/L)" + [5] "Sodium (mmol/L)" + +--- + + Code + x + Output + [1] "> 1.5x ULN" "> 3.0x ULN" "> 3.0x ULN" "< 0.95x LLN" "> 1.05x ULN" + +--- + + Code + x + Output + [1] 2 3 3 1 1 + +--- + + Code + x + Output + [1] 2 2 2 1 2 + +--- + + Code + x + Output + [1] "C" "C" "C" "C" "C" + +--- + + Code + x + Output + [1] "51" "50" "51" "51" "51" + +--- + + Code + x + Output + [1] "0" "3 (6.00)" "1 (1.96)" "1 (1.96)" "0" + +--- + + Code + x + Output + [1] "CLINICAL CHEMISTRY" "CLINICAL CHEMISTRY" "CLINICAL CHEMISTRY" + [4] "CLINICAL CHEMISTRY" "CLINICAL CHEMISTRY" + +--- + + Code + x + Output + [1] "Bilirubin (micromol/L)" + [2] "Aspartate Aminotransferase (microkat/L)" + [3] "Alanine Aminotransferase (microkat/L)" + [4] "Sodium (mmol/L)" + [5] "Sodium (mmol/L)" + +--- + + Code + x + Output + [1] "> 1.5x ULN" "> 3.0x ULN" "> 3.0x ULN" "< 0.95x LLN" "> 1.05x ULN" + +--- + + Code + x + Output + [1] 2 3 3 1 1 + +--- + + Code + x + Output + [1] 2 2 2 1 2 + +--- + + Code + x + Output + [1] "C" "C" "C" "C" "C" + +--- + + Code + x + Output + [1] "43" "42" "43" "43" "43" + +--- + + Code + x + Output + [1] "0" "3 (7.14)" "1 (2.33)" "1 (2.33)" "0" + +--- + + Code + x + Output + [1] "8" "8" "8" "8" "8" + +--- + + Code + x + Output + [1] "0" "0" "0" "0" "0" + +--- + + Code + x + Output + [1] "51" "50" "51" "51" "51" + +--- + + Code + x + Output + [1] "0" "3 (6.00)" "1 (1.96)" "1 (1.96)" "0" + +--- + + Code + x + Output + [1] "43" "42" "43" "43" "43" + +--- + + Code + x + Output + [1] "0" "3 (7.14)" "1 (2.33)" "1 (2.33)" "0" + +--- + + Code + x + Output + [1] "8" "8" "8" "8" "8" + +--- + + Code + x + Output + [1] "0" "0" "0" "0" "0" + +--- + + Code + x + Output + [1] "51" "50" "51" "51" "51" + +--- + + Code + x + Output + [1] "0" "3 (6.00)" "1 (1.96)" "1 (1.96)" "0" + +--- + + Code + x + Output + [1] "CLINICAL CHEMISTRY" "CLINICAL CHEMISTRY" "CLINICAL CHEMISTRY" + [4] "CLINICAL CHEMISTRY" "CLINICAL CHEMISTRY" + +--- + + Code + x + Output + [1] "Bilirubin (micromol/L)" + [2] "Aspartate Aminotransferase (microkat/L)" + [3] "Alanine Aminotransferase (microkat/L)" + [4] "Sodium (mmol/L)" + [5] "Sodium (mmol/L)" + +--- + + Code + x + Output + [1] "> 1.5x ULN" "> 3.0x ULN" "> 3.0x ULN" "< 0.95x LLN" "> 1.05x ULN" + +--- + + Code + x + Output + [1] 2 3 3 1 1 + +--- + + Code + x + Output + [1] 2 2 2 1 2 + +--- + + Code + x + Output + [1] "C" "C" "C" "C" "C" + +--- + + Code + x + Output + [1] "43" "42" "43" "43" "43" + +--- + + Code + x + Output + [1] "0" "3 (7.14)" "1 (2.33)" "1 (2.33)" "0" + +--- + + Code + x + Output + [1] "8" "8" "8" "8" "8" + +--- + + Code + x + Output + [1] "0" "0" "0" "0" "0" + diff --git a/tests/testthat/_snaps/ae_forestplot.md b/tests/testthat/_snaps/ae_forestplot.md new file mode 100644 index 0000000..a8b313a --- /dev/null +++ b/tests/testthat/_snaps/ae_forestplot.md @@ -0,0 +1,90 @@ +# Test Case 2: Standard Inputs 1 + + Code + x[["geom_params"]][-1] + Output + $xmin + [1] 0 + + $xmax + [1] 1 + + $ymin + [1] 0.15 + + $ymax + [1] 1 + + $scale + [1] 1 + + $clip + [1] "inherit" + + $halign + [1] 0.5 + + $valign + [1] 0.5 + + +--- + + Code + x[["geom_params"]][-1] + Output + $xmin + [1] 0 + + $xmax + [1] 1 + + $ymin + [1] 0.075 + + $ymax + [1] 0.15 + + $scale + [1] 1 + + $clip + [1] "inherit" + + $halign + [1] 0.5 + + $valign + [1] 0.5 + + +--- + + Code + x[["geom_params"]][-1] + Output + $xmin + [1] 0 + + $xmax + [1] 1 + + $ymin + [1] 0 + + $ymax + [1] 0.075 + + $scale + [1] 1 + + $clip + [1] "inherit" + + $halign + [1] 0.5 + + $valign + [1] 0.5 + + diff --git a/tests/testthat/_snaps/ae_volcano_plot.md b/tests/testthat/_snaps/ae_volcano_plot.md new file mode 100644 index 0000000..c5a5031 --- /dev/null +++ b/tests/testthat/_snaps/ae_volcano_plot.md @@ -0,0 +1,86 @@ +# Test 1: Volcano plot with standard inputs + + Code + volcano_test[["mapping"]] + Output + Aesthetic mapping: + * `x` -> `.data[["RISK"]]` + * `y` -> `.data[["PVALUE"]]` + * `text` -> `.data[["HOVER_TEXT"]]` + * `fill` -> `.data[["BYVAR1"]]` + +--- + + Code + volcano_test[["labels"]] + Output + $x + [1] "RISK" + + $y + [1] "PVALUE" + + $text + [1] "HOVER_TEXT" + + $fill + [1] "BYVAR1" + + $size + [1] "CTRL_N" + + $yintercept + [1] "yintercept" + + $xintercept + [1] "xintercept" + + +--- + + Code + x$aes_params + Output + $shape + [1] 21 + + $alpha + [1] 0.5 + + +--- + + Code + x$aes_params + Output + $colour + [1] "grey30" + + $linetype + [1] "dashed" + + +--- + + Code + x$aes_params + Output + $colour + [1] "grey30" + + $linetype + [1] "dashed" + + +--- + + Code + x$aes_params + Output + $colour + [1] "grey30" + + $linetype + [1] "dotted" + + diff --git a/tests/testthat/_snaps/bar_plot.md b/tests/testthat/_snaps/bar_plot.md new file mode 100644 index 0000000..25ed08f --- /dev/null +++ b/tests/testthat/_snaps/bar_plot.md @@ -0,0 +1,32 @@ +# Test Case 1: bar_plot works with expected inputs + + Code + bar_out[[x]] + Output + Aesthetic mapping: + * `x` -> `XVAR` + * `y` -> `YVAR` + * `fill` -> `.data[["TRTVAR"]]` + * `group` -> `.data[["TRTVAR"]]` + +--- + + Code + bar_out[[x]] + Output + $x + [1] "" + + $y + [1] "" + + $title + NULL + + $fill + [1] "TRTVAR" + + $group + [1] "TRTVAR" + + diff --git a/tests/testthat/_snaps/box_plot.md b/tests/testthat/_snaps/box_plot.md new file mode 100644 index 0000000..3b3f513 --- /dev/null +++ b/tests/testthat/_snaps/box_plot.md @@ -0,0 +1,168 @@ +# Standard box plot outputs + + Code + p[[x]] + Output + Aesthetic mapping: + * `colour` -> `.data[["TRTVAR"]]` + * `x` -> `.data[["XVAR"]]` + * `ymin` -> `.data[["whiskerlow"]]` + * `ymax` -> `.data[["whiskerup"]]` + * `lower` -> `.data[["q25"]]` + * `upper` -> `.data[["q75"]]` + * `middle` -> `.data[["median"]]` + +--- + + Code + p[[x]] + Output + $x + [1] "Race" + + $y + [1] "Age" + + $title + NULL + + $colour + [1] "TRTVAR" + + $ymin + [1] "whiskerlow" + + $ymax + [1] "whiskerup" + + $lower + [1] "q25" + + $upper + [1] "q75" + + $middle + [1] "median" + + $group + [1] "TRTVAR" + + $shape + [1] "TRTVAR" + + $size + [1] "TRTVAR" + + +--- + + Code + p[[x]] + Output + Aesthetic mapping: + * `fill` -> `.data[["TRTVAR"]]` + * `x` -> `.data[["XVAR"]]` + * `ymin` -> `.data[["whiskerlow"]]` + * `ymax` -> `.data[["whiskerup"]]` + * `lower` -> `.data[["q25"]]` + * `upper` -> `.data[["q75"]]` + * `middle` -> `.data[["median"]]` + +--- + + Code + p[[x]] + Output + $x + [1] "Race" + + $y + [1] "Age" + + $title + NULL + + $fill + [1] "TRTVAR" + + $ymin + [1] "whiskerlow" + + $ymax + [1] "whiskerup" + + $lower + [1] "q25" + + $upper + [1] "q75" + + $middle + [1] "median" + + $group + [1] "TRTVAR" + + $shape + [1] "TRTVAR" + + $size + [1] "TRTVAR" + + +--- + + Code + p[[x]] + Output + Aesthetic mapping: + * `fill` -> `.data[["TRTVAR"]]` + * `x` -> `.data[["XVAR"]]` + * `ymin` -> `.data[["min"]]` + * `ymax` -> `.data[["max"]]` + * `lower` -> `.data[["q25"]]` + * `upper` -> `.data[["q75"]]` + * `middle` -> `.data[["median"]]` + +--- + + Code + p[[x]] + Output + $x + [1] "Race" + + $y + [1] "Age" + + $title + NULL + + $fill + [1] "TRTVAR" + + $ymin + [1] "min" + + $ymax + [1] "max" + + $lower + [1] "q25" + + $upper + [1] "q75" + + $middle + [1] "median" + + $group + [1] "TRTVAR" + + $shape + [1] "TRTVAR" + + $size + [1] "TRTVAR" + + diff --git a/tests/testthat/_snaps/event_analysis.md b/tests/testthat/_snaps/event_analysis.md new file mode 100644 index 0000000..0cf31ae --- /dev/null +++ b/tests/testthat/_snaps/event_analysis.md @@ -0,0 +1,396 @@ +# Test Case 1: process_event_analysis works with expected inputs + + Code + print(tibble::as_tibble(x), n = Inf, width = Inf) + Output + # A tibble: 12 x 19 + BYVAR1 TRTVAR DPTVAR + + 1 Abdominal Pain/Narrow "Placebo" AEDECOD + 2 Abdominal Pain/Narrow~~Dyspepsia/Broad "Placebo" AEDECOD + 3 Abdominal Pain/Narrow "Placebo" AEDECOD + 4 Abdominal Pain/Narrow "Xanomeline Low\nDose" AEDECOD + 5 Abdominal Pain/Narrow~~Dyspepsia/Broad "Xanomeline Low\nDose" AEDECOD + 6 Abdominal Pain/Narrow "Xanomeline Low\nDose" AEDECOD + 7 Abdominal Pain/Narrow "Xanomeline High\nDose" AEDECOD + 8 Abdominal Pain/Narrow "Xanomeline High\nDose" AEDECOD + 9 Abdominal Pain/Narrow~~Dyspepsia/Broad "Xanomeline High\nDose" AEDECOD + 10 Abdominal Pain/Narrow "Total" AEDECOD + 11 Abdominal Pain/Narrow "Total" AEDECOD + 12 Abdominal Pain/Narrow~~Dyspepsia/Broad "Total" AEDECOD + DPTVAL CVALUE DENOMN FREQ DPTVALN BYVAR1N PCT CPCT + + 1 ABDOMINAL PAIN 1 ( 0.70%) 142 1 2 1 0.704 " 0.70" + 2 ABDOMINAL DISCOMFORT 0 142 0 1 2 0 " 0.00" + 3 STOMACH DISCOMFORT 0 142 0 130 1 0 " 0.00" + 4 ABDOMINAL PAIN 3 ( 1.45%) 207 3 2 1 1.45 " 1.45" + 5 ABDOMINAL DISCOMFORT 0 207 0 1 2 0 " 0.00" + 6 STOMACH DISCOMFORT 0 207 0 130 1 0 " 0.00" + 7 ABDOMINAL PAIN 1 ( 0.45%) 222 1 2 1 0.450 " 0.45" + 8 STOMACH DISCOMFORT 1 ( 0.45%) 222 1 130 1 0.450 " 0.45" + 9 ABDOMINAL DISCOMFORT 1 ( 0.45%) 222 1 1 2 0.450 " 0.45" + 10 ABDOMINAL PAIN 5 ( 0.88%) 571 5 2 1 0.876 " 0.88" + 11 STOMACH DISCOMFORT 1 ( 0.18%) 571 1 130 1 0.175 " 0.18" + 12 ABDOMINAL DISCOMFORT 1 ( 0.18%) 571 1 1 2 0.175 " 0.18" + XVAR DPTVARN CN HTERM HVAL + + 1 ABDOMINAL PAIN 1 C FMQ_NAM ABDOMINAL PAIN + 2 ABDOMINAL DISCOMFORT 1 C FMQ_NAM ABDOMINAL PAIN + 3 STOMACH DISCOMFORT 1 C FMQ_NAM ABDOMINAL PAIN + 4 ABDOMINAL PAIN 1 C FMQ_NAM ABDOMINAL PAIN + 5 ABDOMINAL DISCOMFORT 1 C FMQ_NAM ABDOMINAL PAIN + 6 STOMACH DISCOMFORT 1 C FMQ_NAM ABDOMINAL PAIN + 7 ABDOMINAL PAIN 1 C FMQ_NAM ABDOMINAL PAIN + 8 STOMACH DISCOMFORT 1 C FMQ_NAM ABDOMINAL PAIN + 9 ABDOMINAL DISCOMFORT 1 C FMQ_NAM ABDOMINAL PAIN + 10 ABDOMINAL PAIN 1 C FMQ_NAM ABDOMINAL PAIN + 11 STOMACH DISCOMFORT 1 C FMQ_NAM ABDOMINAL PAIN + 12 ABDOMINAL DISCOMFORT 1 C FMQ_NAM ABDOMINAL PAIN + LVAL Percent DECODh + + 1 ABDOMINAL DISCOMFORT " 0.70 \n Low Term:ABDOMINAL PAIN" 3 + 2 ABDOMINAL DISCOMFORT " 0.00 \n Low Term:ABDOMINAL DISCOMFORT" 9999 + 3 ABDOMINAL DISCOMFORT " 0.00 \n Low Term:STOMACH DISCOMFORT" 1.5 + 4 ABDOMINAL DISCOMFORT " 1.45 \n Low Term:ABDOMINAL PAIN" 3 + 5 ABDOMINAL DISCOMFORT " 0.00 \n Low Term:ABDOMINAL DISCOMFORT" 9999 + 6 ABDOMINAL DISCOMFORT " 0.00 \n Low Term:STOMACH DISCOMFORT" 1.5 + 7 ABDOMINAL DISCOMFORT " 0.45 \n Low Term:ABDOMINAL PAIN" 2 + 8 ABDOMINAL DISCOMFORT " 0.45 \n Low Term:STOMACH DISCOMFORT" 2 + 9 ABDOMINAL DISCOMFORT " 0.45 \n Low Term:ABDOMINAL DISCOMFORT" 9999 + 10 ABDOMINAL DISCOMFORT " 0.88 \n Low Term:ABDOMINAL PAIN" 3 + 11 ABDOMINAL DISCOMFORT " 0.18 \n Low Term:STOMACH DISCOMFORT" 1.5 + 12 ABDOMINAL DISCOMFORT " 0.18 \n Low Term:ABDOMINAL DISCOMFORT" 9999 + +--- + + Code + print(tibble::as_tibble(x), n = Inf, width = Inf) + Output + # A tibble: 4 x 15 + BYVAR1 TRTVAR DPTVAR + + 1 Abdominal Pain/Narrow~~Dyspepsia/Broad "Placebo" AEDECOD + 2 Abdominal Pain/Narrow~~Dyspepsia/Broad "Xanomeline Low\nDose" AEDECOD + 3 Abdominal Pain/Narrow~~Dyspepsia/Broad "Xanomeline High\nDose" AEDECOD + 4 Abdominal Pain/Narrow~~Dyspepsia/Broad "Total" AEDECOD + DPTVAL CVALUE DENOMN FREQ DPTVALN BYVAR1N PCT CPCT + + 1 ABDOMINAL DISCOMFORT 0 142 0 1 2 0 " 0.00" + 2 ABDOMINAL DISCOMFORT 0 207 0 1 2 0 " 0.00" + 3 ABDOMINAL DISCOMFORT 1 ( 0.45%) 222 1 1 2 0.450 " 0.45" + 4 ABDOMINAL DISCOMFORT 1 ( 0.18%) 571 1 1 2 0.175 " 0.18" + XVAR DPTVARN CN Percent + + 1 ABDOMINAL DISCOMFORT 1 C " 0.00" + 2 ABDOMINAL DISCOMFORT 1 C " 0.00" + 3 ABDOMINAL DISCOMFORT 1 C " 0.45" + 4 ABDOMINAL DISCOMFORT 1 C " 0.18" + +# Test Case 2: event_analysis_plot works with expected inputs + + Code + plot$x$data + Output + [[1]] + [[1]]$orientation + [1] "v" + + [[1]]$width + [1] 0.4 0.4 0.4 0.4 + attr(,"class") + [1] "mapped_discrete" "numeric" + attr(,"apiSrc") + [1] TRUE + + [[1]]$base + [1] 0 0 0 0 + attr(,"apiSrc") + [1] TRUE + + [[1]]$x + [1] 1 2 3 4 + attr(,"class") + [1] "mapped_discrete" "numeric" + attr(,"apiSrc") + [1] TRUE + + [[1]]$y + [1] 0.0000000 0.0000000 0.4504505 0.1751313 + attr(,"apiSrc") + [1] TRUE + + [[1]]$text + [1] " 0.00" " 0.00" " 0.45" " 0.18" + attr(,"apiSrc") + [1] TRUE + + [[1]]$type + [1] "bar" + + [[1]]$textposition + [1] "none" + + [[1]]$marker + [[1]]$marker$autocolorscale + [1] FALSE + + [[1]]$marker$color + [1] "rgba(58,95,205,1)" + + [[1]]$marker$line + [[1]]$marker$line$width + [1] 1.511811 + + [[1]]$marker$line$color + [1] "rgba(96,96,96,1)" + + + + [[1]]$showlegend + [1] FALSE + + [[1]]$xaxis + [1] "x" + + [[1]]$yaxis + [1] "y" + + [[1]]$hoverinfo + [1] "text" + + [[1]]$name + [1] "" + + + [[2]] + [[2]]$orientation + [1] "v" + + [[2]]$width + [1] 0.4 0.4 0.4 0.4 + attr(,"class") + [1] "mapped_discrete" "numeric" + attr(,"apiSrc") + [1] TRUE + + [[2]]$base + [1] 0 0 0 0 + attr(,"apiSrc") + [1] TRUE + + [[2]]$x + [1] 1 2 3 4 + attr(,"class") + [1] "mapped_discrete" "numeric" + attr(,"apiSrc") + [1] TRUE + + [[2]]$y + [1] 0.0000000 0.0000000 0.4504505 0.1751313 + attr(,"apiSrc") + [1] TRUE + + [[2]]$text + [1] " 0.00
Low Term:ABDOMINAL DISCOMFORT" + [2] " 0.00
Low Term:ABDOMINAL DISCOMFORT" + [3] " 0.45
Low Term:ABDOMINAL DISCOMFORT" + [4] " 0.18
Low Term:ABDOMINAL DISCOMFORT" + attr(,"apiSrc") + [1] TRUE + + [[2]]$type + [1] "bar" + + [[2]]$textposition + [1] "none" + + [[2]]$marker + [[2]]$marker$autocolorscale + [1] FALSE + + [[2]]$marker$color + [1] "rgba(58,95,205,1)" + + [[2]]$marker$line + [[2]]$marker$line$width + [1] 1.511811 + + [[2]]$marker$line$color + [1] "rgba(96,96,96,1)" + + + + [[2]]$name + [1] "ABDOMINAL DISCOMFORT" + + [[2]]$legendgroup + [1] "ABDOMINAL DISCOMFORT" + + [[2]]$showlegend + [1] TRUE + + [[2]]$xaxis + [1] "x2" + + [[2]]$yaxis + [1] "y2" + + [[2]]$hoverinfo + [1] "text" + + + [[3]] + [[3]]$orientation + [1] "v" + + [[3]]$width + [1] 0.4 0.4 0.4 0.4 + attr(,"class") + [1] "mapped_discrete" "numeric" + attr(,"apiSrc") + [1] TRUE + + [[3]]$base + [1] 0.0000000 0.0000000 0.4504505 0.1751313 + attr(,"apiSrc") + [1] TRUE + + [[3]]$x + [1] 1 2 3 4 + attr(,"class") + [1] "mapped_discrete" "numeric" + attr(,"apiSrc") + [1] TRUE + + [[3]]$y + [1] 0.7042254 1.4492754 0.4504505 0.8756567 + attr(,"apiSrc") + [1] TRUE + + [[3]]$text + [1] " 0.70
Low Term:ABDOMINAL PAIN" + [2] " 1.45
Low Term:ABDOMINAL PAIN" + [3] " 0.45
Low Term:ABDOMINAL PAIN" + [4] " 0.88
Low Term:ABDOMINAL PAIN" + attr(,"apiSrc") + [1] TRUE + + [[3]]$type + [1] "bar" + + [[3]]$textposition + [1] "none" + + [[3]]$marker + [[3]]$marker$autocolorscale + [1] FALSE + + [[3]]$marker$color + [1] "rgba(248,118,109,1)" + + [[3]]$marker$line + [[3]]$marker$line$width + [1] 1.511811 + + [[3]]$marker$line$color + [1] "rgba(96,96,96,1)" + + + + [[3]]$name + [1] "ABDOMINAL PAIN" + + [[3]]$legendgroup + [1] "ABDOMINAL PAIN" + + [[3]]$showlegend + [1] TRUE + + [[3]]$xaxis + [1] "x2" + + [[3]]$yaxis + [1] "y2" + + [[3]]$hoverinfo + [1] "text" + + + [[4]] + [[4]]$orientation + [1] "v" + + [[4]]$width + [1] 0.4 0.4 0.4 0.4 + attr(,"class") + [1] "mapped_discrete" "numeric" + attr(,"apiSrc") + [1] TRUE + + [[4]]$base + [1] 0.7042254 1.4492754 0.9009009 1.0507881 + attr(,"apiSrc") + [1] TRUE + + [[4]]$x + [1] 1 2 3 4 + attr(,"class") + [1] "mapped_discrete" "numeric" + attr(,"apiSrc") + [1] TRUE + + [[4]]$y + [1] 0.0000000 0.0000000 0.4504505 0.1751313 + attr(,"apiSrc") + [1] TRUE + + [[4]]$text + [1] " 0.00
Low Term:STOMACH DISCOMFORT" + [2] " 0.00
Low Term:STOMACH DISCOMFORT" + [3] " 0.45
Low Term:STOMACH DISCOMFORT" + [4] " 0.18
Low Term:STOMACH DISCOMFORT" + attr(,"apiSrc") + [1] TRUE + + [[4]]$type + [1] "bar" + + [[4]]$textposition + [1] "none" + + [[4]]$marker + [[4]]$marker$autocolorscale + [1] FALSE + + [[4]]$marker$color + [1] "rgba(0,191,196,1)" + + [[4]]$marker$line + [[4]]$marker$line$width + [1] 1.511811 + + [[4]]$marker$line$color + [1] "rgba(96,96,96,1)" + + + + [[4]]$name + [1] "STOMACH DISCOMFORT" + + [[4]]$legendgroup + [1] "STOMACH DISCOMFORT" + + [[4]]$showlegend + [1] TRUE + + [[4]]$xaxis + [1] "x2" + + [[4]]$yaxis + [1] "y2" + + [[4]]$hoverinfo + [1] "text" + + + diff --git a/tests/testthat/_snaps/forest_plot.md b/tests/testthat/_snaps/forest_plot.md new file mode 100644 index 0000000..2b0149d --- /dev/null +++ b/tests/testthat/_snaps/forest_plot.md @@ -0,0 +1,297 @@ +# Test case 1: Forest Plot Base Works with standard inputs + + Code + fp[[x]] + Output + Aesthetic mapping: + * `x` -> `.data[["RISK"]]` + * `y` -> `.data[["DPTVAL"]]` + * `xmin` -> `.data[["RISKCIL"]]` + * `xmax` -> `.data[["RISKCIU"]]` + * `text` -> `.data[["HOVER_RISK"]]` + * `group` -> `.data[["TRTPAIR"]]` + * `colour` -> `.data[["TRTPAIR"]]` + +--- + + Code + fp[[x]] + Output + [[1]] + geom_errorbarh: na.rm = FALSE, width = 0.1 + stat_identity: na.rm = FALSE + position_dodgev + + [[2]] + geom_point: na.rm = FALSE + stat_identity: na.rm = FALSE + position_dodgev + + [[3]] + mapping: xintercept = ~xintercept + geom_vline: na.rm = FALSE + stat_identity: na.rm = FALSE + position_identity + + [[4]] + mapping: yintercept = ~yintercept + geom_hline: na.rm = FALSE + stat_identity: na.rm = FALSE + position_identity + + +--- + + Code + fp[[x]] + Output + $axis.title.x + List of 11 + $ family : NULL + $ face : NULL + $ colour : NULL + $ size : num 8 + $ hjust : NULL + $ vjust : NULL + $ angle : NULL + $ lineheight : NULL + $ margin : NULL + $ debug : NULL + $ inherit.blank: logi FALSE + - attr(*, "class")= chr [1:2] "element_text" "element" + + $axis.title.y + list() + - attr(*, "class")= chr [1:2] "element_blank" "element" + + $axis.text.x + List of 11 + $ family : NULL + $ face : NULL + $ colour : NULL + $ size : num 8 + $ hjust : NULL + $ vjust : NULL + $ angle : NULL + $ lineheight : NULL + $ margin : NULL + $ debug : NULL + $ inherit.blank: logi FALSE + - attr(*, "class")= chr [1:2] "element_text" "element" + + $axis.text.y + list() + - attr(*, "class")= chr [1:2] "element_blank" "element" + + $axis.ticks + list() + - attr(*, "class")= chr [1:2] "element_blank" "element" + + $axis.line + List of 6 + $ colour : chr "black" + $ linewidth : NULL + $ linetype : NULL + $ lineend : NULL + $ arrow : logi FALSE + $ inherit.blank: logi FALSE + - attr(*, "class")= chr [1:2] "element_line" "element" + + $legend.position + [1] "bottom" + + $legend.direction + [1] "horizontal" + + $panel.background + list() + - attr(*, "class")= chr [1:2] "element_blank" "element" + + $panel.border + List of 5 + $ fill : logi NA + $ colour : chr "black" + $ linewidth : num 1 + $ linetype : NULL + $ inherit.blank: logi FALSE + - attr(*, "class")= chr [1:2] "element_rect" "element" + + $panel.grid.major + list() + - attr(*, "class")= chr [1:2] "element_blank" "element" + + $panel.grid.minor + list() + - attr(*, "class")= chr [1:2] "element_blank" "element" + + $plot.title + List of 11 + $ family : NULL + $ face : NULL + $ colour : NULL + $ size : num 10 + $ hjust : num 0.1 + $ vjust : NULL + $ angle : NULL + $ lineheight : NULL + $ margin : NULL + $ debug : NULL + $ inherit.blank: logi FALSE + - attr(*, "class")= chr [1:2] "element_text" "element" + + $plot.margin + [1] 0cm 0cm 0cm 0cm + + attr(,"complete") + [1] FALSE + attr(,"validate") + [1] TRUE + +# Test case 1: Forest Plot Scatter Works with standard inputs + + Code + sp[[x]] + Output + Aesthetic mapping: + * `x` -> `.data[["PCT"]]` + * `y` -> `.data[["DPTVAL"]]` + * `colour` -> `.data[["TRTVAR"]]` + * `shape` -> `.data[["TRTVAR"]]` + * `size` -> `.data[["TRTVAR"]]` + * `text` -> `.data[["HOVER_PCT"]]` + +--- + + Code + sp[[x]] + Output + [[1]] + geom_point: na.rm = FALSE + stat_identity: na.rm = FALSE + position_identity + + [[2]] + mapping: yintercept = ~yintercept + geom_hline: na.rm = FALSE + stat_identity: na.rm = FALSE + position_identity + + +--- + + Code + sp[[x]] + Output + $axis.title.x + List of 11 + $ family : NULL + $ face : NULL + $ colour : NULL + $ size : num 8 + $ hjust : NULL + $ vjust : NULL + $ angle : NULL + $ lineheight : NULL + $ margin : NULL + $ debug : NULL + $ inherit.blank: logi FALSE + - attr(*, "class")= chr [1:2] "element_text" "element" + + $axis.title.y + List of 11 + $ family : NULL + $ face : NULL + $ colour : NULL + $ size : NULL + $ hjust : NULL + $ vjust : NULL + $ angle : NULL + $ lineheight : NULL + $ margin : NULL + $ debug : NULL + $ inherit.blank: logi FALSE + - attr(*, "class")= chr [1:2] "element_text" "element" + + $axis.text.x + List of 11 + $ family : NULL + $ face : NULL + $ colour : NULL + $ size : num 4 + $ hjust : NULL + $ vjust : NULL + $ angle : NULL + $ lineheight : NULL + $ margin : NULL + $ debug : NULL + $ inherit.blank: logi FALSE + - attr(*, "class")= chr [1:2] "element_text" "element" + + $axis.text.y + List of 11 + $ family : NULL + $ face : NULL + $ colour : NULL + $ size : NULL + $ hjust : NULL + $ vjust : NULL + $ angle : NULL + $ lineheight : NULL + $ margin : NULL + $ debug : NULL + $ inherit.blank: logi FALSE + - attr(*, "class")= chr [1:2] "element_text" "element" + + $legend.background + List of 5 + $ fill : NULL + $ colour : chr "black" + $ linewidth : NULL + $ linetype : chr "solid" + $ inherit.blank: logi FALSE + - attr(*, "class")= chr [1:2] "element_rect" "element" + + $legend.position + [1] "bottom" + + $legend.direction + [1] "horizontal" + + $panel.background + List of 5 + $ fill : chr "white" + $ colour : chr "black" + $ linewidth : NULL + $ linetype : NULL + $ inherit.blank: logi FALSE + - attr(*, "class")= chr [1:2] "element_rect" "element" + + $panel.border + List of 5 + $ fill : logi NA + $ colour : chr "black" + $ linewidth : num 1 + $ linetype : NULL + $ inherit.blank: logi FALSE + - attr(*, "class")= chr [1:2] "element_rect" "element" + + $plot.title + List of 11 + $ family : NULL + $ face : NULL + $ colour : NULL + $ size : NULL + $ hjust : num 0.5 + $ vjust : NULL + $ angle : NULL + $ lineheight : NULL + $ margin : NULL + $ debug : NULL + $ inherit.blank: logi FALSE + - attr(*, "class")= chr [1:2] "element_text" "element" + + attr(,"complete") + [1] FALSE + attr(,"validate") + [1] TRUE + diff --git a/tests/testthat/_snaps/graph_utils.md b/tests/testthat/_snaps/graph_utils.md new file mode 100644 index 0000000..5210530 --- /dev/null +++ b/tests/testthat/_snaps/graph_utils.md @@ -0,0 +1,161 @@ +# empty_plot works as expected + + Code + exp_ptly_obj + Output + [[1]] + [[1]]$x + [1] 1 + + [[1]]$y + [1] 1 + + [[1]]$text + [1] "No data available for these values" + + [[1]]$hovertext + [1] "x: 1
y: 1" + + [[1]]$textfont + [[1]]$textfont$size + [1] 30.23622 + + [[1]]$textfont$color + [1] "rgba(0,0,0,1)" + + + [[1]]$type + [1] "scatter" + + [[1]]$mode + [1] "text" + + [[1]]$hoveron + [1] "points" + + [[1]]$showlegend + [1] FALSE + + [[1]]$xaxis + [1] "x" + + [[1]]$yaxis + [1] "y" + + [[1]]$hoverinfo + [1] "text" + + [[1]]$name + [1] "" + + + +# theme_cleany works as expected + + Code + actual + Output + List of 14 + $ axis.title.x :List of 11 + ..$ family : NULL + ..$ face : NULL + ..$ colour : NULL + ..$ size : num 8 + ..$ hjust : NULL + ..$ vjust : NULL + ..$ angle : NULL + ..$ lineheight : NULL + ..$ margin : NULL + ..$ debug : NULL + ..$ inherit.blank: logi FALSE + ..- attr(*, "class")= chr [1:2] "element_text" "element" + $ axis.title.y : list() + ..- attr(*, "class")= chr [1:2] "element_blank" "element" + $ axis.text.x :List of 11 + ..$ family : NULL + ..$ face : NULL + ..$ colour : NULL + ..$ size : num 6 + ..$ hjust : NULL + ..$ vjust : NULL + ..$ angle : NULL + ..$ lineheight : NULL + ..$ margin : NULL + ..$ debug : NULL + ..$ inherit.blank: logi FALSE + ..- attr(*, "class")= chr [1:2] "element_text" "element" + $ axis.text.y : list() + ..- attr(*, "class")= chr [1:2] "element_blank" "element" + $ axis.ticks : list() + ..- attr(*, "class")= chr [1:2] "element_blank" "element" + $ axis.line :List of 6 + ..$ colour : chr "black" + ..$ linewidth : NULL + ..$ linetype : NULL + ..$ lineend : NULL + ..$ arrow : logi FALSE + ..$ inherit.blank: logi FALSE + ..- attr(*, "class")= chr [1:2] "element_line" "element" + $ legend.position : chr "bottom" + $ legend.direction: chr "horizontal" + $ panel.background: list() + ..- attr(*, "class")= chr [1:2] "element_blank" "element" + $ panel.border :List of 5 + ..$ fill : logi NA + ..$ colour : chr "black" + ..$ linewidth : num 1 + ..$ linetype : NULL + ..$ inherit.blank: logi FALSE + ..- attr(*, "class")= chr [1:2] "element_rect" "element" + $ panel.grid.major: list() + ..- attr(*, "class")= chr [1:2] "element_blank" "element" + $ panel.grid.minor: list() + ..- attr(*, "class")= chr [1:2] "element_blank" "element" + $ plot.title :List of 11 + ..$ family : NULL + ..$ face : NULL + ..$ colour : NULL + ..$ size : num 10 + ..$ hjust : num 0.1 + ..$ vjust : NULL + ..$ angle : NULL + ..$ lineheight : NULL + ..$ margin : NULL + ..$ debug : NULL + ..$ inherit.blank: logi FALSE + ..- attr(*, "class")= chr [1:2] "element_text" "element" + $ plot.margin : 'simpleUnit' num [1:4] 0cm 0cm 0cm 0cm + ..- attr(*, "unit")= int 1 + - attr(*, "class")= chr [1:2] "theme" "gg" + - attr(*, "complete")= logi FALSE + - attr(*, "validate")= logi TRUE + +# tbl_to_plot works as expected + + Code + fig[[x]] + Output + Aesthetic mapping: + * `x` -> `.data[["CYL"]]` + * `y` -> `.data[["manufacturer"]]` + * `label` -> `.data[["HWY"]]` + * `colour` -> `.data[["CYL"]]` + +--- + + Code + fig[[x]] + Output + $x + [1] "CYL" + + $y + [1] "manufacturer" + + $label + [1] "HWY" + + $colour + [1] "CYL" + + diff --git a/tests/testthat/_snaps/line_plot.md b/tests/testthat/_snaps/line_plot.md new file mode 100644 index 0000000..7b13b9b --- /dev/null +++ b/tests/testthat/_snaps/line_plot.md @@ -0,0 +1,34 @@ +# Standard line plot outputs + + Code + fig[[x]] + Output + Aesthetic mapping: + * `x` -> `.data[["XVAR"]]` + * `y` -> `.data[["YVAR"]]` + * `group` -> `.data[["TRTVAR"]]` + +--- + + Code + fig[[x]] + Output + $x + [1] "Race" + + $y + [1] "Mean Age" + + $fill + [1] "Treatment" + + $title + NULL + + $group + [1] "TRTVAR" + + $colour + [1] "TRTVAR" + + diff --git a/tests/testthat/_snaps/occ_tier_summary.md b/tests/testthat/_snaps/occ_tier_summary.md index 3d06a81..583a84c 100644 --- a/tests/testthat/_snaps/occ_tier_summary.md +++ b/tests/testthat/_snaps/occ_tier_summary.md @@ -1,63 +1,944 @@ -# Standard inputs for occ_tier works +# occ_tier standard inputs works Code - output + print(output, n = Inf, width = Inf) Output - # A tibble: 84 x 13 - TRTVAR DPTVAR DPTVAL CVALUE DENOMN FREQ PCT XVAR CN DPTVARN DPTVALN - - 1 Placebo TIER RESPI~ 1 ( 1~ 52 1 1.92 RESP~ C 1 0 - 2 Placebo TIER CARDI~ 2 ( 3~ 52 2 3.85 CARD~ C 2 0 - 3 Placebo TIER NERVO~ 3 ( 5~ 52 3 5.77 NERV~ C 3 0 - 4 Placebo TIER INFEC~ 8 (15~ 52 8 15.4 INFE~ C 4 0 - 5 Placebo TIER GASTR~ 12 (2~ 52 12 23.1 GAST~ C 5 0 - 6 Placebo TIER GENER~ 16 (3~ 52 16 30.8 GENE~ C 6 0 - 7 Placebo TIER SKIN ~ 17 (3~ 52 17 32.7 SKIN~ C 7 0 - 8 Xanomeli~ TIER CARDI~ 5 ( 7~ 69 5 7.25 CARD~ C 2 0 - 9 Xanomeli~ TIER GASTR~ 6 ( 8~ 69 6 8.7 GAST~ C 5 0 - 10 Xanomeli~ TIER GENER~ 38 (5~ 69 38 55.1 GENE~ C 6 0 - # i 74 more rows - # i 2 more variables: SUBGRPVARX , SUBGRPVARXN + # A tibble: 12 x 14 + TRTVAR DPTVAR + + 1 Placebo TIER + 2 Placebo TIER + 3 Xanomeline Low Dose TIER + 4 Xanomeline Low Dose TIER + 5 Xanomeline High Dose TIER + 6 Xanomeline High Dose TIER + 7 Placebo TIER + 8 Placebo TIER + 9 Xanomeline Low Dose TIER + 10 Xanomeline Low Dose TIER + 11 Xanomeline High Dose TIER + 12 Xanomeline High Dose TIER + DPTVAL CVALUE DENOMN + + 1 "GENERAL DISORDERS AND ADMINISTRATION SITE CONDITIONS" 6 (17.14%) 35 + 2 "SKIN AND SUBCUTANEOUS TISSUE DISORDERS" 8 (22.86%) 35 + 3 "GENERAL DISORDERS AND ADMINISTRATION SITE CONDITIONS" 22 (32.35%) 68 + 4 "SKIN AND SUBCUTANEOUS TISSUE DISORDERS" 23 (33.82%) 68 + 5 "GENERAL DISORDERS AND ADMINISTRATION SITE CONDITIONS" 22 (31.88%) 69 + 6 "SKIN AND SUBCUTANEOUS TISSUE DISORDERS" 26 (37.68%) 69 + 7 "\t\t\tAPPLICATION SITE PRURITUS" 6 ( 8.70%) 69 + 8 "\t\t\tPRURITUS" 8 (11.59%) 69 + 9 "\t\t\tAPPLICATION SITE PRURITUS" 22 (28.57%) 77 + 10 "\t\t\tPRURITUS" 23 (29.87%) 77 + 11 "\t\t\tAPPLICATION SITE PRURITUS" 22 (27.85%) 79 + 12 "\t\t\tPRURITUS" 26 (32.91%) 79 + FREQ PCT CPCT XVAR + + 1 6 17.1 "17.14" GENERAL DISORDERS AND ADMINISTRATION SITE CONDITIONS + 2 8 22.9 "22.86" SKIN AND SUBCUTANEOUS TISSUE DISORDERS + 3 22 32.4 "32.35" GENERAL DISORDERS AND ADMINISTRATION SITE CONDITIONS + 4 23 33.8 "33.82" SKIN AND SUBCUTANEOUS TISSUE DISORDERS + 5 22 31.9 "31.88" GENERAL DISORDERS AND ADMINISTRATION SITE CONDITIONS + 6 26 37.7 "37.68" SKIN AND SUBCUTANEOUS TISSUE DISORDERS + 7 6 8.70 " 8.70" APPLICATION SITE PRURITUS + 8 8 11.6 "11.59" PRURITUS + 9 22 28.6 "28.57" APPLICATION SITE PRURITUS + 10 23 29.9 "29.87" PRURITUS + 11 22 27.8 "27.85" APPLICATION SITE PRURITUS + 12 26 32.9 "32.91" PRURITUS + CN DPTVARN DPTVALN SUBGRPVARX SUBGRPVARXN + + 1 C 1 0 "n (%) " 1 + 2 C 2 0 "n (%) " 1 + 3 C 1 0 "n (%) " 1 + 4 C 2 0 "n (%) " 1 + 5 C 1 0 "n (%) " 1 + 6 C 2 0 "n (%) " 1 + 7 C 1 1 "n (%) " 1 + 8 C 2 1 "n (%) " 1 + 9 C 1 1 "n (%) " 1 + 10 C 2 1 "n (%) " 1 + 11 C 1 1 "n (%) " 1 + 12 C 2 1 "n (%) " 1 ---- +# occ_tier modified inputs works Code - output + print(output, n = Inf, width = Inf) Output - # A tibble: 84 x 13 - TRTVAR DPTVAR DPTVAL CVALUE DENOMN FREQ PCT XVAR CN DPTVARN DPTVALN - - 1 Placebo TIER RESPI~ 1 ( 1~ 52 1 1.92 RESP~ C 1 0 - 2 Placebo TIER CARDI~ 2 ( 3~ 52 2 3.85 CARD~ C 2 0 - 3 Placebo TIER NERVO~ 3 ( 5~ 52 3 5.77 NERV~ C 3 0 - 4 Placebo TIER INFEC~ 8 (15~ 52 8 15.4 INFE~ C 4 0 - 5 Placebo TIER GASTR~ 12 (2~ 52 12 23.1 GAST~ C 5 0 - 6 Placebo TIER GENER~ 16 (3~ 52 16 30.8 GENE~ C 6 0 - 7 Placebo TIER SKIN ~ 17 (3~ 52 17 32.7 SKIN~ C 7 0 - 8 Xanomeli~ TIER CARDI~ 5 ( 7~ 69 5 7.25 CARD~ C 2 0 - 9 Xanomeli~ TIER GASTR~ 6 ( 8~ 69 6 8.7 GAST~ C 5 0 - 10 Xanomeli~ TIER GENER~ 38 (5~ 69 38 55.1 GENE~ C 6 0 - # i 74 more rows - # i 2 more variables: SUBGRPVARX , SUBGRPVARXN + # A tibble: 21 x 14 + TRTVAR DPTVAR + + 1 Placebo TIER + 2 Xanomeline Low Dose TIER + 3 Xanomeline High Dose TIER + 4 Placebo TIER + 5 Xanomeline Low Dose TIER + 6 Xanomeline High Dose TIER + 7 Placebo TIER + 8 Xanomeline Low Dose TIER + 9 Xanomeline High Dose TIER + 10 Placebo TIER + 11 Xanomeline Low Dose TIER + 12 Xanomeline High Dose TIER + 13 Placebo TIER + 14 Xanomeline Low Dose TIER + 15 Xanomeline High Dose TIER + 16 Placebo TIER + 17 Xanomeline Low Dose TIER + 18 Xanomeline High Dose TIER + 19 Placebo TIER + 20 Xanomeline Low Dose TIER + 21 Xanomeline High Dose TIER + DPTVAL CVALUE DENOMN + + 1 "GASTROINTESTINAL DISORDERS" "" 69 + 2 "GASTROINTESTINAL DISORDERS" "" 77 + 3 "GASTROINTESTINAL DISORDERS" "" 79 + 4 "GENERAL DISORDERS AND ADMINISTRATION SITE CONDITIONS" "" 69 + 5 "GENERAL DISORDERS AND ADMINISTRATION SITE CONDITIONS" "" 77 + 6 "GENERAL DISORDERS AND ADMINISTRATION SITE CONDITIONS" "" 79 + 7 "NERVOUS SYSTEM DISORDERS" "" 69 + 8 "NERVOUS SYSTEM DISORDERS" "" 77 + 9 "NERVOUS SYSTEM DISORDERS" "" 79 + 10 "SKIN AND SUBCUTANEOUS TISSUE DISORDERS" "" 69 + 11 "SKIN AND SUBCUTANEOUS TISSUE DISORDERS" "" 77 + 12 "SKIN AND SUBCUTANEOUS TISSUE DISORDERS" "" 79 + 13 "\t\t\tAPPLICATION SITE PRURITUS" "6 ( 8.70%)" 69 + 14 "\t\t\tAPPLICATION SITE PRURITUS" "22 (28.57%)" 77 + 15 "\t\t\tAPPLICATION SITE PRURITUS" "22 (27.85%)" 79 + 16 "\t\t\tPRURITUS" "8 (11.59%)" 69 + 17 "\t\t\tPRURITUS" "23 (29.87%)" 77 + 18 "\t\t\tPRURITUS" "26 (32.91%)" 79 + 19 "Any Adverse Event" "69 (100.00%)" 69 + 20 "Any Adverse Event" "77 (100.00%)" 77 + 21 "Any Adverse Event" "79 (100.00%)" 79 + FREQ PCT CPCT XVAR + + 1 17 24.6 "24.64" GASTROINTESTINAL DISORDERS + 2 15 19.5 "19.48" GASTROINTESTINAL DISORDERS + 3 21 26.6 "26.58" GASTROINTESTINAL DISORDERS + 4 21 30.4 "30.43" GENERAL DISORDERS AND ADMINISTRATION SITE CONDITIONS + 5 47 61.0 "61.04" GENERAL DISORDERS AND ADMINISTRATION SITE CONDITIONS + 6 40 50.6 "50.63" GENERAL DISORDERS AND ADMINISTRATION SITE CONDITIONS + 7 12 17.4 "17.39" NERVOUS SYSTEM DISORDERS + 8 20 26.0 "25.97" NERVOUS SYSTEM DISORDERS + 9 27 34.2 "34.18" NERVOUS SYSTEM DISORDERS + 10 21 30.4 "30.43" SKIN AND SUBCUTANEOUS TISSUE DISORDERS + 11 42 54.5 "54.55" SKIN AND SUBCUTANEOUS TISSUE DISORDERS + 12 42 53.2 "53.16" SKIN AND SUBCUTANEOUS TISSUE DISORDERS + 13 6 8.70 " 8.70" APPLICATION SITE PRURITUS + 14 22 28.6 "28.57" APPLICATION SITE PRURITUS + 15 22 27.8 "27.85" APPLICATION SITE PRURITUS + 16 8 11.6 "11.59" PRURITUS + 17 23 29.9 "29.87" PRURITUS + 18 26 32.9 "32.91" PRURITUS + 19 69 100 "100.00" 1 + 20 77 100 "100.00" 1 + 21 79 100 "100.00" 1 + CN DPTVARN DPTVALN SUBGRPVARX SUBGRPVARXN + + 1 C 1 0 "n (%) " 1 + 2 C 1 0 "n (%) " 1 + 3 C 1 0 "n (%) " 1 + 4 C 2 0 "n (%) " 1 + 5 C 2 0 "n (%) " 1 + 6 C 2 0 "n (%) " 1 + 7 C 3 0 "n (%) " 1 + 8 C 3 0 "n (%) " 1 + 9 C 3 0 "n (%) " 1 + 10 C 4 0 "n (%) " 1 + 11 C 4 0 "n (%) " 1 + 12 C 4 0 "n (%) " 1 + 13 C 2 1 "n (%) " 1 + 14 C 2 1 "n (%) " 1 + 15 C 2 1 "n (%) " 1 + 16 C 4 1 "n (%) " 1 + 17 C 4 1 "n (%) " 1 + 18 C 4 1 "n (%) " 1 + 19 C 0 0 "n (%) " 1 + 20 C 0 0 "n (%) " 1 + 21 C 0 0 "n (%) " 1 -# Cut off applied for occ_tier works +# occ_tier standard inputs works max severity Code - output + print(output, n = Inf, width = Inf) Output - # A tibble: 102 x 13 - TRTVAR DPTVAR DPTVAL CVALUE DENOMN FREQ PCT XVAR CN DPTVARN DPTVALN - - 1 Placebo TIER CARDI~ 9 (13~ 69 9 13.0 CARD~ C 1 0 - 2 Xanomeli~ TIER CARDI~ 10 (1~ 77 10 13.0 CARD~ C 1 0 - 3 Xanomeli~ TIER CARDI~ 10 (1~ 79 10 12.7 CARD~ C 1 0 - 4 Placebo TIER GASTR~ 16 (2~ 69 16 23.2 GAST~ C 2 0 - 5 Xanomeli~ TIER GASTR~ 13 (1~ 77 13 16.9 GAST~ C 2 0 - 6 Xanomeli~ TIER GASTR~ 15 (1~ 79 15 19.0 GAST~ C 2 0 - 7 Placebo TIER GENER~ 21 (3~ 69 21 30.4 GENE~ C 3 0 - 8 Xanomeli~ TIER GENER~ 45 (5~ 77 45 58.4 GENE~ C 3 0 - 9 Xanomeli~ TIER GENER~ 35 (4~ 79 35 44.3 GENE~ C 3 0 - 10 Placebo TIER INFEC~ 16 (2~ 69 16 23.2 INFE~ C 4 0 - # i 92 more rows - # i 2 more variables: SUBGRPVARX , SUBGRPVARXN + # A tibble: 153 x 16 + TRTVAR SUBGRPVAR1 DPTVAR + + 1 Placebo MILD TIER + 2 Placebo MODERATE TIER + 3 Placebo SEVERE TIER + 4 Xanomeline Low Dose MILD TIER + 5 Xanomeline Low Dose MODERATE TIER + 6 Xanomeline Low Dose SEVERE TIER + 7 Xanomeline High Dose MILD TIER + 8 Xanomeline High Dose MODERATE TIER + 9 Xanomeline High Dose SEVERE TIER + 10 Placebo MILD TIER + 11 Placebo MODERATE TIER + 12 Placebo SEVERE TIER + 13 Xanomeline Low Dose MILD TIER + 14 Xanomeline Low Dose MODERATE TIER + 15 Xanomeline Low Dose SEVERE TIER + 16 Xanomeline High Dose MILD TIER + 17 Xanomeline High Dose MODERATE TIER + 18 Xanomeline High Dose SEVERE TIER + 19 Placebo MILD TIER + 20 Placebo MODERATE TIER + 21 Placebo SEVERE TIER + 22 Xanomeline Low Dose MILD TIER + 23 Xanomeline Low Dose MODERATE TIER + 24 Xanomeline Low Dose SEVERE TIER + 25 Xanomeline High Dose MILD TIER + 26 Xanomeline High Dose MODERATE TIER + 27 Xanomeline High Dose SEVERE TIER + 28 Placebo MILD TIER + 29 Placebo MODERATE TIER + 30 Placebo SEVERE TIER + 31 Xanomeline Low Dose MILD TIER + 32 Xanomeline Low Dose MODERATE TIER + 33 Xanomeline Low Dose SEVERE TIER + 34 Xanomeline High Dose MILD TIER + 35 Xanomeline High Dose MODERATE TIER + 36 Xanomeline High Dose SEVERE TIER + 37 Placebo MILD TIER + 38 Placebo MODERATE TIER + 39 Placebo SEVERE TIER + 40 Xanomeline Low Dose MILD TIER + 41 Xanomeline Low Dose MODERATE TIER + 42 Xanomeline Low Dose SEVERE TIER + 43 Xanomeline High Dose MILD TIER + 44 Xanomeline High Dose MODERATE TIER + 45 Xanomeline High Dose SEVERE TIER + 46 Placebo MILD TIER + 47 Placebo MODERATE TIER + 48 Placebo SEVERE TIER + 49 Xanomeline Low Dose MILD TIER + 50 Xanomeline Low Dose MODERATE TIER + 51 Xanomeline Low Dose SEVERE TIER + 52 Xanomeline High Dose MILD TIER + 53 Xanomeline High Dose MODERATE TIER + 54 Xanomeline High Dose SEVERE TIER + 55 Placebo MILD TIER + 56 Placebo MODERATE TIER + 57 Placebo SEVERE TIER + 58 Xanomeline Low Dose MILD TIER + 59 Xanomeline Low Dose MODERATE TIER + 60 Xanomeline Low Dose SEVERE TIER + 61 Xanomeline High Dose MILD TIER + 62 Xanomeline High Dose MODERATE TIER + 63 Xanomeline High Dose SEVERE TIER + 64 Placebo MILD TIER + 65 Placebo MODERATE TIER + 66 Placebo SEVERE TIER + 67 Xanomeline Low Dose MILD TIER + 68 Xanomeline Low Dose MODERATE TIER + 69 Xanomeline Low Dose SEVERE TIER + 70 Xanomeline High Dose MILD TIER + 71 Xanomeline High Dose MODERATE TIER + 72 Xanomeline High Dose SEVERE TIER + 73 Placebo MILD TIER + 74 Placebo MODERATE TIER + 75 Placebo SEVERE TIER + 76 Xanomeline Low Dose MILD TIER + 77 Xanomeline Low Dose MODERATE TIER + 78 Xanomeline Low Dose SEVERE TIER + 79 Xanomeline High Dose MILD TIER + 80 Xanomeline High Dose MODERATE TIER + 81 Xanomeline High Dose SEVERE TIER + 82 Placebo MILD TIER + 83 Placebo MODERATE TIER + 84 Placebo SEVERE TIER + 85 Xanomeline Low Dose MILD TIER + 86 Xanomeline Low Dose MODERATE TIER + 87 Xanomeline Low Dose SEVERE TIER + 88 Xanomeline High Dose MILD TIER + 89 Xanomeline High Dose MODERATE TIER + 90 Xanomeline High Dose SEVERE TIER + 91 Placebo MILD TIER + 92 Placebo MODERATE TIER + 93 Placebo SEVERE TIER + 94 Xanomeline Low Dose MILD TIER + 95 Xanomeline Low Dose MODERATE TIER + 96 Xanomeline Low Dose SEVERE TIER + 97 Xanomeline High Dose MILD TIER + 98 Xanomeline High Dose MODERATE TIER + 99 Xanomeline High Dose SEVERE TIER + 100 Placebo MILD TIER + 101 Placebo MODERATE TIER + 102 Placebo SEVERE TIER + 103 Xanomeline Low Dose MILD TIER + 104 Xanomeline Low Dose MODERATE TIER + 105 Xanomeline Low Dose SEVERE TIER + 106 Xanomeline High Dose MILD TIER + 107 Xanomeline High Dose MODERATE TIER + 108 Xanomeline High Dose SEVERE TIER + 109 Placebo MILD TIER + 110 Placebo MODERATE TIER + 111 Placebo SEVERE TIER + 112 Xanomeline Low Dose MILD TIER + 113 Xanomeline Low Dose MODERATE TIER + 114 Xanomeline Low Dose SEVERE TIER + 115 Xanomeline High Dose MILD TIER + 116 Xanomeline High Dose MODERATE TIER + 117 Xanomeline High Dose SEVERE TIER + 118 Placebo MILD TIER + 119 Placebo MODERATE TIER + 120 Placebo SEVERE TIER + 121 Xanomeline Low Dose MILD TIER + 122 Xanomeline Low Dose MODERATE TIER + 123 Xanomeline Low Dose SEVERE TIER + 124 Xanomeline High Dose MILD TIER + 125 Xanomeline High Dose MODERATE TIER + 126 Xanomeline High Dose SEVERE TIER + 127 Placebo MILD TIER + 128 Placebo MODERATE TIER + 129 Placebo SEVERE TIER + 130 Xanomeline Low Dose MILD TIER + 131 Xanomeline Low Dose MODERATE TIER + 132 Xanomeline Low Dose SEVERE TIER + 133 Xanomeline High Dose MILD TIER + 134 Xanomeline High Dose MODERATE TIER + 135 Xanomeline High Dose SEVERE TIER + 136 Placebo MILD TIER + 137 Placebo MODERATE TIER + 138 Placebo SEVERE TIER + 139 Xanomeline Low Dose MILD TIER + 140 Xanomeline Low Dose MODERATE TIER + 141 Xanomeline Low Dose SEVERE TIER + 142 Xanomeline High Dose MILD TIER + 143 Xanomeline High Dose MODERATE TIER + 144 Xanomeline High Dose SEVERE TIER + 145 Placebo MILD TIER + 146 Placebo MODERATE TIER + 147 Xanomeline Low Dose MILD TIER + 148 Xanomeline Low Dose MODERATE TIER + 149 Xanomeline Low Dose SEVERE TIER + 150 Xanomeline High Dose MILD TIER + 151 Xanomeline High Dose MODERATE TIER + 152 Xanomeline High Dose SEVERE TIER + 153 Placebo SEVERE TIER + DPTVAL CVALUE DENOMN + + 1 "CARDIAC DISORDERS" 8 (15.09%) 53 + 2 "CARDIAC DISORDERS" 2 ( 3.77%) 53 + 3 "CARDIAC DISORDERS" 2 ( 3.77%) 53 + 4 "CARDIAC DISORDERS" 8 (10.81%) 74 + 5 "CARDIAC DISORDERS" 5 ( 6.76%) 74 + 6 "CARDIAC DISORDERS" 0 74 + 7 "CARDIAC DISORDERS" 9 (12.33%) 73 + 8 "CARDIAC DISORDERS" 5 ( 6.85%) 73 + 9 "CARDIAC DISORDERS" 1 ( 1.37%) 73 + 10 "GASTROINTESTINAL DISORDERS" 15 (28.30%) 53 + 11 "GASTROINTESTINAL DISORDERS" 2 ( 3.77%) 53 + 12 "GASTROINTESTINAL DISORDERS" 0 53 + 13 "GASTROINTESTINAL DISORDERS" 10 (13.51%) 74 + 14 "GASTROINTESTINAL DISORDERS" 4 ( 5.41%) 74 + 15 "GASTROINTESTINAL DISORDERS" 0 74 + 16 "GASTROINTESTINAL DISORDERS" 14 (19.18%) 73 + 17 "GASTROINTESTINAL DISORDERS" 4 ( 5.48%) 73 + 18 "GASTROINTESTINAL DISORDERS" 2 ( 2.74%) 73 + 19 "GENERAL DISORDERS AND ADMINISTRATION SITE CONDITIONS" 16 (30.19%) 53 + 20 "GENERAL DISORDERS AND ADMINISTRATION SITE CONDITIONS" 5 ( 9.43%) 53 + 21 "GENERAL DISORDERS AND ADMINISTRATION SITE CONDITIONS" 0 53 + 22 "GENERAL DISORDERS AND ADMINISTRATION SITE CONDITIONS" 19 (25.68%) 74 + 23 "GENERAL DISORDERS AND ADMINISTRATION SITE CONDITIONS" 21 (28.38%) 74 + 24 "GENERAL DISORDERS AND ADMINISTRATION SITE CONDITIONS" 7 ( 9.46%) 74 + 25 "GENERAL DISORDERS AND ADMINISTRATION SITE CONDITIONS" 19 (26.03%) 73 + 26 "GENERAL DISORDERS AND ADMINISTRATION SITE CONDITIONS" 21 (28.77%) 73 + 27 "GENERAL DISORDERS AND ADMINISTRATION SITE CONDITIONS" 0 73 + 28 "NERVOUS SYSTEM DISORDERS" 6 (11.32%) 53 + 29 "NERVOUS SYSTEM DISORDERS" 2 ( 3.77%) 53 + 30 "NERVOUS SYSTEM DISORDERS" 0 53 + 31 "NERVOUS SYSTEM DISORDERS" 10 (13.51%) 74 + 32 "NERVOUS SYSTEM DISORDERS" 7 ( 9.46%) 74 + 33 "NERVOUS SYSTEM DISORDERS" 3 ( 4.05%) 74 + 34 "NERVOUS SYSTEM DISORDERS" 13 (17.81%) 73 + 35 "NERVOUS SYSTEM DISORDERS" 8 (10.96%) 73 + 36 "NERVOUS SYSTEM DISORDERS" 4 ( 5.48%) 73 + 37 "SKIN AND SUBCUTANEOUS TISSUE DISORDERS" 12 (22.64%) 53 + 38 "SKIN AND SUBCUTANEOUS TISSUE DISORDERS" 8 (15.09%) 53 + 39 "SKIN AND SUBCUTANEOUS TISSUE DISORDERS" 0 53 + 40 "SKIN AND SUBCUTANEOUS TISSUE DISORDERS" 12 (16.22%) 74 + 41 "SKIN AND SUBCUTANEOUS TISSUE DISORDERS" 23 (31.08%) 74 + 42 "SKIN AND SUBCUTANEOUS TISSUE DISORDERS" 4 ( 5.41%) 74 + 43 "SKIN AND SUBCUTANEOUS TISSUE DISORDERS" 24 (32.88%) 73 + 44 "SKIN AND SUBCUTANEOUS TISSUE DISORDERS" 15 (20.55%) 73 + 45 "SKIN AND SUBCUTANEOUS TISSUE DISORDERS" 1 ( 1.37%) 73 + 46 "\t\t\tSINUS BRADYCARDIA" 1 ( 1.54%) 65 + 47 "\t\t\tSINUS BRADYCARDIA" 1 ( 1.54%) 65 + 48 "\t\t\tSINUS BRADYCARDIA" 0 65 + 49 "\t\t\tSINUS BRADYCARDIA" 6 ( 7.79%) 77 + 50 "\t\t\tSINUS BRADYCARDIA" 1 ( 1.30%) 77 + 51 "\t\t\tSINUS BRADYCARDIA" 0 77 + 52 "\t\t\tSINUS BRADYCARDIA" 4 ( 5.26%) 76 + 53 "\t\t\tSINUS BRADYCARDIA" 4 ( 5.26%) 76 + 54 "\t\t\tSINUS BRADYCARDIA" 0 76 + 55 "\t\t\tDIARRHOEA" 9 (13.85%) 65 + 56 "\t\t\tDIARRHOEA" 0 65 + 57 "\t\t\tDIARRHOEA" 0 65 + 58 "\t\t\tDIARRHOEA" 4 ( 5.19%) 77 + 59 "\t\t\tDIARRHOEA" 0 77 + 60 "\t\t\tDIARRHOEA" 0 77 + 61 "\t\t\tDIARRHOEA" 2 ( 2.63%) 76 + 62 "\t\t\tDIARRHOEA" 2 ( 2.63%) 76 + 63 "\t\t\tDIARRHOEA" 0 76 + 64 "\t\t\tAPPLICATION SITE ERYTHEMA" 3 ( 4.62%) 65 + 65 "\t\t\tAPPLICATION SITE ERYTHEMA" 0 65 + 66 "\t\t\tAPPLICATION SITE ERYTHEMA" 0 65 + 67 "\t\t\tAPPLICATION SITE ERYTHEMA" 4 ( 5.19%) 77 + 68 "\t\t\tAPPLICATION SITE ERYTHEMA" 6 ( 7.79%) 77 + 69 "\t\t\tAPPLICATION SITE ERYTHEMA" 2 ( 2.60%) 77 + 70 "\t\t\tAPPLICATION SITE ERYTHEMA" 9 (11.84%) 76 + 71 "\t\t\tAPPLICATION SITE ERYTHEMA" 6 ( 7.89%) 76 + 72 "\t\t\tAPPLICATION SITE ERYTHEMA" 0 76 + 73 "\t\t\tAPPLICATION SITE IRRITATION" 1 ( 1.54%) 65 + 74 "\t\t\tAPPLICATION SITE IRRITATION" 2 ( 3.08%) 65 + 75 "\t\t\tAPPLICATION SITE IRRITATION" 0 65 + 76 "\t\t\tAPPLICATION SITE IRRITATION" 3 ( 3.90%) 77 + 77 "\t\t\tAPPLICATION SITE IRRITATION" 3 ( 3.90%) 77 + 78 "\t\t\tAPPLICATION SITE IRRITATION" 3 ( 3.90%) 77 + 79 "\t\t\tAPPLICATION SITE IRRITATION" 3 ( 3.95%) 76 + 80 "\t\t\tAPPLICATION SITE IRRITATION" 6 ( 7.89%) 76 + 81 "\t\t\tAPPLICATION SITE IRRITATION" 0 76 + 82 "\t\t\tAPPLICATION SITE PRURITUS" 5 ( 7.69%) 65 + 83 "\t\t\tAPPLICATION SITE PRURITUS" 1 ( 1.54%) 65 + 84 "\t\t\tAPPLICATION SITE PRURITUS" 0 65 + 85 "\t\t\tAPPLICATION SITE PRURITUS" 13 (16.88%) 77 + 86 "\t\t\tAPPLICATION SITE PRURITUS" 8 (10.39%) 77 + 87 "\t\t\tAPPLICATION SITE PRURITUS" 1 ( 1.30%) 77 + 88 "\t\t\tAPPLICATION SITE PRURITUS" 10 (13.16%) 76 + 89 "\t\t\tAPPLICATION SITE PRURITUS" 12 (15.79%) 76 + 90 "\t\t\tAPPLICATION SITE PRURITUS" 0 76 + 91 "\t\t\tDIZZINESS" 2 ( 3.08%) 65 + 92 "\t\t\tDIZZINESS" 0 65 + 93 "\t\t\tDIZZINESS" 0 65 + 94 "\t\t\tDIZZINESS" 5 ( 6.49%) 77 + 95 "\t\t\tDIZZINESS" 3 ( 3.90%) 77 + 96 "\t\t\tDIZZINESS" 0 77 + 97 "\t\t\tDIZZINESS" 7 ( 9.21%) 76 + 98 "\t\t\tDIZZINESS" 3 ( 3.95%) 76 + 99 "\t\t\tDIZZINESS" 1 ( 1.32%) 76 + 100 "\t\t\tERYTHEMA" 4 ( 6.15%) 65 + 101 "\t\t\tERYTHEMA" 4 ( 6.15%) 65 + 102 "\t\t\tERYTHEMA" 0 65 + 103 "\t\t\tERYTHEMA" 6 ( 7.79%) 77 + 104 "\t\t\tERYTHEMA" 8 (10.39%) 77 + 105 "\t\t\tERYTHEMA" 0 77 + 106 "\t\t\tERYTHEMA" 10 (13.16%) 76 + 107 "\t\t\tERYTHEMA" 4 ( 5.26%) 76 + 108 "\t\t\tERYTHEMA" 0 76 + 109 "\t\t\tHYPERHIDROSIS" 2 ( 3.08%) 65 + 110 "\t\t\tHYPERHIDROSIS" 0 65 + 111 "\t\t\tHYPERHIDROSIS" 0 65 + 112 "\t\t\tHYPERHIDROSIS" 1 ( 1.30%) 77 + 113 "\t\t\tHYPERHIDROSIS" 3 ( 3.90%) 77 + 114 "\t\t\tHYPERHIDROSIS" 0 77 + 115 "\t\t\tHYPERHIDROSIS" 8 (10.53%) 76 + 116 "\t\t\tHYPERHIDROSIS" 0 76 + 117 "\t\t\tHYPERHIDROSIS" 0 76 + 118 "\t\t\tPRURITUS" 7 (10.77%) 65 + 119 "\t\t\tPRURITUS" 1 ( 1.54%) 65 + 120 "\t\t\tPRURITUS" 0 65 + 121 "\t\t\tPRURITUS" 9 (11.69%) 77 + 122 "\t\t\tPRURITUS" 11 (14.29%) 77 + 123 "\t\t\tPRURITUS" 1 ( 1.30%) 77 + 124 "\t\t\tPRURITUS" 17 (22.37%) 76 + 125 "\t\t\tPRURITUS" 9 (11.84%) 76 + 126 "\t\t\tPRURITUS" 0 76 + 127 "\t\t\tRASH" 2 ( 3.08%) 65 + 128 "\t\t\tRASH" 3 ( 4.62%) 65 + 129 "\t\t\tRASH" 0 65 + 130 "\t\t\tRASH" 9 (11.69%) 77 + 131 "\t\t\tRASH" 3 ( 3.90%) 77 + 132 "\t\t\tRASH" 1 ( 1.30%) 77 + 133 "\t\t\tRASH" 5 ( 6.58%) 76 + 134 "\t\t\tRASH" 3 ( 3.95%) 76 + 135 "\t\t\tRASH" 1 ( 1.32%) 76 + 136 "Any Adverse Event" 21 (39.62%) 53 + 137 "Any Adverse Event" 8 (15.09%) 53 + 138 "Any Adverse Event" 0 53 + 139 "Any Adverse Event" 16 (21.62%) 74 + 140 "Any Adverse Event" 24 (32.43%) 74 + 141 "Any Adverse Event" 6 ( 8.11%) 74 + 142 "Any Adverse Event" 19 (26.03%) 73 + 143 "Any Adverse Event" 32 (43.84%) 73 + 144 "Any Adverse Event" 2 ( 2.74%) 73 + 145 "Total preferred term events" 36 NA + 146 "Total preferred term events" 12 NA + 147 "Total preferred term events" 60 NA + 148 "Total preferred term events" 46 NA + 149 "Total preferred term events" 8 NA + 150 "Total preferred term events" 75 NA + 151 "Total preferred term events" 49 NA + 152 "Total preferred term events" 2 NA + 153 "Total preferred term events" 0 NA + FREQ SUBGRPVAR1N PCT CPCT + + 1 8 1 15.1 "15.09" + 2 2 2 3.77 " 3.77" + 3 2 3 3.77 " 3.77" + 4 8 1 10.8 "10.81" + 5 5 2 6.76 " 6.76" + 6 0 3 0 " 0.00" + 7 9 1 12.3 "12.33" + 8 5 2 6.85 " 6.85" + 9 1 3 1.37 " 1.37" + 10 15 1 28.3 "28.30" + 11 2 2 3.77 " 3.77" + 12 0 3 0 " 0.00" + 13 10 1 13.5 "13.51" + 14 4 2 5.41 " 5.41" + 15 0 3 0 " 0.00" + 16 14 1 19.2 "19.18" + 17 4 2 5.48 " 5.48" + 18 2 3 2.74 " 2.74" + 19 16 1 30.2 "30.19" + 20 5 2 9.43 " 9.43" + 21 0 3 0 " 0.00" + 22 19 1 25.7 "25.68" + 23 21 2 28.4 "28.38" + 24 7 3 9.46 " 9.46" + 25 19 1 26.0 "26.03" + 26 21 2 28.8 "28.77" + 27 0 3 0 " 0.00" + 28 6 1 11.3 "11.32" + 29 2 2 3.77 " 3.77" + 30 0 3 0 " 0.00" + 31 10 1 13.5 "13.51" + 32 7 2 9.46 " 9.46" + 33 3 3 4.05 " 4.05" + 34 13 1 17.8 "17.81" + 35 8 2 11.0 "10.96" + 36 4 3 5.48 " 5.48" + 37 12 1 22.6 "22.64" + 38 8 2 15.1 "15.09" + 39 0 3 0 " 0.00" + 40 12 1 16.2 "16.22" + 41 23 2 31.1 "31.08" + 42 4 3 5.41 " 5.41" + 43 24 1 32.9 "32.88" + 44 15 2 20.5 "20.55" + 45 1 3 1.37 " 1.37" + 46 1 1 1.54 " 1.54" + 47 1 2 1.54 " 1.54" + 48 0 3 0 " 0.00" + 49 6 1 7.79 " 7.79" + 50 1 2 1.30 " 1.30" + 51 0 3 0 " 0.00" + 52 4 1 5.26 " 5.26" + 53 4 2 5.26 " 5.26" + 54 0 3 0 " 0.00" + 55 9 1 13.8 "13.85" + 56 0 2 0 " 0.00" + 57 0 3 0 " 0.00" + 58 4 1 5.19 " 5.19" + 59 0 2 0 " 0.00" + 60 0 3 0 " 0.00" + 61 2 1 2.63 " 2.63" + 62 2 2 2.63 " 2.63" + 63 0 3 0 " 0.00" + 64 3 1 4.62 " 4.62" + 65 0 2 0 " 0.00" + 66 0 3 0 " 0.00" + 67 4 1 5.19 " 5.19" + 68 6 2 7.79 " 7.79" + 69 2 3 2.60 " 2.60" + 70 9 1 11.8 "11.84" + 71 6 2 7.89 " 7.89" + 72 0 3 0 " 0.00" + 73 1 1 1.54 " 1.54" + 74 2 2 3.08 " 3.08" + 75 0 3 0 " 0.00" + 76 3 1 3.90 " 3.90" + 77 3 2 3.90 " 3.90" + 78 3 3 3.90 " 3.90" + 79 3 1 3.95 " 3.95" + 80 6 2 7.89 " 7.89" + 81 0 3 0 " 0.00" + 82 5 1 7.69 " 7.69" + 83 1 2 1.54 " 1.54" + 84 0 3 0 " 0.00" + 85 13 1 16.9 "16.88" + 86 8 2 10.4 "10.39" + 87 1 3 1.30 " 1.30" + 88 10 1 13.2 "13.16" + 89 12 2 15.8 "15.79" + 90 0 3 0 " 0.00" + 91 2 1 3.08 " 3.08" + 92 0 2 0 " 0.00" + 93 0 3 0 " 0.00" + 94 5 1 6.49 " 6.49" + 95 3 2 3.90 " 3.90" + 96 0 3 0 " 0.00" + 97 7 1 9.21 " 9.21" + 98 3 2 3.95 " 3.95" + 99 1 3 1.32 " 1.32" + 100 4 1 6.15 " 6.15" + 101 4 2 6.15 " 6.15" + 102 0 3 0 " 0.00" + 103 6 1 7.79 " 7.79" + 104 8 2 10.4 "10.39" + 105 0 3 0 " 0.00" + 106 10 1 13.2 "13.16" + 107 4 2 5.26 " 5.26" + 108 0 3 0 " 0.00" + 109 2 1 3.08 " 3.08" + 110 0 2 0 " 0.00" + 111 0 3 0 " 0.00" + 112 1 1 1.30 " 1.30" + 113 3 2 3.90 " 3.90" + 114 0 3 0 " 0.00" + 115 8 1 10.5 "10.53" + 116 0 2 0 " 0.00" + 117 0 3 0 " 0.00" + 118 7 1 10.8 "10.77" + 119 1 2 1.54 " 1.54" + 120 0 3 0 " 0.00" + 121 9 1 11.7 "11.69" + 122 11 2 14.3 "14.29" + 123 1 3 1.30 " 1.30" + 124 17 1 22.4 "22.37" + 125 9 2 11.8 "11.84" + 126 0 3 0 " 0.00" + 127 2 1 3.08 " 3.08" + 128 3 2 4.62 " 4.62" + 129 0 3 0 " 0.00" + 130 9 1 11.7 "11.69" + 131 3 2 3.90 " 3.90" + 132 1 3 1.30 " 1.30" + 133 5 1 6.58 " 6.58" + 134 3 2 3.95 " 3.95" + 135 1 3 1.32 " 1.32" + 136 21 1 39.6 "39.62" + 137 8 2 15.1 "15.09" + 138 0 3 0 " 0.00" + 139 16 1 21.6 "21.62" + 140 24 2 32.4 "32.43" + 141 6 3 8.11 " 8.11" + 142 19 1 26.0 "26.03" + 143 32 2 43.8 "43.84" + 144 2 3 2.74 " 2.74" + 145 36 1 NA + 146 12 2 NA + 147 60 1 NA + 148 46 2 NA + 149 8 3 NA + 150 75 1 NA + 151 49 2 NA + 152 2 3 NA + 153 0 3 NA + XVAR CN DPTVARN DPTVALN + + 1 CARDIAC DISORDERS C 1 0 + 2 CARDIAC DISORDERS C 1 0 + 3 CARDIAC DISORDERS C 1 0 + 4 CARDIAC DISORDERS C 1 0 + 5 CARDIAC DISORDERS C 1 0 + 6 CARDIAC DISORDERS C 1 0 + 7 CARDIAC DISORDERS C 1 0 + 8 CARDIAC DISORDERS C 1 0 + 9 CARDIAC DISORDERS C 1 0 + 10 GASTROINTESTINAL DISORDERS C 2 0 + 11 GASTROINTESTINAL DISORDERS C 2 0 + 12 GASTROINTESTINAL DISORDERS C 2 0 + 13 GASTROINTESTINAL DISORDERS C 2 0 + 14 GASTROINTESTINAL DISORDERS C 2 0 + 15 GASTROINTESTINAL DISORDERS C 2 0 + 16 GASTROINTESTINAL DISORDERS C 2 0 + 17 GASTROINTESTINAL DISORDERS C 2 0 + 18 GASTROINTESTINAL DISORDERS C 2 0 + 19 GENERAL DISORDERS AND ADMINISTRATION SITE CONDITIONS C 3 0 + 20 GENERAL DISORDERS AND ADMINISTRATION SITE CONDITIONS C 3 0 + 21 GENERAL DISORDERS AND ADMINISTRATION SITE CONDITIONS C 3 0 + 22 GENERAL DISORDERS AND ADMINISTRATION SITE CONDITIONS C 3 0 + 23 GENERAL DISORDERS AND ADMINISTRATION SITE CONDITIONS C 3 0 + 24 GENERAL DISORDERS AND ADMINISTRATION SITE CONDITIONS C 3 0 + 25 GENERAL DISORDERS AND ADMINISTRATION SITE CONDITIONS C 3 0 + 26 GENERAL DISORDERS AND ADMINISTRATION SITE CONDITIONS C 3 0 + 27 GENERAL DISORDERS AND ADMINISTRATION SITE CONDITIONS C 3 0 + 28 NERVOUS SYSTEM DISORDERS C 4 0 + 29 NERVOUS SYSTEM DISORDERS C 4 0 + 30 NERVOUS SYSTEM DISORDERS C 4 0 + 31 NERVOUS SYSTEM DISORDERS C 4 0 + 32 NERVOUS SYSTEM DISORDERS C 4 0 + 33 NERVOUS SYSTEM DISORDERS C 4 0 + 34 NERVOUS SYSTEM DISORDERS C 4 0 + 35 NERVOUS SYSTEM DISORDERS C 4 0 + 36 NERVOUS SYSTEM DISORDERS C 4 0 + 37 SKIN AND SUBCUTANEOUS TISSUE DISORDERS C 5 0 + 38 SKIN AND SUBCUTANEOUS TISSUE DISORDERS C 5 0 + 39 SKIN AND SUBCUTANEOUS TISSUE DISORDERS C 5 0 + 40 SKIN AND SUBCUTANEOUS TISSUE DISORDERS C 5 0 + 41 SKIN AND SUBCUTANEOUS TISSUE DISORDERS C 5 0 + 42 SKIN AND SUBCUTANEOUS TISSUE DISORDERS C 5 0 + 43 SKIN AND SUBCUTANEOUS TISSUE DISORDERS C 5 0 + 44 SKIN AND SUBCUTANEOUS TISSUE DISORDERS C 5 0 + 45 SKIN AND SUBCUTANEOUS TISSUE DISORDERS C 5 0 + 46 SINUS BRADYCARDIA C 1 1 + 47 SINUS BRADYCARDIA C 1 1 + 48 SINUS BRADYCARDIA C 1 1 + 49 SINUS BRADYCARDIA C 1 1 + 50 SINUS BRADYCARDIA C 1 1 + 51 SINUS BRADYCARDIA C 1 1 + 52 SINUS BRADYCARDIA C 1 1 + 53 SINUS BRADYCARDIA C 1 1 + 54 SINUS BRADYCARDIA C 1 1 + 55 DIARRHOEA C 2 1 + 56 DIARRHOEA C 2 1 + 57 DIARRHOEA C 2 1 + 58 DIARRHOEA C 2 1 + 59 DIARRHOEA C 2 1 + 60 DIARRHOEA C 2 1 + 61 DIARRHOEA C 2 1 + 62 DIARRHOEA C 2 1 + 63 DIARRHOEA C 2 1 + 64 APPLICATION SITE ERYTHEMA C 3 1 + 65 APPLICATION SITE ERYTHEMA C 3 1 + 66 APPLICATION SITE ERYTHEMA C 3 1 + 67 APPLICATION SITE ERYTHEMA C 3 1 + 68 APPLICATION SITE ERYTHEMA C 3 1 + 69 APPLICATION SITE ERYTHEMA C 3 1 + 70 APPLICATION SITE ERYTHEMA C 3 1 + 71 APPLICATION SITE ERYTHEMA C 3 1 + 72 APPLICATION SITE ERYTHEMA C 3 1 + 73 APPLICATION SITE IRRITATION C 3 2 + 74 APPLICATION SITE IRRITATION C 3 2 + 75 APPLICATION SITE IRRITATION C 3 2 + 76 APPLICATION SITE IRRITATION C 3 2 + 77 APPLICATION SITE IRRITATION C 3 2 + 78 APPLICATION SITE IRRITATION C 3 2 + 79 APPLICATION SITE IRRITATION C 3 2 + 80 APPLICATION SITE IRRITATION C 3 2 + 81 APPLICATION SITE IRRITATION C 3 2 + 82 APPLICATION SITE PRURITUS C 3 3 + 83 APPLICATION SITE PRURITUS C 3 3 + 84 APPLICATION SITE PRURITUS C 3 3 + 85 APPLICATION SITE PRURITUS C 3 3 + 86 APPLICATION SITE PRURITUS C 3 3 + 87 APPLICATION SITE PRURITUS C 3 3 + 88 APPLICATION SITE PRURITUS C 3 3 + 89 APPLICATION SITE PRURITUS C 3 3 + 90 APPLICATION SITE PRURITUS C 3 3 + 91 DIZZINESS C 4 1 + 92 DIZZINESS C 4 1 + 93 DIZZINESS C 4 1 + 94 DIZZINESS C 4 1 + 95 DIZZINESS C 4 1 + 96 DIZZINESS C 4 1 + 97 DIZZINESS C 4 1 + 98 DIZZINESS C 4 1 + 99 DIZZINESS C 4 1 + 100 ERYTHEMA C 5 1 + 101 ERYTHEMA C 5 1 + 102 ERYTHEMA C 5 1 + 103 ERYTHEMA C 5 1 + 104 ERYTHEMA C 5 1 + 105 ERYTHEMA C 5 1 + 106 ERYTHEMA C 5 1 + 107 ERYTHEMA C 5 1 + 108 ERYTHEMA C 5 1 + 109 HYPERHIDROSIS C 5 2 + 110 HYPERHIDROSIS C 5 2 + 111 HYPERHIDROSIS C 5 2 + 112 HYPERHIDROSIS C 5 2 + 113 HYPERHIDROSIS C 5 2 + 114 HYPERHIDROSIS C 5 2 + 115 HYPERHIDROSIS C 5 2 + 116 HYPERHIDROSIS C 5 2 + 117 HYPERHIDROSIS C 5 2 + 118 PRURITUS C 5 3 + 119 PRURITUS C 5 3 + 120 PRURITUS C 5 3 + 121 PRURITUS C 5 3 + 122 PRURITUS C 5 3 + 123 PRURITUS C 5 3 + 124 PRURITUS C 5 3 + 125 PRURITUS C 5 3 + 126 PRURITUS C 5 3 + 127 RASH C 5 4 + 128 RASH C 5 4 + 129 RASH C 5 4 + 130 RASH C 5 4 + 131 RASH C 5 4 + 132 RASH C 5 4 + 133 RASH C 5 4 + 134 RASH C 5 4 + 135 RASH C 5 4 + 136 1 C 0 0 + 137 1 C 0 0 + 138 1 C 0 0 + 139 1 C 0 0 + 140 1 C 0 0 + 141 1 C 0 0 + 142 1 C 0 0 + 143 1 C 0 0 + 144 1 C 0 0 + 145 1 C 6 0 + 146 1 C 6 0 + 147 1 C 6 0 + 148 1 C 6 0 + 149 1 C 6 0 + 150 1 C 6 0 + 151 1 C 6 0 + 152 1 C 6 0 + 153 1 C 6 0 + SUBGRPVARX SUBGRPVARXN + + 1 "n " 1 + 2 "n " 1 + 3 "n " 1 + 4 "n " 1 + 5 "n " 1 + 6 "n " 1 + 7 "n " 1 + 8 "n " 1 + 9 "n " 1 + 10 "n " 1 + 11 "n " 1 + 12 "n " 1 + 13 "n " 1 + 14 "n " 1 + 15 "n " 1 + 16 "n " 1 + 17 "n " 1 + 18 "n " 1 + 19 "n " 1 + 20 "n " 1 + 21 "n " 1 + 22 "n " 1 + 23 "n " 1 + 24 "n " 1 + 25 "n " 1 + 26 "n " 1 + 27 "n " 1 + 28 "n " 1 + 29 "n " 1 + 30 "n " 1 + 31 "n " 1 + 32 "n " 1 + 33 "n " 1 + 34 "n " 1 + 35 "n " 1 + 36 "n " 1 + 37 "n " 1 + 38 "n " 1 + 39 "n " 1 + 40 "n " 1 + 41 "n " 1 + 42 "n " 1 + 43 "n " 1 + 44 "n " 1 + 45 "n " 1 + 46 "n " 1 + 47 "n " 1 + 48 "n " 1 + 49 "n " 1 + 50 "n " 1 + 51 "n " 1 + 52 "n " 1 + 53 "n " 1 + 54 "n " 1 + 55 "n " 1 + 56 "n " 1 + 57 "n " 1 + 58 "n " 1 + 59 "n " 1 + 60 "n " 1 + 61 "n " 1 + 62 "n " 1 + 63 "n " 1 + 64 "n " 1 + 65 "n " 1 + 66 "n " 1 + 67 "n " 1 + 68 "n " 1 + 69 "n " 1 + 70 "n " 1 + 71 "n " 1 + 72 "n " 1 + 73 "n " 1 + 74 "n " 1 + 75 "n " 1 + 76 "n " 1 + 77 "n " 1 + 78 "n " 1 + 79 "n " 1 + 80 "n " 1 + 81 "n " 1 + 82 "n " 1 + 83 "n " 1 + 84 "n " 1 + 85 "n " 1 + 86 "n " 1 + 87 "n " 1 + 88 "n " 1 + 89 "n " 1 + 90 "n " 1 + 91 "n " 1 + 92 "n " 1 + 93 "n " 1 + 94 "n " 1 + 95 "n " 1 + 96 "n " 1 + 97 "n " 1 + 98 "n " 1 + 99 "n " 1 + 100 "n " 1 + 101 "n " 1 + 102 "n " 1 + 103 "n " 1 + 104 "n " 1 + 105 "n " 1 + 106 "n " 1 + 107 "n " 1 + 108 "n " 1 + 109 "n " 1 + 110 "n " 1 + 111 "n " 1 + 112 "n " 1 + 113 "n " 1 + 114 "n " 1 + 115 "n " 1 + 116 "n " 1 + 117 "n " 1 + 118 "n " 1 + 119 "n " 1 + 120 "n " 1 + 121 "n " 1 + 122 "n " 1 + 123 "n " 1 + 124 "n " 1 + 125 "n " 1 + 126 "n " 1 + 127 "n " 1 + 128 "n " 1 + 129 "n " 1 + 130 "n " 1 + 131 "n " 1 + 132 "n " 1 + 133 "n " 1 + 134 "n " 1 + 135 "n " 1 + 136 "n " 1 + 137 "n " 1 + 138 "n " 1 + 139 "n " 1 + 140 "n " 1 + 141 "n " 1 + 142 "n " 1 + 143 "n " 1 + 144 "n " 1 + 145 "n " 1 + 146 "n " 1 + 147 "n " 1 + 148 "n " 1 + 149 "n " 1 + 150 "n " 1 + 151 "n " 1 + 152 "n " 1 + 153 "n " 1 diff --git a/tests/testthat/_snaps/ptly_utils.md b/tests/testthat/_snaps/ptly_utils.md new file mode 100644 index 0000000..3bdbc04 --- /dev/null +++ b/tests/testthat/_snaps/ptly_utils.md @@ -0,0 +1,108 @@ +# Test Case 1: plotly_legend works + + Code + fig1$x$layoutAttrs[[1]] + Output + $annotations + $annotations$text + [1] "Age Group" + + $annotations$xref + [1] "paper" + + $annotations$yref + [1] "paper" + + $annotations$x + [1] 0.3 + + $annotations$xanchor + [1] "right" + + $annotations$y + [1] -0.2 + + $annotations$yanchor + [1] "top" + + $annotations$legendtitle + [1] TRUE + + $annotations$showarrow + [1] FALSE + + $annotations$font + $annotations$font$size + [1] 10 + + + + +--- + + Code + fig1$x$layoutAttrs[[2]] + Output + $showlegend + [1] TRUE + + $legend + $legend$orientation + [1] "h" + + $legend$x + [1] 0.3 + + $legend$y + [1] -0.2 + + $legend$size + [1] 8 + + $legend$xanchor + [1] "left" + + $legend$yanchor + [1] "top" + + $legend$font + $legend$font$size + [1] 8 + + + + +--- + + Code + fig2$x$layoutAttrs[[1]] + Output + $showlegend + [1] TRUE + + $legend + $legend$orientation + [1] "v" + + $legend$x + [1] 0.2 + + $legend$y + [1] 0.1 + + $legend$size + [1] 8 + + $legend$xanchor + [1] "center" + + $legend$yanchor + [1] "top" + + $legend$font + $legend$font$size + [1] 8 + + + + diff --git a/tests/testthat/test-adae_risk_summary.R b/tests/testthat/test-adae_risk_summary.R index 90f1fdc..1feac5a 100644 --- a/tests/testthat/test-adae_risk_summary.R +++ b/tests/testthat/test-adae_risk_summary.R @@ -16,84 +16,91 @@ ae_entry <- ae_pre_process[["data"]] |> pop_fil = "Overall Population" ) -test_that("Standard Inputs work", { - output <- ae_entry |> +test_that("Test Case 1: adae_summary with standard inputs works", { + ae_risk <- ae_entry |> adae_risk_summary( - a_subset = ae_pre_process[["a_subset"]], + a_subset = ae_pre_process$a_subset, summary_by = "Patients", hterm = "AEBODSYS", lterm = "AEDECOD", ctrlgrp = "Placebo", trtgrp = "Xanomeline Low Dose", - statistics = "Risk Ratio", + statistics = "Risk Difference", alpha = 0.05, - cutoff = 5, + cutoff_where = "PCT > 5", sort_opt = "Ascending", - sort_var = "Count" + sort_var = "Count", + risklabels = tbl_risk_labels() ) - expect_s3_class(output, "data.frame") - # Check risk calculated as expected; - expect_true("Risk Ratio (CI)" %in% names(output)) - # CHeck only pair of treatments present in output: - expect_length(unique(output$TRTPAIR), 1) + expect_equal(unique(pull(ae_risk, "TRTPAIR")), "Placebo -vs- Xanomeline Low Dose") + expect_snapshot(ae_risk) + output <- ae_risk |> + tbl_processor(keepvars = "Risk Ratio (CI)") |> + tbl_display() expect_snapshot(output) - out_table <- output |> - tbl_processor(keepvars = c("Risk Ratio (CI)", "P-value")) - expect_snapshot(out_table) }) -test_that("High cutoff as expected", { - expect_message( +test_that("Test Case 2: adae_summary with summary row", { + ae_risk <- ae_entry |> adae_risk_summary( - ae_entry, - a_subset = ae_pre_process[["a_subset"]], + a_subset = ae_pre_process$a_subset, summary_by = "Patients", hterm = "AEBODSYS", lterm = "AEDECOD", ctrlgrp = "Placebo", trtgrp = "Xanomeline Low Dose", - statistics = "Risk Ratio", + statistics = "Risk Difference", alpha = 0.05, - cutoff = 20, - sort_opt = "Ascending", - sort_var = "Count" + cutoff_where = "PCT > 5", + sort_opt = "Alphabetical", + sort_var = "Count", + risklabels = tbl_risk_labels(), + sum_row = "Y", + sum_row_label = "Any AE" ) - ) + expect_equal(unique(pull(ae_risk, "TRTPAIR")), "Placebo -vs- Xanomeline Low Dose") + expect_snapshot(ae_risk) + output <- ae_risk |> + tbl_processor(keepvars = "Risk Ratio (CI)") |> + tbl_display() + expect_snapshot(output) }) -test_that("Errors Resolved correctly", { - expect_error( +test_that("Test Case 3: Check empty and errors", { + ae_risk <- data.frame() |> adae_risk_summary( - data.frame(), - a_subset = ae_pre_process[["a_subset"]], + a_subset = NA, summary_by = "Patients", hterm = "AEBODSYS", lterm = "AEDECOD", ctrlgrp = "Placebo", trtgrp = "Xanomeline Low Dose", - statistics = "Risk Ratio", + statistics = "Risk Difference", alpha = 0.05, - cutoff = 5, - sort_opt = "Ascending", - sort_var = "Count" - ), - "Input data is empty" - ) - expect_error( + cutoff_where = "PCT > 5", + sort_opt = "Alphabetical", + sort_var = "Count", + risklabels = tbl_risk_labels(), + sum_row = "Y", + sum_row_label = "Any AE" + ) + expect_equal(nrow(ae_risk), 0) + ae_risk1 <- ae_entry |> adae_risk_summary( - ae_entry, - a_subset = ae_pre_process[["a_subset"]], + a_subset = ae_pre_process$a_subset, summary_by = "Patients", hterm = "AEBODSYS", lterm = "AEDECOD", ctrlgrp = "Placebo", trtgrp = "Xanomeline Low Dose", - statistics = "Relative Risk", + statistics = "Risk Difference", alpha = 0.05, - cutoff = 5, - sort_opt = "Ascending", - sort_var = "Count" - ), - "Invalid Risk Statistics; specify any one of `Risk Ratio` or `Risk Difference`" - ) + cutoff_where = "PCT > 50", + sort_opt = "Alphabetical", + sort_var = "Count", + risklabels = tbl_risk_labels(), + sum_row = "Y", + sum_row_label = "Any AE" + ) + expect_equal(nrow(ae_risk1), 0) }) diff --git a/tests/testthat/test-adlb_r301.R b/tests/testthat/test-adlb_r301.R index 6c0a824..2321f24 100644 --- a/tests/testthat/test-adlb_r301.R +++ b/tests/testthat/test-adlb_r301.R @@ -1,138 +1,109 @@ -#' Incidence of Laboratory Test Abnormalities (Without Regard to Baseline Abnormality) -#' -#' @param datain Input dataset (`adlb`). -#' @param crit_vars Criteria variables -#' @param pctdisp Denominator to calculate percentages by. -#' Values: `"TRT", "VAR","COL", "SUBGRP", "SGRPN", "CAT", "NONE", "NO", "DPTVAR"` -#' @param a_subset Subset conditions for analysis of dependent variable. -#' @param denom_subset Subset conditions for denominator eg. `"APSBLFL == 'Y'"` -#' -#' @return `data.frame` with summary of laboratory abnormality incidence counts -#' @export -#' -#' @examples -#' data("lab_data") -#' -#' lb_entry <- lab_data$adlb |> -#' mentry( -#' subset = NA_character_, -#' byvar = "PARCAT1~PARAM", -#' subgrpvar = NA_character_, -#' trtvar = "TRTA", -#' trtsort = "TRTAN", -#' trttotalyn = "N", -#' sgtotalyn = "N", -#' add_grpmiss = "N", -#' pop_fil = "SAFFL" -#' ) -#' -#' out <- -#' lb_entry |> -#' lab_abnormality_summary( -#' crit_vars = "CRIT3~CRIT4", -#' pctdisp = "SUBGRP", -#' a_subset = NA_character_, -#' denom_subset = NA_character_ -#' ) |> -#' display_bign_head(mentry_data = lb_entry) |> -#' tbl_processor( -#' dptlabel = "" -#' ) -#' -#' out -#' -#' # `flextable` output -#' out |> -#' tbl_display( -#' bylabel = "Parameter Category~Parameter", -#' dpthead = "Primary Criteria" -#' ) -#' -lab_abnormality_summary <- function(datain, - crit_vars = "CRIT3~CRIT4", - pctdisp = "SUBGRP", - a_subset = NA_character_, - denom_subset = NA_character_) { - # Data checks and error messages - stopifnot(is.data.frame(datain) && nrow(datain) > 0) - dptvars <- toupper(str_to_vec(crit_vars)) - dptvars_fl <- glue("{dptvars}FL") - byvars <- var_start(datain, "BYVAR") - byvarsN <- glue("{byvars}N") - stopifnot("Criteria Variables/Flags not present in `datain`" = all(dptvars %in% names(datain)) || - all(dptvars_fl %in% names(datain))) - # handle denom_subset when not specified - if (is.na(denom_subset) || str_squish(denom_subset) == "") { - if ("APSBLFL" %in% names(datain)) { - message("`denom_subset` not specified, set to APSBLFL == 'Y'") - dsubset <- c("APSBLFL == 'Y'") - } else { - stop( - "`APSBLFL` not present in `datain`, please provide a valid denominator subset condition" - ) - } - } else { - dsubset <- denom_subset - } - # Pre process adlb - adlb <- datain |> - filter(!str_sub(.data[["PARAMCD"]], start = -2L) %in% c("PL", "SL")) |> - # Replace missing values numeric equivalent grouping variables with 0 - mutate(across(any_of(byvarsN), ~ replace_na(., 0))) - # Calculate lab abnormalities by Criteria Flags - seq_along(dptvars) |> - map(\(dptval) { - asubset <- glue("{dptvars_fl[dptval]} == 'Y'") - if (!is.na(a_subset) && - str_squish(a_subset) != "") { - asubset <- glue("{a_subset} & {asubset}") - } - ## add lab abnormality counts - adlb |> - count_abnormalities( - asubset, - dsubset, - toupper(byvars), - dptvars[[dptval]], - pctdisp - ) - }) |> - # combine and display lab abnormality table - bind_rows() |> - mutate(across(c("DENOMN", "CVALUE"), as.character)) |> - rename(N = DENOMN, n = CVALUE) |> - pivot_longer(c("N", "n"), names_to = "SUBGRPVARX", values_to = "CVALUE") |> - mutate(SUBGRPVARXN = 9999) -} +data("adlb") -#' Count Lab Abnormalities -#' -#' @inheritParams lab_abnormality_summary -#' -#' @return List of data frames -#' @noRd -#' -count_abnormalities <- - function(datain, - a_subset, - denom_subset, - byvars, - dptvars, - pctdisp) { - crit_df <- - datain |> - filter(.data[[dptvars]] != "") |> - group_by(across(all_of(c(byvars, dptvars)))) |> - distinct() |> - ungroup() |> - rename(DPTVAR = all_of(dptvars)) - ## summarize categorical variables on data filtered by criteria flags - crit_df |> - mcatstat( - a_subset = a_subset, - denom_subset = denom_subset, - dptvar = "DPTVAR", - pctdisp = pctdisp, - pctsyn = "N" - ) - } +lb_entry <- adlb |> + mentry( + subset = NA_character_, + byvar = "PARCAT1~PARAM", + subgrpvar = NA_character_, + trtvar = "TRTA", + trtsort = NA_character_, + trttotalyn = "N", + sgtotalyn = "N", + add_grpmiss = "N", + pop_fil = "SAFFL" + ) + +lb_entry_subgrp <- adlb |> + mentry( + subset = NA_character_, + byvar = "PARCAT1~PARAM", + subgrpvar = "RACE", + trtvar = "TRTA", + trtsort = "TRTAN", + trttotalyn = "Y", + sgtotalyn = "Y", + add_grpmiss = "Y", + pop_fil = "SAFFL" + ) + +lb_entry_subset <- adlb |> + mentry( + subset = NA_character_, + byvar = "PARCAT1~PARAM", + subgrpvar = "RACE", + trtvar = "TRTA", + trtsort = NA_character_, + trttotalyn = "N", + sgtotalyn = "N", + add_grpmiss = "N", + pop_fil = "SAFFL" + ) + +test_that("lab_abnormality_summary works as expected with different options", { + out <- lb_entry |> + lab_abnormality_summary( + crit_vars = "CRIT3~CRIT4", + pctdisp = "SUBGRP", + a_subset = NA_character_, + denom_subset = NA_character_ + ) + + out <- out |> + display_bign_head(lb_entry) |> + tbl_processor(addrowvars = NA_character_) + + out_by_subgrp <- + lb_entry_subgrp |> + lab_abnormality_summary( + crit_vars = "CRIT3~CRIT4", + pctdisp = "SUBGRP", + a_subset = NA_character_, + denom_subset = "APSBLFL == 'Y'" + ) + + out_by_subgrp <- out_by_subgrp |> + display_bign_head(mentry_data = lb_entry_subgrp) |> + tbl_processor(addrowvars = NA_character_) + + out_by_subset <- + lb_entry_subset |> + lab_abnormality_summary( + crit_vars = "CRIT3~CRIT4", + pctdisp = "SUBGRP", + a_subset = "RACE == 'WHITE'" + ) + + out_by_subset <- out_by_subset |> + display_bign_head(mentry_data = lb_entry_subset) |> + tbl_processor(addrowvars = NA_character_) + + purrr::walk(out, \(x) expect_snapshot(x)) + purrr::walk(out_by_subgrp, \(x) expect_snapshot(x)) + purrr::walk(out_by_subset, \(x) expect_snapshot(x)) +}) + +test_that("lab_abnormality_summary returns expected error messages", { + df_ <- lb_entry |> + dplyr::select(-APSBLFL) + + expect_error( + lab_abnormality_summary( + datain = df_, + crit_vars = "CRIT3~CRIT4", + pctdisp = "SUBGRP", + a_subset = NA_character_, + denom_subset = NA_character_ + ), + "`APSBLFL` not present in `datain`, please provide a valid denominator subset condition" + ) + + expect_error( + lab_abnormality_summary( + datain = df_, + crit_vars = "CRIT3~CRIT487900", + pctdisp = "SUBGRP", + a_subset = NA_character_, + denom_subset = NA_character_ + ) + ) +}) diff --git a/tests/testthat/test-adsl_r001.R b/tests/testthat/test-adsl_r001.R new file mode 100644 index 0000000..f0c5c78 --- /dev/null +++ b/tests/testthat/test-adsl_r001.R @@ -0,0 +1,173 @@ +data(adsl) + +mentry_df <- adsl |> + mentry( + subset = NA_character_, + byvar = "SEX", + trtvar = "TRT01A", + trtsort = "TRT01AN", + subgrpvar = NA_character_, + trttotalyn = "N", + add_grpmiss = "N", + sgtotalyn = "N", + pop_fil = "SAFFL" + ) + +test_that("split_var_types works", { + actual <- split_var_types(c("AGE-S", "RACE", "SEX")) + expected <- + list(num_vars = "AGE", cat_vars = c("RACE", "SEX"), all_vars = c("AGE", "RACE", "SEX")) + + actual_ <- split_var_types(c("AGE", "RACE", "SEX")) + expected_ <- + list( + num_vars = character(0), + cat_vars = c("AGE", "RACE", "SEX"), + all_vars = c("AGE", "RACE", "SEX") + ) + + expect_identical(actual, expected) + expect_identical(actual_, expected_) +}) + +test_that("adsl_summary works as expected", { + adsl_sum <- adsl_summary( + datain = mentry_df, + vars = "AGEGR1~AGE-S~RACE" + ) + + dataf <- adsl_sum |> + display_bign_head(mentry_df) |> + tbl_processor( + dptlabel = str_to_vec("Age Group~Age~Race"), + statlabel = str_to_vec("N~Range~Mean (SD)~Median~Interquartile Range") + ) + + adsl_sum_ <- adsl_summary( + datain = mentry_df, + vars = "AGEGR1~AGE~RACE" + ) + + dataf_ <- adsl_sum_ |> + display_bign_head(mentry_df) |> + tbl_processor( + statlabel = str_to_vec("N~Range~Meansd~Median~IQR"), + dptlabel = str_to_vec("Age Group~Age~Race") + ) + + expect_false(identical(dataf, dataf_)) + expect_true(nrow(dataf) < nrow(dataf_)) + expect_snapshot(print(tibble::as_tibble(dataf), n = Inf, width = Inf)) + expect_snapshot(print(tibble::as_tibble(dataf_), n = Inf, width = Inf)) +}) + +test_that("adsl_summary gives returns correct summary statistics", { + adsl_sum <- adsl_summary( + datain = mentry_df, + vars = "AGEGR1~AGE-S~RACE" + ) + + actual <- adsl_sum |> + display_bign_head(mentry_df) |> + tbl_processor( + statlabel = str_to_vec("N~Range~Meansd~Median~IQR"), + dptlabel = str_to_vec("Age Group~Age~Race") + ) + + agegr1 <- adsl |> + dplyr::filter(SAFFL == "Y") |> + dplyr::select(dplyr::all_of(c("TRT01A", "SEX", "AGEGR1"))) |> + dplyr::group_by(dplyr::across(dplyr::everything())) |> + dplyr::summarise(N = n()) |> + dplyr::filter(TRT01A == "Placebo") |> + dplyr::pull(N) + + exp_agegr1 <- actual |> + dplyr::filter(DPTVAR == "Age Group") |> + dplyr::arrange(BYVAR1, DPTVAL) |> + dplyr::mutate(dplyr::across( + dplyr::starts_with("Placebo"), + \(x) as.integer(stringr::str_squish(stringr::str_sub(x, 1, 2))) + )) |> + dplyr::pull(dplyr::starts_with("Placebo")) + + age_stat <- adsl |> + dplyr::filter(SAFFL == "Y") |> + dplyr::select(dplyr::all_of(c("TRT01A", "SEX", "AGE"))) |> + dplyr::group_by(dplyr::across(c("TRT01A", "SEX"))) |> + dplyr::summarize( + Mean = paste0(round_f(mean(.data[["AGE"]]), 2), " (", round_f(sd(.data[["AGE"]]), 2), ")") + ) |> + dplyr::arrange(.data[["SEX"]]) |> + dplyr::pull(Mean) + + exp_age_stat <- actual |> + dplyr::filter(DPTVAL == "Meansd") |> + dplyr::relocate(`Xanomeline Low Dose (N=84)`, .after = `Xanomeline High Dose (N=84)`) + + expect_identical(unique(actual[["DPTVAR"]]), c("Age Group", "Age", "Race")) + expect_identical(agegr1, exp_agegr1) + expect_identical(age_stat, unname(unlist(c(exp_age_stat[1, 7:9], exp_age_stat[2, 7:9])))) +}) + +test_that("adsl_summary works with subsets", { + adsl_sum <- mentry_df |> + adsl_summary( + vars = "AGEGR1~AGE-S~SEX~RACE", + a_subset = "AGE<65~AGE>80~NA~NA" + ) + + actual <- adsl_sum |> + display_bign_head(mentry_df) |> + tbl_processor( + statlabel = str_to_vec("N~Range~Meansd~Median~IQR"), + dptlabel = str_to_vec("Age Group~Age~Sex~Race") + ) + + adsl_sum_ <- mentry_df |> + adsl_summary( + vars = "AGEGR1~AGE-S~SEX~RACE", + denom_subset = "RACE=='WHITE'~NA~NA~NA" + ) + + actual_ <- adsl_sum_ |> + display_bign_head(mentry_df) |> + tbl_processor( + statlabel = str_to_vec("N~Range~Meansd~Median~IQR"), + dptlabel = str_to_vec("Age Group~Age~Sex~Race") + ) + + expect_snapshot(print(actual, n = Inf, width = Inf)) + expect_snapshot(print(tibble::as_tibble(actual_), n = Inf, width = Inf)) + + expect_error( + adsl_summary( + datain = mentry_df, + vars = "AGEGR1~AGE-S~SEX~RACE", + a_subset = "AGE<65~AGE>80~NA" + ), + "Number of subsets should be 1 or equal to number of corresponding variables" + ) + expect_error( + adsl_summary( + datain = mentry_df, + vars = "AGEGR1~AGE-S~SEX~RACE", + denom_subset = "AGE<65~NA" + ), + "Number of subsets should be 1 or equal to number of corresponding variables" + ) +}) + +test_that("Analysis variables not present", { + adsl_sum1 <- mentry_df |> + adsl_summary( + vars = "AGEGR1/AGEGR1N~AGE-S~SEX/SEXN~RACE/RACEN", + a_subset = "AGE<65~AGE>80~NA~NA" + ) + adsl_sum2 <- mentry_df |> + adsl_summary( + vars = "AGEGR1/AGEGR1N~AGE-S~SEX/SEXN~RACE/RACEN~AGEGR5/AGEGR5N", + a_subset = "AGE<65~AGE>80~NA~NA~NA" + ) + expect_equal(adsl_sum1, adsl_sum2) +}) diff --git a/tests/testthat/test-ae_forestplot.R b/tests/testthat/test-ae_forestplot.R new file mode 100644 index 0000000..58ce7b1 --- /dev/null +++ b/tests/testthat/test-ae_forestplot.R @@ -0,0 +1,178 @@ +data("ae_risk") +data("ae_pre_process") +ae_entry <- mentry( + datain = ae_pre_process$data, + subset = NA, + byvar = "AEBODSYS", + trtvar = "TRTA", + trtsort = "TRTAN", + subgrpvar = NA, + trttotalyn = "N", + add_grpmiss = "N", + sgtotalyn = "N", + pop_fil = "SAFFL" +) +ae_risk_forest <- risk_stat( + datain = ae_entry, + a_subset = ae_pre_process$a_subset, + summary_by = "Patients", + eventvar = "AEDECOD", + ctrlgrp = "Placebo", + trtgrp = "Xanomeline High Dose", + statistics = "Risk Ratio", + alpha = 0.05, + cutoff_where = "PCT > 5", + sort_opt = "Ascending", + sort_var = "Count" +) |> + plot_display_bign(ae_entry) +series_opts <- ae_risk_forest |> + plot_aes_opts(series_color = c("black", "royalblue2")) +# AE Forest Plot + +axis_opts1 <- plot_axis_opts( + xaxis_label = "Risk Ratio", + xopts = list(labelsize = 8, ticksize = 8) +) +axis_opts <- append(axis_opts1, list(xpos = "top")) +ae_risk1_forest <- risk_stat( + datain = ae_entry, + a_subset = ae_pre_process$a_subset, + summary_by = "Patients", + eventvar = "AEDECOD", + ctrlgrp = "Placebo", + trtgrp = "Xanomeline High Dose~~Xanomeline Low Dose", + statistics = "Risk Ratio", + alpha = 0.05, + cutoff_where = "PCT > 5", + sort_opt = "Ascending", + sort_var = "Count", + g_sort_by_ht = "Y" +) |> + plot_display_bign(ae_entry) +series_opts1 <- plot_aes_opts(ae_risk1_forest, series_color = "black~black~black") + +forest_dat <- ae_risk_forest |> + filter(!is.nan(.data[["RISK"]]), !is.infinite(.data[["RISK"]])) +test_that("Test Case 1: Standard error checks", { + expect_error( + ae_forest_plot( + datain = ae_risk[0, ], + series_opts = list( + color = g_seriescol(ae_risk, c("black", "royalblue2"), "TRTVAR"), + shape = g_seriessym(ae_risk, NA, "TRTVAR"), + size = rep(1, 2) + ), + axis_opts = plot_axis_opts( + xaxis_label = "Risk Ratio", + xopts = list(labelsize = 8) + ) + ), + "Input ae_forest_plot data is empty" + ) + expect_error( + ae_forest_plot( + datain = ae_risk, + series_opts = list( + color = g_seriescol(ae_risk, c("black", "royalblue2"), "TRTVAR"), + shape = g_seriessym(ae_risk, NA, "TRTVAR"), + size = rep(1, 2) + ), + rel_widths = c(0.38, 0.27), + axis_opts = plot_axis_opts( + xaxis_label = "Risk Ratio", + xopts = list(labelsize = 8) + ), + text_size = 2.4, + term_label = "Reported Term for the Adverse Event", + risk_ref = 1, + highlight_sig = "N" + ), + "rel_widths should be equal to the number of plot columns" + ) +}) +test_that("Test Case 2: Standard Inputs 1", { + forest1 <- ae_forest_plot( + datain = ae_risk_forest, + series_opts = series_opts, + trtpair_color = c("#F8766D", "#00BFC4"), + axis_opts = axis_opts, + term_label = "Reported Term for the Adverse Event", + highlight_sig = "Y", + rel_widths = c(0.5, 0.35, 0.15), + ht_dispyn = "N", + pvalue_dispyn = "Y", + terms_perpg = NULL + ) + expect_type(forest1, "list") + expect_length(forest1, 1) + expect_true("ggplot" %in% class(forest1[[1]])) + expect_length(forest1[[1]][["layers"]], 3) + purrr::walk(forest1[[1]][["layers"]], \(x) { + expect_snapshot(x[["geom_params"]][-1]) + }) +}) + +test_that("Test Case 3: Standard Inputs 2", { + forest2 <- ae_forest_plot( + datain = ae_risk1_forest, + series_opts = series_opts1, + axis_opts = axis_opts1, + term_label = "", + risk_ref = 1, + pairwise = "Y", + highlight_sig = "N", + rel_widths = c(0.25, 0.45, 0.3), + ht_dispyn = "Y", + pvalue_dispyn = "N", + terms_perpg = NULL + ) + expect_type(forest2, "list") + expect_length(forest2, 2) + expect_true("ggplot" %in% class(forest2[[1]])) + expect_length(forest2[[2]][["layers"]], 3) +}) +test_that("Test Case 4: Page splitting", { + forest3 <- ae_forest_plot( + datain = ae_risk_forest, + series_opts = series_opts, + trtpair_color = c("#F8766D", "#00BFC4"), + axis_opts = axis_opts, + term_label = "Reported Term for the Adverse Event", + highlight_sig = "Y", + rel_widths = c(0.5, 0.35, 0.15), + ht_dispyn = "N", + pvalue_dispyn = "Y", + terms_perpg = 8 + ) + expect_true(length(forest3) > 1) + purrr::walk( + forest3, + \(x) expect_true("ggplot" %in% class(x)) + ) + expect_length(forest3[[3]][["layers"]], 3) +}) + +# Create scatter plot to test sig points +splot <- forest_plot_scatter( + datain = ae_risk_forest, + xvar = "PCT", + yvar = "DPTVAL", + series_var = "TRTVAR", + series_opts = series_opts, + hovervar = "HOVER_PCT", + xaxis_pos = "top", + legend_opts = list(pos = "bottom", dir = "horizontal"), + axis_opts = list(xsize = 8, xtsize = 6, xaxis_label = "Percentage") +) +test_that("Test Case: Significant Points", { + actual <- ae_forest_hlt_sig( + plotin = splot, + datain = forest_dat, + pvalue_sig = 0.05, + pts_size = 1.5 + ) + expect_length(actual[["layers"]], 3) + expect_true(length(actual[["layers"]]) > length(splot[["layers"]])) + expect_equal(actual[["labels"]][["fill"]], "EFFECT") +}) diff --git a/tests/testthat/test-ae_pre_processor.R b/tests/testthat/test-ae_pre_processor.R index b1f71aa..78417f3 100644 --- a/tests/testthat/test-ae_pre_processor.R +++ b/tests/testthat/test-ae_pre_processor.R @@ -1,172 +1,140 @@ # testcase 1: data(adae) data(FMQ_Consolidated_List) -test_that("Test Case 1: With standard inputs", { +adae1 <- adae |> + dplyr::mutate(ASEVN = dplyr::recode(.data[["AESEV"]], "MILD" = 1, "MODERATE" = 2, "SEVERE" = 3)) +test_that("Test Case 1: With standard arguments", { + adaetest <- adae + adaetest$AEDECOD[1] <- NA actual <- ae_pre_processor( - datain = adae, - aeSubset = "AOCCPFL=='Y'", - aeDenomSubset = "!is.na(ASTDT)", + datain = adaetest, ae_filter = "Any Event", - aeObsPeriod = "Overall Duration", - aeObsResidual = 0, - trtvar = "TRTA", - trtsort = "TRTAN", - pop_fil = "SAFFL", - fmq_data = FMQ_Consolidated_List, - aeEventVar = "AEDECOD", - aeByVar = "AEBODSYS", - aeSubGrpVar = NA, - aeBigN = "N", - aeGrpVarMiss = "N", - aeTrtTot = "N", - aeSubGrpTot = "N" + obs_residual = NA, + fmq_data = NA ) - date_formats <- c("%d%b%Y", "%Y-%m-%d") - - expected <- adae %>% - mutate( - AESTDT = as.Date(ASTDT, tryFormats = date_formats, optional = FALSE), - AEENDT = as.Date(AENDT, tryFormats = date_formats, optional = FALSE), - RFSTDTC = as.Date(TRTSDT, tryFormats = date_formats, optional = FALSE), - RFENDTC = as.Date(TRTEDT, tryFormats = date_formats, optional = FALSE) - ) %>% - tidyr::drop_na(RFSTDTC) %>% + date_formats <- c("%d%b%Y", "%Y-%m-%d", "%m/%d/%Y", "%d/%m/%Y") + expected <- adaetest |> mutate( - AEDECOD = if_else(!is.na(AESTDT) & is.na(AEDECOD), "Not yet coded", AEDECOD), - AESTDT = if_else(is.na(AESTDT) & !is.na(AEDECOD), RFSTDTC, AESTDT), - AESEV = toupper(AESEV) - ) - - mdsin <- mentry( - datain = expected, - ui_aSubset = "AOCCPFL=='Y'", - ui_dSubset = "!is.na(ASTDT)", - ui_byvar = "AEBODSYS", - ui_subgrpvar = NA, - ui_trtvar = "TRTA", - ui_trtsort = "TRTAN", - ui_trttotalyn = "N", - ui_sgtotalyn = "N", - ui_bign = "N", - ui_addGrpMiss = "N", - ui_pop_fil = "SAFFL" - ) - expect_named(actual, c("dsin", "dout", "bigN")) - expect_equal(actual$dsin, mdsin$dsin) - expect_equal(actual$dout, mdsin$dout) - expect_true(is.na(actual$bigN)) + ASTDT = as.Date(.data[["ASTDT"]], tryFormats = date_formats, optional = FALSE), + AENDT = as.Date(.data[["AENDT"]], tryFormats = date_formats, optional = FALSE), + TRTSDT = as.Date(.data[["TRTSDT"]], tryFormats = date_formats, optional = FALSE), + TRTEDT = as.Date(.data[["TRTEDT"]], tryFormats = date_formats, optional = FALSE) + ) |> + tidyr::drop_na(all_of("TRTSDT")) |> + mutate(AEDECOD = if_else(is.na(.data[["AEDECOD"]]) & !is.na(.data[["ASTDT"]]), + "Not Yet Coded", .data[["AEDECOD"]] + )) + expect_named(actual, c("data", "a_subset")) + expect_equal(actual$data, expected) + expect_true(is.na(actual$a_subset)) }) -# testcase 2: -test_that("Test Case 2: Varying inputs", { +test_that("Test Case 2: Check filtering", { actual <- ae_pre_processor( datain = adae, - aeSubset = "AOCCPFL=='Y'", - aeDenomSubset = "!is.na(ASTDT)", - ae_filter = "Treatment emergent", - aeObsPeriod = "Other", - aeObsResidual = 5, - trtvar = "TRTA", - trtsort = "TRTAN", - pop_fil = "SAFFL", - fmq_data = FMQ_Consolidated_List, - aeEventVar = "AEDECOD", - aeByVar = "AEBODSYS", - aeSubGrpVar = NA, - aeBigN = "N", - aeGrpVarMiss = "N", - aeTrtTot = "Y", - aeSubGrpTot = "N" + ae_filter = "Serious", + obs_residual = 5, + fmq_data = NA + ) + date_formats <- c("%d%b%Y", "%Y-%m-%d", "%m/%d/%Y", "%d/%m/%Y") + expected <- adae |> + mutate( + ASTDT = as.Date(.data[["ASTDT"]], tryFormats = date_formats, optional = FALSE), + AENDT = as.Date(.data[["AENDT"]], tryFormats = date_formats, optional = FALSE), + TRTSDT = as.Date(.data[["TRTSDT"]], tryFormats = date_formats, optional = FALSE), + TRTEDT = as.Date(.data[["TRTEDT"]], tryFormats = date_formats, optional = FALSE) + ) |> + tidyr::drop_na(all_of("TRTSDT")) |> + filter( + .data[["AESER"]] == "Y", .data[["ASTDT"]] > .data[["TRTSDT"]], + .data[["ASTDT"]] < (.data[["TRTEDT"]] + 5) + ) + expect_named(actual, c("data", "a_subset")) + expect_equal(actual$data, expected) + expect_equal( + actual$a_subset, + "AESER == 'Y' & (ASTDT > TRTSDT) & (ASTDT < (TRTEDT + 5))" ) - expect_named(actual, c("dsin", "dout", "bigN")) - expect_true(is.na(actual$bigN)) - # AE filter applied: - expect_equal(unique(actual$dsin$TRTEMFL), "Y") - # Analysis Subset applied: - expect_equal(unique(actual$dsin$AOCCPFL), "Y") - # Denom Subset: - expect_false(any(is.na(actual$dout$ASTDT))) - # Observation Period: - expect_true(all(actual$dsin$AESTDT > actual$dsin$RFSTDTC)) - expect_true(all(actual$dsin$AESTDT < (actual$dsin$RFENDTC + 5))) - # All required variables created in if(): - expect_true(all(c("AESTDT", "AEENDT", "RFSTDTC", "RFENDTC", "AESEV") %in% names(actual$dout))) - # There are no non-coded terms in default adae: - expect_false("Not yet coded" %in% unique(actual$dsin$AEDECOD)) - # Treatment - expect_is(actual$dsin$TRTVAR, "factor") - # Total Treatment: - expect_true("Total" %in% unique(actual$dsin$TRTVAR)) - # FMQ is not given as eventvar: - expect_false("FMQ_NAM" %in% names(actual$dout)) }) # test case 3: test_that("Test Case 3: FMQ created from Consolidated List", { actual <- ae_pre_processor( datain = adae, - aeSubset = "USUBJID != ''", - aeDenomSubset = "!is.na(ASTDT)", - ae_filter = c("Mild", "Recovered/Resolved"), - aeObsPeriod = "Overall Duration", - trtvar = "TRTA", - trtsort = "TRTAN", - pop_fil = "SAFFL", - fmq_data = FMQ_Consolidated_List, - aeEventVar = "AEDECOD", - aeByVar = "FMQ_NAM", - aeSubGrpVar = NA, - aeBigN = "Y", - aeGrpVarMiss = "N", - aeTrtTot = "N", - aeSubGrpTot = "N" + ae_filter = NA, + obs_residual = NA, + fmq_data = FMQ_Consolidated_List ) - expect_named(actual, c("dsin", "dout", "bigN")) - expect_is(actual$bigN, "data.frame") - # AE filter applied: - expect_equal(toupper(unique(actual$dsin$AESEV)), "MILD") - expect_equal(toupper(unique(actual$dsin$AEOUT)), "RECOVERED/RESOLVED") # FMQ is given as BYVAR - expect_true("FMQ_NAM" %in% names(actual$dout)) - # Using PT = "Rash" as example: - Fmq_rash <- FMQ_Consolidated_List %>% - filter(PT == "Rash") %>% + expect_true("FMQ_NAM" %in% names(actual$data)) + # Using PT = "Anxiety" as example: + Fmq_Anxiety <- FMQ_Consolidated_List |> + filter(PT == "Anxiety") |> mutate(FMQ_NAM = paste0(FMQ, "/", FMQCAT)) - expectedfmq <- paste(unique(Fmq_rash$FMQ_NAM), collapse = "~~") - actualfmq <- actual$dout %>% - filter(AEDECOD == "RASH") %>% - distinct(FMQ_NAM) %>% + expectedfmq <- paste(unique(Fmq_Anxiety$FMQ_NAM), collapse = "~~") + actualfmq <- actual$data |> + filter(AEDECOD == "ANXIETY") |> + distinct(FMQ_NAM) |> pull() expect_equal(actualfmq, expectedfmq) }) -# test case 4 -test_that("Test Case 4: Dates Converted As Expected:", { + +# test case 3: +test_that("Test Case 4: Filters executed correctly", { + expect_error( + ae_pre_processor( + datain = adae, + ae_filter = "GRADE 1", + obs_residual = NA, + fmq_data = FMQ_Consolidated_List + ), "ATOXGR not found in data. Cannot apply ae_filter" + ) actual <- ae_pre_processor( datain = adae, - aeSubset = "USUBJID != ''", - aeDenomSubset = "!is.na(ASTDT)", ae_filter = "Serious", - aeObsPeriod = "Other", - aeObsResidual = 5, - trtvar = "TRTA", - trtsort = "TRTAN", - pop_fil = "SAFFL", - fmq_data = FMQ_Consolidated_List, - aeEventVar = "AEDECOD", - aeByVar = "AEBODSYS", - aeSubGrpVar = NA, - aeBigN = "N", - aeGrpVarMiss = "N", - aeTrtTot = "Y", - aeSubGrpTot = "N" + subset = "AGE > 80", + fmq_data = NA ) - expect_named(actual, c("dsin", "dout", "bigN")) - # AE filter applied: - expect_equal(unique(actual$dsin$AESER), "Y") - # expected class to be 'Date' - expect_is(actual$dsin$AESTDT, "Date") - expect_is(actual$dsin$AEENDT, "Date") - expect_is(actual$dsin$RFSTDTC, "Date") - expect_is(actual$dsin$RFENDTC, "Date") + expect_named(actual, c("data", "a_subset")) + expect_equal(nrow(actual$data), 0) + expect_equal(actual$a_subset, "AESER == 'Y' & AGE > 80") + actual1 <- ae_pre_processor( + datain = data.frame() + ) + expect_equal(actual1, list(data = data.frame(), a_subset = NA_character_)) +}) + +# test case 5: +test_that("Test Case 5: Max Sev/Toxicity", { + actual <- ae_pre_processor( + datain = adae1, + subset = "TRTEMFL == 'Y' & SITEID == '703'", + max_sevctc = "SEV", + sev_ctcvar = "ASEVN", + hterm = "AEBODSYS", + lterm = "AEDECOD", + pt_total = "Y" + ) + expected <- adae1 |> + filter(TRTEMFL == "Y", SITEID == "703") |> + mutate( + ASTDT = as.Date(.data[["ASTDT"]], tryFormats = date_formats, optional = FALSE), + AENDT = as.Date(.data[["AENDT"]], tryFormats = date_formats, optional = FALSE), + TRTSDT = as.Date(.data[["TRTSDT"]], tryFormats = date_formats, optional = FALSE), + TRTEDT = as.Date(.data[["TRTEDT"]], tryFormats = date_formats, optional = FALSE) + ) |> + tidyr::drop_na(all_of("TRTSDT")) |> + group_by(across(all_of(c("TRTA", "USUBJID", "AEBODSYS", "AEDECOD")))) |> + mutate(MAX_SEVCTC = ifelse(.data[["ASEVN"]] == max(.data[["ASEVN"]], na.rm = TRUE), 1, 0)) |> + filter(.data[["MAX_SEVCTC"]] == 1) |> + group_by(across(any_of(c("TRTA", "USUBJID")))) |> + mutate(ANY = ifelse(.data[["ASEVN"]] == max(.data[["ASEVN"]], na.rm = TRUE), 1, 0)) |> + group_by(across(any_of(c("TRTA", "USUBJID", "AEBODSYS")))) |> + mutate(HT_FL = ifelse(.data[["ASEVN"]] == max(.data[["ASEVN"]], na.rm = TRUE), 1, 0)) |> + group_by(across(any_of(c("TRTA", "AEDECOD", "USUBJID")))) |> + mutate(PT_CNT = ifelse(.data[["ASEVN"]] == max(.data[["ASEVN"]], na.rm = TRUE), 1, 0)) |> + ungroup() + expect_equal(actual$data, expected) + expect_equal(actual$a_subset, "TRTEMFL == 'Y' & SITEID == '703' & MAX_SEVCTC == 1") }) diff --git a/tests/testthat/test-ae_volcano_plot.R b/tests/testthat/test-ae_volcano_plot.R index 45b389b..908f588 100644 --- a/tests/testthat/test-ae_volcano_plot.R +++ b/tests/testthat/test-ae_volcano_plot.R @@ -1,4 +1,37 @@ -data("ae_risk") +data("adae") +ae_pre <- ae_pre_processor( + datain = adae, + obs_residual = 0, + fmq_data = NA, + subset = "TRTEMFL == 'Y'" +) + +ae_entry <- mentry( + datain = ae_pre$data, + subset = NA, + byvar = "AEBODSYS", + trtvar = "TRTA", + trtsort = "TRTAN", + subgrpvar = NA, + trttotalyn = "N", + add_grpmiss = "N", + sgtotalyn = "N", + pop_fil = "SAFFL" +) + +ae_risk <- risk_stat( + datain = ae_entry, + a_subset = ae_pre$a_subset, + summary_by = "Patients", + eventvar = "AEDECOD", + ctrlgrp = "Placebo", + trtgrp = "Xanomeline High Dose", + statistics = "Risk Ratio", + alpha = 0.05, + cutoff_where = "PCT > 5", + sort_opt = "Ascending", + sort_var = "Count" +) vaxis_opts <- ae_volcano_opts( datain = ae_risk, statistic = "Risk Ratio", @@ -21,7 +54,7 @@ test_that("Test 1: Volcano plot Options works", { c("xaxis_label", "yaxis_label", "ylinearopts", "yaxis_scale", "xref") ) expected <- list( - xaxis_label = "<--- Favors Control (N=69) ---- Favors Exposure (N=79) --->\nRisk Ratio", + xaxis_label = "<--- Favors Control (N=62) ---- Favors Exposure (N=69) --->\nRisk Ratio", yaxis_label = "-log10 p-value", ylinearopts = list(breaks = as.numeric(paste0("1e-", 0:20)), labels = as.character(0:20)), yaxis_scale = reverselog_trans(10), @@ -33,7 +66,7 @@ test_that("Test 1: Volcano plot Options works", { pvalue_trans = "none" ) expected2 <- list( - xaxis_label = "<--- Favors Control (N=69) ---- Favors Exposure (N=79) --->\nRisk Ratio", + xaxis_label = "<--- Favors Control (N=62) ---- Favors Exposure (N=69) --->\nRisk Ratio", yaxis_label = "p-value", ylinearopts = list( breaks = c(0.05, 0, rep(1, 10) / 10^(9:0)), diff --git a/tests/testthat/test-bar_plot.R b/tests/testthat/test-bar_plot.R new file mode 100644 index 0000000..c02e944 --- /dev/null +++ b/tests/testthat/test-bar_plot.R @@ -0,0 +1,78 @@ +data(adsl) +adsl_entry <- mentry( + datain = adsl, + subset = "EFFFL=='Y'", + byvar = "RACE", + trtvar = "TRT01A", + trtsort = "TRT01AN", + pop_fil = NA +) + +adsl_sum <- msumstat( + datain = adsl_entry, + dptvar = "AGE", + statvar = "mean", + figyn = "Y" +)[["gsum"]] |> + mutate( + XVAR = BYVAR1, + YVAR = as.numeric(mean) + ) + +test_that("Test Case 1: bar_plot works with expected inputs", { + bar_out <- bar_plot( + datain = adsl_sum, + flip_plot = "N", + series_opts = list( + color = c("red", "gold", "cyan") + ), + axis_opts = plot_axis_opts(), + legend_opts = list( + label = "", pos = "bottom", + dir = "horizontal" + ), + series_var = "TRTVAR", + series_labelvar = "TRTVAR", + bar_pos = "dodged", + griddisplay = "N", + plot_title = NULL + ) + + legendgroups <- unique(bar_out[["data"]][["TRTVAR"]]) + + expect_type(bar_out, "list") + expect_true(is.ggplot(bar_out)) + expect_equal(legendgroups, unique(bar_out[["data"]][["TRTVAR"]])) + purrr::walk( + c("mapping", "labels"), + \(x) expect_snapshot(bar_out[[x]]) + ) +}) + +test_that("Test Case 2: bar_plot works with modified inputs", { + bar_out <- bar_plot( + datain = adsl_sum, + flip_plot = "Y", + series_opts = list( + color = c("red", "gold", "cyan"), + contrast = c("black", "grey", "pink") + ), + axis_opts = plot_axis_opts(), + legend_opts = list( + label = "", pos = "bottom", + dir = "horizontal" + ), + series_var = "TRTVAR", + series_labelvar = "TRTVAR", + bar_pos = "stacked", + griddisplay = "N", + plot_title = NULL + ) + + legendgroups <- unique(bar_out[["data"]][["TRTVAR"]]) + + expect_type(bar_out, "list") + expect_equal(legendgroups, unique(bar_out[["data"]][["TRTVAR"]])) + expect_true(nrow(bar_out$data) > 0) + expect_true(length(bar_out) > 0) +}) diff --git a/tests/testthat/test-box_plot.R b/tests/testthat/test-box_plot.R new file mode 100644 index 0000000..1968d2f --- /dev/null +++ b/tests/testthat/test-box_plot.R @@ -0,0 +1,115 @@ +adsl_entry <- mentry( + datain = adsl, + subset = "EFFFL=='Y'", + byvar = "RACE", + trtvar = "TRT01A", + trtsort = "TRT01AN", + pop_fil = NA +) + +adsl_sum <- msumstat( + datain = adsl_entry, + dptvar = "AGE", + statvar = c( + "mean", "median", "q25", "q75", "whiskerlow", + "whiskerup", "outliers" + ) +) + +adsl_sum$gsum$XVAR <- fct_reorder(adsl_sum$gsum$BYVAR1, adsl_sum$gsum$BYVAR1N) +fig_col <- box_plot( + datain = adsl_sum$gsum, + axis_opts = plot_axis_opts(xaxis_label = "Race", yaxis_label = "Age"), + legend_opts = list( + label = "Treatment", pos = "bottom", + dir = "horizontal" + ), + series_opts = list( + color = c("red", "gold", "cyan"), shape = c(16, 17, 15), + size = c(1, 1, 1) + ) +) +fig_fill <- box_plot( + datain = adsl_sum$gsum, + axis_opts = plot_axis_opts(xaxis_label = "Race", yaxis_label = "Age"), + legend_opts = list( + label = "Treatment", pos = "bottom", + dir = "horizontal" + ), + series_opts = list( + color = c("red", "gold", "cyan"), shape = c(16, 17, 15), + size = c(1, 1, 1) + ), + boxfill = "Y", + griddisplay = "Y" +) + +# Whiskers varied to min and max +adsl_sum2 <- msumstat( + datain = adsl_entry, + dptvar = "AGE", + statvar = c( + "mean", "median", "q25", "q75", "min", + "max" + ) +) +adsl_sum2$gsum$XVAR <- fct_reorder(adsl_sum2$gsum$BYVAR1, adsl_sum2$gsum$BYVAR1N) +fig_fill2 <- box_plot( + datain = adsl_sum2$gsum, + axis_opts = plot_axis_opts(xaxis_label = "Race", yaxis_label = "Age"), + legend_opts = list( + label = "Treatment", pos = "bottom", + dir = "horizontal" + ), + series_opts = list( + color = c("red", "gold", "cyan"), shape = c(16, 17, 15), + size = c(1, 1, 1) + ), + boxfill = "Y" +) + +# Tests: +test_that("Standard box plot outputs", { + expect_true(is.ggplot(fig_col)) + expectdata <- adsl_sum$gsum |> + mutate(across(all_of(c( + "mean", "median", "q25", "q75", "whiskerlow", + "whiskerup" + )), as.numeric)) + expect_equal(fig_col$data, expectdata) + purrr::walk( + list(fig_col, fig_fill, fig_fill2), + \(p) purrr::walk(c("mapping", "labels"), \(x) expect_snapshot(p[[x]])) + ) +}) + +test_that("Errors resolved", { + expect_error( + box_plot( + datain = adsl_sum$gsum |> select(-all_of(c("median", "q25"))), + axis_opts = plot_axis_opts(xaxis_label = "Race", yaxis_label = "Age"), + legend_opts = list( + label = "Treatment", pos = "bottom", + dir = "horizontal" + ), + series_opts = list( + color = c("red", "gold", "cyan"), shape = c(16, 17, 15), + size = c(1, 1, 1) + ) + ), "Expected statistics not found" + ) + expect_error( + box_plot( + datain = data.frame(), + axis_opts = plot_axis_opts(xaxis_label = "Race", yaxis_label = "Age"), + legend_opts = list( + label = "Treatment", pos = "bottom", + dir = "horizontal" + ), + series_opts = list( + color = c("red", "gold", "cyan"), shape = c(16, 17, 15), + size = c(1, 1, 1) + ) + ) + ) +}) diff --git a/tests/testthat/test-dataset_merge.R b/tests/testthat/test-dataset_merge.R index 020a258..eae21a2 100644 --- a/tests/testthat/test-dataset_merge.R +++ b/tests/testthat/test-dataset_merge.R @@ -4,7 +4,7 @@ df1 <- iris |> df2 <- iris |> dplyr::select(Species, dplyr::ends_with("Width")) - +options(warn = 1) test_that("dataset_merge works", { expected <- dplyr::left_join(df1, df2, by = "Species") actual <- dataset_merge(df1, df2, byvars = "Species") @@ -51,4 +51,23 @@ test_that("dataset_merge returns expected errors", { ), "All subsets cannot be `NA`, use `subset = NULL` instead" ) + expect_error( + dataset_merge( + df1, + df2, + byvars = "Species", + subset = NULL, + type = "outer" + ), + "Type should be one of left, right, inner, full" + ) + expect_warning( + dataset_merge( + df1, + df2, + byvars = "Species", + subset = list("Species != 'versicolor'", NA_character_), + type = "full" + ) + ) }) diff --git a/tests/testthat/test-edish_plot.R b/tests/testthat/test-edish_plot.R index 6c0a824..1001910 100644 --- a/tests/testthat/test-edish_plot.R +++ b/tests/testthat/test-edish_plot.R @@ -1,138 +1,103 @@ -#' Incidence of Laboratory Test Abnormalities (Without Regard to Baseline Abnormality) -#' -#' @param datain Input dataset (`adlb`). -#' @param crit_vars Criteria variables -#' @param pctdisp Denominator to calculate percentages by. -#' Values: `"TRT", "VAR","COL", "SUBGRP", "SGRPN", "CAT", "NONE", "NO", "DPTVAR"` -#' @param a_subset Subset conditions for analysis of dependent variable. -#' @param denom_subset Subset conditions for denominator eg. `"APSBLFL == 'Y'"` -#' -#' @return `data.frame` with summary of laboratory abnormality incidence counts -#' @export -#' -#' @examples -#' data("lab_data") -#' -#' lb_entry <- lab_data$adlb |> -#' mentry( -#' subset = NA_character_, -#' byvar = "PARCAT1~PARAM", -#' subgrpvar = NA_character_, -#' trtvar = "TRTA", -#' trtsort = "TRTAN", -#' trttotalyn = "N", -#' sgtotalyn = "N", -#' add_grpmiss = "N", -#' pop_fil = "SAFFL" -#' ) -#' -#' out <- -#' lb_entry |> -#' lab_abnormality_summary( -#' crit_vars = "CRIT3~CRIT4", -#' pctdisp = "SUBGRP", -#' a_subset = NA_character_, -#' denom_subset = NA_character_ -#' ) |> -#' display_bign_head(mentry_data = lb_entry) |> -#' tbl_processor( -#' dptlabel = "" -#' ) -#' -#' out -#' -#' # `flextable` output -#' out |> -#' tbl_display( -#' bylabel = "Parameter Category~Parameter", -#' dpthead = "Primary Criteria" -#' ) -#' -lab_abnormality_summary <- function(datain, - crit_vars = "CRIT3~CRIT4", - pctdisp = "SUBGRP", - a_subset = NA_character_, - denom_subset = NA_character_) { - # Data checks and error messages - stopifnot(is.data.frame(datain) && nrow(datain) > 0) - dptvars <- toupper(str_to_vec(crit_vars)) - dptvars_fl <- glue("{dptvars}FL") - byvars <- var_start(datain, "BYVAR") - byvarsN <- glue("{byvars}N") - stopifnot("Criteria Variables/Flags not present in `datain`" = all(dptvars %in% names(datain)) || - all(dptvars_fl %in% names(datain))) - # handle denom_subset when not specified - if (is.na(denom_subset) || str_squish(denom_subset) == "") { - if ("APSBLFL" %in% names(datain)) { - message("`denom_subset` not specified, set to APSBLFL == 'Y'") - dsubset <- c("APSBLFL == 'Y'") - } else { - stop( - "`APSBLFL` not present in `datain`, please provide a valid denominator subset condition" - ) - } - } else { - dsubset <- denom_subset - } - # Pre process adlb - adlb <- datain |> - filter(!str_sub(.data[["PARAMCD"]], start = -2L) %in% c("PL", "SL")) |> - # Replace missing values numeric equivalent grouping variables with 0 - mutate(across(any_of(byvarsN), ~ replace_na(., 0))) - # Calculate lab abnormalities by Criteria Flags - seq_along(dptvars) |> - map(\(dptval) { - asubset <- glue("{dptvars_fl[dptval]} == 'Y'") - if (!is.na(a_subset) && - str_squish(a_subset) != "") { - asubset <- glue("{a_subset} & {asubset}") - } - ## add lab abnormality counts - adlb |> - count_abnormalities( - asubset, - dsubset, - toupper(byvars), - dptvars[[dptval]], - pctdisp - ) - }) |> - # combine and display lab abnormality table - bind_rows() |> - mutate(across(c("DENOMN", "CVALUE"), as.character)) |> - rename(N = DENOMN, n = CVALUE) |> - pivot_longer(c("N", "n"), names_to = "SUBGRPVARX", values_to = "CVALUE") |> - mutate(SUBGRPVARXN = 9999) -} +merged_data <- adsl_merge( + adsl = adsl, + dataset_add = adlb +) |> + mentry( + subset = "SAFFL == 'Y'", + trtvar = "TRT01A", + trtsort = "TRT01AN" + ) -#' Count Lab Abnormalities -#' -#' @inheritParams lab_abnormality_summary -#' -#' @return List of data frames -#' @noRd -#' -count_abnormalities <- - function(datain, - a_subset, - denom_subset, - byvars, - dptvars, - pctdisp) { - crit_df <- - datain |> - filter(.data[[dptvars]] != "") |> - group_by(across(all_of(c(byvars, dptvars)))) |> - distinct() |> - ungroup() |> - rename(DPTVAR = all_of(dptvars)) - ## summarize categorical variables on data filtered by criteria flags - crit_df |> - mcatstat( - a_subset = a_subset, - denom_subset = denom_subset, - dptvar = "DPTVAR", - pctdisp = pctdisp, - pctsyn = "N" - ) - } +pt_data <- process_edish_data( + datain = merged_data, + xvar = "both", + alt_paramcd = "L00030S", + ast_paramcd = "L00028S", + bili_paramcd = "L00021S" +) + +# dataset to test xvar for "ast/alt" +dt_xvar <- process_edish_data( + datain = merged_data, + xvar = "ast", + alt_paramcd = "L00030S", + ast_paramcd = "L00028S", + bili_paramcd = "L00021S" +) +series_opts <- plot_aes_opts(pt_data, + series_size = c(2, 2), + series_shape = "circle~square" +) +e_plot <- edish_plot( + datain = pt_data, + axis_opts = plot_axis_opts( + xlinearopts = list( + breaks = c(0.1, 1, 2, 10), + limits = c(0.1, 10), + labels = c("0.1", "1", "2x ULN", "10") + ), + ylinearopts = list( + breaks = c(0.1, 1, 3, 10), + limits = c(0.1, 10), + labels = c("0.1", "1", "3x ULN", "10") + ), + xaxis_label = "Peak ALT/AST (x ULN)", + yaxis_label = "Peak Total Bilirubin (x ULN)" + ), + xrefline = c("2", "gray30", "dashed"), + yrefline = c("3", "gray30", "dashed"), + quad_labels = + "Potential Hy's Law Cases~Temple's Corollary~Gilberts Syndrome or Cholestasis~Normal", + legend_opts = list( + label = "Treatment", + pos = "bottom", dir = "horizontal" + ), + series_opts = series_opts, + interactive = "N" +) + +test_that("edish data Works with standard inputs", { + actual_trt <- unique(pt_data$TRTVAR) + + expect_equal(levels(actual_trt), c("Drug1")) + expect_equal(nrow(pt_data), 57) + + expect_error(process_edish_data( + datain = merged_data, + alt_paramcd = "L00030S", + ast_paramcd = "L00028S", + bili_paramcd = "wefewf" + ), "Please provide valid PARAMCD") + + # test xvar for "ast/alt" + expect_equal(dt_xvar$XVAR, dt_xvar$ast) +}) + +test_that("edish_plot works with expected output", { + expect_type(e_plot, "list") + expect_true(nrow(e_plot$data) > 0) + expect_equal( + e_plot$labels$x, + "Peak ALT/AST (x ULN)" + ) + expect_equal( + e_plot$labels$y, + "Peak Total Bilirubin (x ULN)" + ) + expect_true(is.ggplot(e_plot)) + + + # plotly output comparison + ptly <- edish_plot( + datain = pt_data, + series_opts = series_opts, + interactive = "Y" + ) + expect_equal(class(ptly), c("plotly", "htmlwidget")) +}) + +test_that("snapshot comparison", { + purrr::walk(c("mapping", "theme", "labels"), function(x) { + expect_snapshot(e_plot[[x]]) + }) +}) diff --git a/tests/testthat/test-event_analysis.R b/tests/testthat/test-event_analysis.R index 99b7225..582d155 100644 --- a/tests/testthat/test-event_analysis.R +++ b/tests/testthat/test-event_analysis.R @@ -1,90 +1,57 @@ -data(event_df) - -test_that("Test Case 1: Event Analysis works with expected inputs", { - goutput <- event_analysis( - datain = event_df$dsin, - datain_N = event_df$dout, - hl_var = "FMQ_NAM", - hl_val = "ABDOMINAL PAIN", - hl_scope = "Narrow", - ll_var = "AEDECOD", - ll_val = "ABDOMINAL DISCOMFORT", - ll_scope = "Narrow", +data(adae) +data(FMQ_Consolidated_List) + +prep_ae <- adae |> + ae_pre_processor( + ae_filter = "ANY", + subset = "AOCCPFL == 'Y'", + obs_residual = 0, + fmq_data = FMQ_Consolidated_List + ) +prep_merge <- adsl_merge( + adsl, + adsl_subset = NA, + dataset_add = prep_ae$data +) +prep_event_analysis <- prep_merge |> + process_event_analysis( + a_subset = prep_ae$a_subset, + trtvar = "TRTA", + trtsort = "TRTAN", + trttotalyn = "Y", summary_by = "Events", - ref_line = 2 + hterm = "FMQ_NAM", + ht_val = "ABDOMINAL PAIN", + ht_scope = "Narrow", + lterm = "AEDECOD", + lt_val = "ABDOMINAL DISCOMFORT", + lt_scope = "Narrow" ) - ptly_data <- goutput[["ptly"]][["x"]][["data"]] - - legendgroups <- - unlist(purrr::compact(purrr::map( - seq_along(ptly_data), - function(x) ptly_data[[x]][["legendgroup"]] - ))) - - expect_equal(length(goutput), 6) - expect_equal(names(goutput), c("ptly", "plot", "rpt_data", "rpt_data1", "title", "footnote")) - expect_true(nrow(goutput$rpt_data) > 0) - expect_type(goutput$ptly, "list") - expect_equal(legendgroups, sort(unique(goutput[["rpt_data"]][["DPTVAL"]]))) - - expect_equal(goutput$title, "Event Analysis plot of Adverse Events") +test_that("Test Case 1: process_event_analysis works with expected inputs", { + expect_named(prep_event_analysis, c("query_df", "pt_df")) + expect_equal(unique(prep_event_analysis$pt_df$DPTVAL), "ABDOMINAL DISCOMFORT") + expect_equal(unique(prep_event_analysis$pt_df$DPTVAR), "AEDECOD") expect_equal( - goutput$footnote, - paste0( - "* N is the total number of events. \nClassifications of adverse events ", - "are based on the Medical Dictionary for Regulatory Activities (MedDRA", - " v21.1). \nFMQ classification is based on FDA FMQ consolidated list. ", - "\nDashed Horizontal line represents incidence percentage reference line. ", - "\nTotals for the No. of Participants/Events at a higher level are not ", - "necessarily the sum of those at the lower levels since a participant ", - "may report two or more. \nPT - Preferred Term ; FMQ - FDA MedDRA Queries ", - "\nEvent counts are the sum of individual occurrences within that category." - ) + unique(prep_event_analysis$query_df$DPTVAL), + c("ABDOMINAL PAIN", "ABDOMINAL DISCOMFORT", "STOMACH DISCOMFORT") ) -}) - - -test_that("Test Case 2: Event Analysis works with expected inputs", { - goutput <- event_analysis( - datain = event_df$dsin, - datain_N = event_df$dout, - hl_var = "FMQ_NAM", - hl_val = "abdominal pain", - hl_scope = "Narrow", - ll_var = "FMQ_NAM", - ll_val = "abdominal discomfort", - ll_scope = "Narrow", - summary_by = "Events", - ref_line = 2 + expect_equal(unique(prep_event_analysis$query_df$DPTVAR), "AEDECOD") + purrr::walk( + prep_event_analysis, + \(x) expect_snapshot(print(tibble::as_tibble(x), n = Inf, width = Inf)) ) +}) - ptly_data <- goutput[["ptly"]][["x"]][["data"]] - - legendgroups <- - unlist(purrr::compact(purrr::map( - seq_along(ptly_data), - function(x) ptly_data[[x]][["legendgroup"]] - ))) - - expect_equal(length(goutput), 6) - expect_equal(names(goutput), c("ptly", "plot", "rpt_data", "rpt_data1", "title", "footnote")) - expect_true(nrow(goutput$rpt_data) > 0) - expect_type(goutput$ptly, "list") - expect_equal(legendgroups, sort(unique(goutput[["rpt_data"]][["DPTVAL"]]))) - expect_identical(goutput$rpt_data, goutput$rpt_data) - expect_equal(goutput$title, "Event Analysis plot of Adverse Events") - expect_equal( - goutput$footnote, - paste0( - "* N is the total number of events. \nClassifications of adverse events ", - "are based on the Medical Dictionary for Regulatory Activities (MedDRA", - " v21.1). \nFMQ classification is based on FDA FMQ consolidated list. ", - "\nDashed Horizontal line represents incidence percentage reference line. ", - "\nTotals for the No. of Participants/Events at a higher level are not ", - "necessarily the sum of those at the lower levels since a participant ", - "may report two or more. \nPT - Preferred Term ; FMQ - FDA MedDRA Queries ", - "\nEvent counts are the sum of individual occurrences within that category." - ) +test_that("Test Case 2: event_analysis_plot works with expected inputs", { + plot <- event_analysis_plot( + datain = prep_event_analysis, + ref_line = 1, + x_tickangle = 15, + disp.proportion = "4~6", + pt_color = "royalblue3", + interactive = "Y" ) + + expect_snapshot(plot$x$data) }) diff --git a/tests/testthat/test-graph_utils.R b/tests/testthat/test-graph_utils.R new file mode 100644 index 0000000..c6cca5c --- /dev/null +++ b/tests/testthat/test-graph_utils.R @@ -0,0 +1,328 @@ +# Test graph_utils +data("adae") +data("adsl") +data("ae_pre_process") + +ae_pre <- mentry( + datain = ae_pre_process$data, + subset = NA, + byvar = "AEBODSYS", + trtvar = "TRTA", + trtsort = "TRTAN", + subgrpvar = "SEX", + trttotalyn = "N", + add_grpmiss = "N", + sgtotalyn = "N", + pop_fil = "SAFFL" +) +## reverselog_trans testing +test_that("Case 1: Transformation works with expected input", { + # Labels for adae data: + trans2 <- reverselog_trans(2) + expect_equal(trans2$name, "reverselog-2") +}) + +############################################################################# +## g_seriescol testing ## + +test_that("Case 1: Works with expected input", { + trt_cols <- g_seriescol(ae_pre, "red~cyan~forestgreen~black~pink~green", "TRTVAR") + + # Correct number of levels and colors assigned + expect_equal(unname(trt_cols), c("red", "cyan", "forestgreen")) + # Names as expected + expect_named( + trt_cols, + c("Placebo", "Xanomeline Low Dose", "Xanomeline High Dose") + ) +}) + +test_that("Case 2: Works with default", { + trt_na <- g_seriescol(ae_pre, NA, "TRTVAR") + # Default colors: + + expect_equal( + unname(trt_na), + c("firebrick2", "blue4", "forestgreen") + ) + expect_named( + trt_na, + c("Placebo", "Xanomeline Low Dose", "Xanomeline High Dose") + ) +}) + +test_that("Case 3: Works with character column", { + trt_ch <- g_seriescol(ae_pre, NA, "SEX") + # Colors: + expect_equal( + unname(trt_ch), + c("firebrick2", "blue4") + ) + expect_named( + trt_ch, + c("F", "M") + ) +}) + +############################################################################# +## g_seriessym ## + +test_that("Case 1: Works with expected input", { + trt_shp <- g_seriessym( + ae_pre, + "triangle~circle~square~asterisk", "TRTVAR" + ) + + # Correct number of levels and colors assigned + expect_equal(unname(trt_shp), c(2, 1, 0)) + # Names as expected + expect_named( + trt_shp, + c("Placebo", "Xanomeline Low Dose", "Xanomeline High Dose") + ) + + # Numeric Input: + trt_shp1 <- g_seriessym( + ae_pre, + c(1, 21, 23, 3), "TRTVAR" + ) + expect_equal(unname(trt_shp1), c(1, 21, 23)) + expect_named( + trt_shp1, + c("Placebo", "Xanomeline Low Dose", "Xanomeline High Dose") + ) +}) + +test_that("Case 2: Works with default", { + trt_nashp <- g_seriessym(ae_pre, NA, "TRTVAR") + # Default colors: + + expect_equal( + unname(trt_nashp), + c(16, 17, 15) + ) + expect_named( + trt_nashp, + c("Placebo", "Xanomeline Low Dose", "Xanomeline High Dose") + ) +}) + +test_that("Case 3: Works with character column", { + trt_chshp <- g_seriessym(ae_pre, NA, "SEX") + # Colors: + expect_equal( + unname(trt_chshp), + c(16, 17) + ) + expect_named( + trt_chshp, + c("F", "M") + ) +}) + +############################################################################# +# plot_display_bign + +# Test Data +# data without treatment +adsl_entry1 <- mentry( + datain = adsl, + subset = "EFFFL=='Y'", + byvar = NA, + trtvar = NA, + trtsort = NA, + subgrpvar = NA, + trttotalyn = "N", + add_grpmiss = "N", + sgtotalyn = "N", + pop_fil = "Overall Population" +) +datain1 <- mcatstat( + datain = adsl_entry1, + dptvar = "AGEGR1" +) +adsl_entry2 <- mentry( + datain = adsl, + subset = "EFFFL=='Y'", + byvar = NA, + trtvar = "TRT01A", + trtsort = "TRT01AN", + subgrpvar = "SEX", + trttotalyn = "N", + add_grpmiss = "N", + sgtotalyn = "N", + pop_fil = "Overall Population" +) +datain2 <- msumstat( + datain = adsl_entry2, + dptvar = "AGE", + statvar = "box" +)$gsum +# Test Cases + +test_that("Test 1:Check for count and values of the output", { + actual <- datain2 |> + plot_display_bign(adsl_entry2, bignyn = "Y") + expect_true(is.data.frame(actual)) + expected <- datain2 |> + add_bigN(adsl_entry2, grpvar = c("TRTVAR", "SUBGRPVAR1"), modvar = "TRTVAR") |> + rename("TRTTXT" = "TRTVAR_BIGN") + expect_equal(actual, expected) + # Testing with case "N" no bign + actual1 <- datain2 |> + plot_display_bign(adsl_entry2, bignyn = "N") + expected1 <- datain2 |> + mutate(TRTTXT = .data[["TRTVAR"]]) + expect_equal(actual1, expected1) +}) + +test_that("Test 2: Check bign without TRTVAR", { + actual <- plot_display_bign(datain1, adsl_entry1, bignyn = "Y") + expect_true(is.data.frame(actual)) + expect_equal(unique(actual$TRTVAR), "Total") + expect_equal( + unique(actual$TRTTXT), + paste0("Total (N=", length(unique(adsl_entry1[["USUBJID"]])), ")") + ) +}) + +test_that("Test 1: test plot_aes_opts works", { + actual <- plot_aes_opts( + datain = adsl_entry2, + series_var = "TRTVAR", + series_color = NA, + series_shape = NA, + series_size = rep(1, 3), + series_contrast = rep("black", 3) + ) + expect_type(actual, "list") + expect_equal(actual$color, g_seriescol(adsl_entry2, NA, "TRTVAR")) + expect_equal(actual$shape, g_seriessym(adsl_entry2, NA, "TRTVAR")) + expect_equal(actual$size, g_seriessym(adsl_entry2, rep(1, 3), "TRTVAR")) + expect_equal(actual$contrast, g_seriescol(adsl_entry2, rep("black", 3), "TRTVAR")) +}) + +test_that("Test 2: Check plot_aes_opts without series var", { + actual <- plot_aes_opts( + datain = adsl_entry2, + series_var = "NOVAR", + series_color = "red~green", + series_shape = "square~circle", + series_size = rep(1, 3), + series_contrast = rep("black", 3) + ) + expect_type(actual, "list") + expect_equal(actual, list(color = "red", shape = 0, size = 1, contrast = "black")) +}) +############################################################################# +# plot_axis_opts + +test_that("Test 1: Expected Inputs", { + actual <- plot_axis_opts() + expect_type(actual, "list") + expected <- list( + Ybrks = waiver(), + Ylims = NULL, + Yticks = waiver(), + Xbrks = waiver(), + Xlims = NULL, + Xticks = waiver(), + xsize = 12, + xface = "plain", + ysize = 12, + yface = "plain", + ytsize = 8, + ytface = "plain", + xtsize = 8, + xtface = "plain", + xtangle = 0, + ytangle = 0, + xaxis_scale = "identity", + yaxis_scale = "identity", + xaxis_label = "", + yaxis_label = "" + ) + expect_equal(actual, expected) + actual1 <- plot_axis_opts(xlinearopts = list(limits = c(0, 100))) + expected1 <- expected + expected1[["Xlims"]] <- c(0, 100) + expect_equal(actual1, expected1) +}) + +test_that("empty_plot works as expected", { + actual <- empty_plot() + static_label <- "No data available for these values" + exp_ptly_obj <- actual$ptly$x$data + + expect_length(actual, 2) + expect_length(actual$plot$data, 0) + expect_equal(actual[["plot"]][["layers"]][[1]][["aes_params"]][["label"]], static_label) + expect_snapshot(exp_ptly_obj) +}) + +test_that("theme_cleany works as expected", { + actual <- theme_cleany(legend_opts = list(pos = "bottom", dir = "horizontal")) + expect_true(all(class(actual) %in% c("theme", "gg"))) + expect_snapshot(actual) +}) + +test_that("theme_std works as expected", { + actual <- theme_std() + expect_true(all(class(actual) %in% c("theme", "gg"))) + expect_length(actual, 10) + expect_equal(actual$legend.position, "bottom") + expect_equal(actual$plot.title$hjust, 0.5) + actual2 <- theme_std(griddisplay = "Y") + expect_length(actual2, 12) + expect_equal(actual2$panel.grid.major.x$colour, "grey") + expect_equal(actual2$panel.grid.major.y$linewidth, 0.1) +}) + +test_that("plot_title_nsubj works as expected", { + plotdata <- ae_pre |> + msumstat( + dptvar = "AGE", + statvar = "mean", + sigdec = 2 + ) + actual1 <- plot_title_nsubj( + ae_pre, + plotdata$gsum, + "SUBGRPVAR1" + ) + expect_true(is.data.frame(actual1)) + expected1 <- ae_pre |> + group_by(across(all_of("SUBGRPVAR1"))) |> + summarise(splitN = n_distinct(.data[["USUBJID"]])) |> + (\(.) left_join(plotdata[["gsum"]], ., by = "SUBGRPVAR1"))() + expect_equal(actual1, expected1, ignore_attr = TRUE) + # No subgroup case + actual2 <- plot_title_nsubj( + ae_pre, + plotdata$gsum, + character(0) + ) + expected2 <- plotdata[["gsum"]] |> + mutate(splitN = n_distinct(ae_pre[["USUBJID"]])) + expect_equal(actual2, expected2, ignore_attr = TRUE) +}) + +test_that("tbl_to_plot works as expected", { + fig <- ggplot2::mpg |> + mutate(CYL = as.character(.data[["cyl"]])) |> + group_by(.data[["CYL"]]) |> + mutate(HWY = round(mean(.data[["hwy"]]))) |> + tbl_to_plot( + "CYL", + "manufacturer", + "HWY" + ) + expect_true("ggplot" %in% class(fig)) + purrr::walk(c("mapping", "labels"), \(x) expect_snapshot(fig[[x]])) +}) + +test_that("series_leg_lab works properly", { + iris1 <- iris |> mutate(SPNEW = fct_inorder(c(rep("A", 50), rep("B", 50), rep("C", 50)))) + expect_equal(series_leg_lab(iris1, "Species", "SPNEW"), as.factor(c("A", "B", "C"))) + expect_equal(series_leg_lab(iris1, "Species", "Species"), waiver()) +}) diff --git a/tests/testthat/test-km_plot.R b/tests/testthat/test-km_plot.R new file mode 100644 index 0000000..9616690 --- /dev/null +++ b/tests/testthat/test-km_plot.R @@ -0,0 +1,68 @@ +data("survival") + +km_df <- survival[["adsl"]] |> + surv_pre_processor( + dataset_analysis = survival[["adtte"]], + analysis_subset = "PARAMCD == 'PFS_P'" + ) + +test_that("km_plot works with default options", { + p <- km_df |> + km_plot(trt_colors = "#F8766D~#00BA38~#619CFF") + surv_df <- purrr::list_modify(p$data, survfit = NULL) + p$theme$legend.background <- NULL + fit <- purrr::list_modify(p[["data"]][["survfit"]][[1]], call = NULL, .Environment = NULL) + + expect_snapshot(print(tibble::as_tibble(surv_df), n = Inf, width = Inf)) + expect_snapshot(p$theme) + purrr::walk(p$labels, ~ expect_snapshot(.x)) + purrr::walk(fit, ~ expect_snapshot(.x)) + expect_true(p$mapping[[1]]) +}) + +test_that("km_plot works with different options", { + p <- km_df |> + km_plot( + disp_conf.int = "Y", + risktab_stats = "n.risk~n.censor", + risktab_height = 0.25, + trt_colors = "#F8766D~#00BA38~#619CFF", + axis_opts = plot_axis_opts( + xlinearopts = list(breaks = 3), + ylinearopts = list(breaks = 0.1), + xaxis_label = "Progression-Free Survival Time (Months)", + yaxis_label = "Probability of Progression Free Survival" + ) + ) + surv_df <- purrr::list_modify(p$data, survfit = NULL) + p$theme$legend.background <- NULL + fit <- p[["data"]][["survfit"]][[1]] + + expect_snapshot(print(tibble::as_tibble(surv_df), n = Inf, width = Inf)) + expect_snapshot(p$theme) + purrr::walk(p$labels, ~ expect_snapshot(.x)) + expect_snapshot(print(tibble::as_tibble(summary(fit)[["table"]]), n = Inf, width = Inf)) + expect_true(p$mapping[[1]]) +}) + +test_that("km_plot returns empty plot when `datain` is empty", { + actual <- km_df |> + dplyr::filter(USUBJID == "xxxx") |> + km_plot( + disp_conf.int = "Y", + risktab_stats = "n.risk~n.censor", + risktab_height = 0.25, + trt_colors = "#F8766D~#00BA38~#619CFF", + axis_opts = plot_axis_opts( + xlinearopts = list(breaks = 3), + ylinearopts = list(breaks = 0.1), + xaxis_label = "Progression-Free Survival Time (Months)", + yaxis_label = "Probability of Progression Free Survival" + ) + ) + + expected <- empty_plot("No data available")[["plot"]] + expect_identical(actual[["data"]], expected[["data"]]) + expect_identical(actual[["mapping"]], expected[["mapping"]]) + expect_identical(actual[["theme"]], expected[["theme"]]) +}) diff --git a/tests/testthat/test-line_plot.R b/tests/testthat/test-line_plot.R new file mode 100644 index 0000000..530d93c --- /dev/null +++ b/tests/testthat/test-line_plot.R @@ -0,0 +1,55 @@ +adsl_entry <- mentry( + datain = adsl, + subset = "EFFFL=='Y'", + byvar = "RACE", + trtvar = "TRT01A", + trtsort = "TRT01AN", + pop_fil = NA +) + +adsl_sum <- msumstat( + datain = adsl_entry, + dptvar = "AGE", + statvar = "mean" +) +adsl_sum$gsum <- adsl_sum$gsum |> + mutate( + XVAR = fct_reorder(.data[["BYVAR1"]], .data[["BYVAR1N"]]), + YVAR = as.numeric(.data[["mean"]]) + ) +fig <- line_plot( + datain = adsl_sum$gsum, + axis_opts = plot_axis_opts(xaxis_label = "Race", yaxis_label = "Mean Age"), + legend_opts = list( + label = "Treatment", pos = "bottom", + dir = "horizontal" + ), + series_opts = list( + color = c("red", "gold", "cyan") + ), + griddisplay = "Y" +) + +test_that("Standard line plot outputs", { + expect_true(is.ggplot(fig)) + expect_equal(fig$data, adsl_sum$gsum) + purrr::walk(c("mapping", "labels"), \(x) expect_snapshot(fig[[x]])) +}) + +test_that("Expect errors", { + expect_error( + line_plot( + datain = adsl_sum$gsum |> select(-all_of("XVAR")), + axis_opts = plot_axis_opts(xaxis_label = "Race", yaxis_label = "Mean Age"), + legend_opts = list( + label = "Treatment", pos = "bottom", + dir = "horizontal" + ), + series_opts = list( + color = c("red", "gold", "cyan") + ), + griddisplay = "Y" + ), + "XVAR, YVAR, series_var and series_labelvar should exist in data" + ) +}) diff --git a/tests/testthat/test-mcatstat.R b/tests/testthat/test-mcatstat.R index 69d9796..8cdd80d 100644 --- a/tests/testthat/test-mcatstat.R +++ b/tests/testthat/test-mcatstat.R @@ -31,8 +31,9 @@ ad_sum <- ad_entry |> group_by(across(all_of("TRTVAR"))) |> mutate( DPTVAL = as.character(.data[["SEX"]]), DENOMN = sum(.data[["FREQ"]]), - PCT = round_f(100 * .data[["FREQ"]] / .data[["DENOMN"]], 2), - CVALUE = paste0(.data[["FREQ"]], " (", .data[["PCT"]], "%)"), CN = "C", + PCT = 100 * .data[["FREQ"]] / .data[["DENOMN"]], + CPCT = round_f(.data[["PCT"]], 2), + CVALUE = paste0(.data[["FREQ"]], " (", .data[["CPCT"]], "%)"), CN = "C", DPTVARN = 1, XVAR = .data[["DPTVAL"]], DPTVAR = "SEX", DPTVALN = as.numeric(factor(.data[["SEX"]])) ) |> @@ -62,13 +63,14 @@ test_that("Case 1:mcatstat output with standard inputs and filters", { }) test_that("Case 2: Empty input", { - expect_error( + expect_equal( mcatstat( datain = ad_entry |> filter(USUBJID == "A"), dptvar = "SEX", pctdisp = "TRT" ), - "No data for mcatstat" + ad_entry |> filter(USUBJID == "A"), + ignore_attr = TRUE ) expect_error( mcatstat( @@ -97,26 +99,26 @@ test_that("Case 3: Unique ID and sign variation", { pctdisp = "TRT", pctsyn = "N" ) - + # All groups and order except actual count values should be equal expect_equal( - m_subj |> select(-all_of(c("DENOMN", "FREQ", "PCT", "CVALUE"))), - m_na |> select(-all_of(c("DENOMN", "FREQ", "PCT", "CVALUE"))) + m_subj |> select(-all_of(c("DENOMN", "FREQ", "PCT", "CVALUE", "CPCT"))), + m_na |> select(-all_of(c("DENOMN", "FREQ", "PCT", "CVALUE", "CPCT"))) ) - + # Counts are different expect_false(setequal(unique(m_subj$DENOMN), unique(m_na$DENOMN))) - + # Check values (for denominator, should also apply to numerator) check_na <- ae_entry |> ungroup() |> group_by(TRTVAR) |> summarise(DENOMN = n()) |> ungroup() - + expect_equal(m_na |> distinct(across(all_of(c("TRTVAR", "DENOMN")))), - check_na, - ignore_attr = TRUE + check_na, + ignore_attr = TRUE ) expect_false(any(grep("%", m_subj$CVALUE))) }) @@ -128,17 +130,17 @@ test_that("Case 4: Percentage denominator variation and denomyn", { dptvar = "SEX", pctdisp = "ABC" ), "Invalid pctdisp") - + m_none <- mcatstat( datain = ad_entry, dptvar = "SEX", pctdisp = "NONE" ) - + # No percentage columns: expect_false(any(c("PCT", "DENOMN") %in% names(m_none))) - expect_equal(m_none$FREQ, m_none$CVALUE) - + expect_equal(as.character(m_none$FREQ), m_none$CVALUE) + # Variable total as denominator m_var <- mcatstat( datain = ad_entry, @@ -160,7 +162,7 @@ test_that("Case 5: Cumulative Frequency and Total", { cum_ctyn = "Y" ) expect_equal(m_cum[, c("TRTVAR", "FREQ", "DPTVAL")], ad_cum) - + m_total <- mcatstat( datain = ad_entry, dptvar = "SEX", @@ -201,12 +203,44 @@ test_that("Case 6: Filters check", { distinct(USUBJID) |> nrow() expect_equal(unique(m_denom$DENOMN), denomvalue) - m_num1 <- mcatstat( +}) + +test_that("Case 7: Sparse check", { + m_num <- mcatstat( datain = ad_entry, - a_subset = "SEX == 'X'", + a_subset = "SEX == 'F'", uniqid = "USUBJID", dptvar = "SEX", - pctdisp = "TRT" + pctdisp = "TRT", + sparseyn = "Y" + ) + expect_equal(unique(m_num$DPTVAL), c("F", "M")) + m_by <- mcatstat( + datain = ae_entry, + a_subset = "AEBODSYS != 'CARDIAC DISORDERS'", + uniqid = "USUBJID", + dptvar = "SEX", + pctdisp = "TRT", + sparsebyvalyn = "Y" + ) + expect_true("CARDIAC DISORDERS" %in% m_by$BYVAR1) + freq <- m_by |> + filter(BYVAR1 == "CARDIAC DISORDERS") |> + pull(.data[["FREQ"]]) |> + unique() + expect_equal(freq, 0) +}) + +test_that("Analysis Subset and return zero", { + m_num <- mcatstat( + datain = ad_entry, + a_subset = "SEX == 'XC'", + uniqid = "USUBJID", + dptvar = "SEX", + pctdisp = "TRT", + return_zero = "Y" ) - expect_equal(nrow(m_num1), 0) + expect_s3_class(m_num, "data.frame") + expect_true(nrow(m_num) > 0) + expect_equal(unique(m_num$FREQ), 0) }) diff --git a/tests/testthat/test-mentry.R b/tests/testthat/test-mentry.R index aa4884d..2ae3e43 100644 --- a/tests/testthat/test-mentry.R +++ b/tests/testthat/test-mentry.R @@ -1,102 +1,103 @@ data(adsl) - +# For testing purposes +adsl[1, "SEX"] <- NA_character_ +adsl[2, "SITEGR1"] <- NA_character_ test_that("Test Case:1 mentry works with the inputs given and returns the expected items", { data_out <- mentry( datain = adsl, - ui_aSubset = "EFFFL=='Y'", - ui_dSubset = NA, - ui_byvar = "SEX", - ui_subgrpvar = "SITEGR1", - ui_trtvar = "TRT01A", - ui_trtsort = "TRT01AN", - ui_trttotalyn = "N", - ui_sgtotalyn = "N", - ui_bign = "Y", - ui_addGrpMiss = "Y", - ui_pop_fil = "SAFFL" + subset = "EFFFL=='Y'", + byvar = "SEX", + subgrpvar = "SITEGR1", + trtvar = "TRT01A", + trtsort = "TRT01AN", + trttotalyn = "N", + sgtotalyn = "N", + add_grpmiss = "Y", + pop_fil = "SAFFL" ) - # it returns a list with 3 items - expect_type(data_out, "list") - expect_equal(length(data_out), 3) - - # it returns dsin, dout, bign - expect_equal(names(data_out), c("dsin", "dout", "bign")) - + # it returns a dataframe + expect_s3_class(data_out, "data.frame") + # testing asubset filter - expect_equal(unique(data_out$dsin$EFFFL), "Y") - - # testing whether bign has 3 variables - treatment, subgroup, and bign count - expect_equal(length(data_out$bign), 3) - + expect_equal(unique(data_out$EFFFL), "Y") + # testing population filter - expect_equal(unique(data_out$dsin$SAFFL), "Y") - + expect_equal(unique(data_out$SAFFL), "Y") + # Treatment check + expect_s3_class(data_out$TRTVAR, "factor") + # testing missing logic in byvar and subgrpvar - expect_false(unique(data_out$dsin$BYVAR1 == "")) - expect_false(unique(data_out$dsin$SUBGRPVAR1 == "")) - - # after getting dout, it is filtered with asubset hence checking whether dsin has less rows than - # dout - expect_true(nrow(data_out$dsin) < nrow(data_out$dout)) + expect_equal( + unique(data_out$BYVAR1), + stringr::str_replace_na(unique(adsl$SEX), "Missing") + ) + expect_equal( + unique(data_out$SUBGRPVAR1), + stringr::str_replace_na(unique(adsl$SITEGR1), "Missing") + ) }) test_that("Test Case:2 byvar and byvarn check", { data_out <- mentry( datain = adsl, - ui_aSubset = "EFFFL=='Y'", - ui_dSubset = NA, - ui_byvar = "SEX,ETHNIC", - ui_subgrpvar = "SITEGR1,BMIBLGR1", - ui_trtvar = "TRT01A", - ui_trtsort = "TRT01AN", - ui_trttotalyn = "N", - ui_sgtotalyn = "N", - ui_bign = "Y", - ui_addGrpMiss = "N", - ui_pop_fil = "SAFFL" + subset = "EFFFL=='Y'", + byvar = "SEX~ETHNIC", + subgrpvar = "RACE/RACEN", + trtvar = "TRT01A", + trtsort = "TRT01AN", + trttotalyn = "N", + sgtotalyn = "N", + add_grpmiss = "N", + pop_fil = "SAFFL" ) byvars_check <- c( "BYVAR1", "BYVAR1N", "BYVAR2", "BYVAR2N", "SUBGRPVAR1", - "SUBGRPVAR1N", "SUBGRPVAR2", "SUBGRPVAR2N" + "SUBGRPVAR1N" ) - expect_equal(names(data_out$dsin), names(data_out$dout)) - expect_true(isTRUE(all(byvars_check %in% names(data_out$dsin)))) + expect_true(isTRUE(all(byvars_check %in% names(data_out)))) + expect_identical(unique(adsl$RACEN[!is.na(adsl$RACEN)]), unique(data_out$SUBGRPVAR1N)) + expect_identical(unique(data_out$BYVAR1), unique(adsl$SEX[!is.na(adsl$SEX)])) + expect_false("Missing" %in% unique(data_out$BYVAR1)) }) -test_that("Test Case: 3 bign conditions check when ui_bign = 'N'", { +test_that("Total and Treatment Values", { data_out <- mentry( datain = adsl, - ui_aSubset = "EFFFL=='Y'", - ui_dSubset = NA, - ui_byvar = "SEX,ETHNIC", - ui_subgrpvar = "SITEGR1,BMIBLGR1", - ui_trtvar = "TRT01A", - ui_trtsort = "TRT01AN", - ui_trttotalyn = "N", - ui_sgtotalyn = "N", - ui_bign = "N", - ui_addGrpMiss = "N", - ui_pop_fil = "SAFFL" + subset = NA_character_, + subgrpvar = "SITEGR1", + trtvar = "TRT01A", + trtsort = NA, + trttotalyn = "Y", + sgtotalyn = "Y", + add_grpmiss = "Y", + pop_fil = "SAFFL" ) - expect_equal(length(data_out), 3) - expect_equal(data_out$bign, NA) + chdata <- data_out |> + filter(.data[["TRTVAR"]] %in% + c("NOT ASSIGNED", "SCREEN FAILURE", "SCRNFAIL", "NOTRT", "NOTASSGN")) + expect_equal(nrow(chdata), 0) + exp <- data_out |> + mutate( + TRTSORT = as.numeric(factor(.data$TRTVAR)), + TRTSORT = ifelse(.data[["TRTVAR"]] == "Total", 999, .data[["TRTSORT"]]) + ) |> + select(all_of(names(data_out))) + expect_equal(data_out, exp) + expect_true("Total" %in% data_out$TRTVAR) + expect_true("Total" %in% data_out$SUBGRPVAR1) }) -test_that("Test Case: 4 bign conditions check when ui_bign = 'Y'", { +test_that("Check Empty Input", { data_out <- mentry( - datain = adsl, - ui_aSubset = "EFFFL=='Y'", - ui_dSubset = NA, - ui_byvar = "SEX,ETHNIC", - ui_subgrpvar = NA, - ui_trtvar = NA, - ui_trtsort = NA, - ui_trttotalyn = "N", - ui_sgtotalyn = "N", - ui_bign = "Y", - ui_addGrpMiss = "N", - ui_pop_fil = "SAFFL" + datain = data.frame(), + subset = NA_character_, + subgrpvar = "SITEGR1", + trtvar = "TRT01A", + trtsort = NA, + trttotalyn = "Y", + sgtotalyn = "Y", + add_grpmiss = "Y", + pop_fil = "SAFFL" ) - - expect_equal(data_out$bign, nrow(adsl)) + expect_equal(data_out, data.frame()) }) diff --git a/tests/testthat/test-msumstat.R b/tests/testthat/test-msumstat.R new file mode 100644 index 0000000..37ac8e4 --- /dev/null +++ b/tests/testthat/test-msumstat.R @@ -0,0 +1,158 @@ +data("adsl") +adsl_entry <- + adsl |> mentry( + subset = "EFFFL=='Y'", + byvar = "AGEGR1", + subgrpvar = "SEX", + trtvar = "TRT01A", + trtsort = "TRT01AN", + trttotalyn = "N", + add_grpmiss = "N", + pop_fil = NA + ) +expected_g <- adsl_entry |> + group_by(across(all_of(c("BYVAR1", "BYVAR1N", "TRTVAR", "SUBGRPVAR1", "SUBGRPVAR1N")))) |> + summarise(mean = round_f(mean(.data[["AGE"]], na.rm = TRUE), 2)) |> + ungroup() |> + mutate(DPTVAR = "AGE", CN = "N", DPTVARN = 1) |> + arrange(across(all_of(c("BYVAR1N", "TRTVAR", "SUBGRPVAR1N")))) +expected_t <- expected_g |> + pivot_longer(cols = "mean", names_to = "DPTVAL", values_to = "CVALUE") |> + ungroup() |> + mutate(DPTVALN = 1) |> + arrange(across(all_of(c("BYVAR1N", "TRTVAR", "SUBGRPVAR1N")))) + +test_that("Test 1: 'Check for expected inputs", { + # Logic check + # for a valid input data, the number of rows returned is >0 + actual <- msumstat( + datain = adsl_entry, + dptvar = "AGE", + statvar = "mean", + sigdec = 2 + ) + # Test + expect_type(actual, "list") + expect_named(actual, c("tsum", "gsum")) + expect_equal( + actual$gsum |> arrange(across(all_of(c("BYVAR1N", "TRTVAR", "SUBGRPVAR1N")))), + expected_g + ) + expect_equal( + actual$tsum |> arrange(across(all_of(c("BYVAR1N", "TRTVAR", "SUBGRPVAR1N")))), + expected_t + ) +}) + + +# Test 2 +test_that("Test 2: Check modified inputs", { + # LOGIC: Check if the valid Dependent Variable Exists and Type Conversion is performed + test_adsl <- adsl_entry |> + mutate(BMIBLCH = as.character(.data[["BMIBL"]])) + actual <- msumstat( + datain = test_adsl, + dptvar = "BMIBLCH", + statvar = "", + sigdec = 2 + ) + expect_named(actual, c("tsum", "gsum")) + expected <- test_adsl |> + mutate(BMIBLCH = as.numeric(BMIBLCH)) |> + filter(!is.na(BMIBLCH)) |> + group_by(across(all_of(c("BYVAR1", "BYVAR1N", "TRTVAR", "SUBGRPVAR1", "SUBGRPVAR1N")))) |> + summarise( + n = as.character(n()), + mean = round_f(mean(.data[["BMIBLCH"]], na.rm = TRUE), 2), + min = round_f(min(.data[["BMIBLCH"]], na.rm = TRUE), 2), + median = round_f(median(.data[["BMIBLCH"]], na.rm = TRUE), 2), + max = round_f(max(.data[["BMIBLCH"]], na.rm = TRUE), 2), + sd = round_f(sd(.data[["BMIBLCH"]], na.rm = TRUE), 2) + ) |> + ungroup() |> + mutate(DPTVAR = "BMIBLCH", CN = "N", DPTVARN = 1) + expect_equal(actual$gsum, expected) + actual1 <- msumstat( + datain = adsl_entry, + dptvar = "AGE", + statvar = "box", + sigdec = 2 + ) + expect_true(all( + c("median", "q25", "q75", "whiskerlow", "whiskerup", "outliers") %in% names(actual1$gsum) + )) + expect_true(all( + unique(actual1$tsum$DPTVAL) == c("median", "q25", "q75", "whiskerlow", "whiskerup", "outliers") + )) +}) + + +test_that("Test 3: Check errors", { + expect_equal( + msumstat( + datain = adsl_entry |> filter(TRTVAR == "A"), + dptvar = "BMIBL", + statvar = "", + sigdec = 2 + ), + adsl_entry |> filter(TRTVAR == "A") + ) + expect_equal( + msumstat( + datain = adsl_entry, + dptvar = "BMIBL", + a_subset = "TRTVAR == 'A'", + statvar = "", + sigdec = 2 + ), + adsl_entry |> filter(TRTVAR == "A") + ) + expect_error( + msumstat( + datain = adsl_entry, + dptvar = "YVAR", + statvar = "", + sigdec = 2 + ), + "Dependent Variable does not Exist" + ) + nostat <- msumstat( + datain = adsl_entry, + dptvar = "AGE", + statvar = "pvalue", + sigdec = 2 + ) + expect_equal(unique(nostat$gsum$pvalue), "_NO_STAT") +}) + +test_that("Test 4: Filter", { + m_filt <- msumstat( + datain = adsl_entry, + a_subset = "SEX == 'F'", + dptvar = "AGE", + statvar = "mean", + sigdec = 2 + ) + expect_true(unique(m_filt$gsum$SUBGRPVAR1) == "F") +}) + +test_that("Test 5: Sparse by value", { + adsl_entry <- mentry( + datain = adsl, + subset = "EFFFL=='Y'", + byvar = "SEX", + trtvar = "TRT01A", + trtsort = "TRT01AN", + pop_fil = NA + ) + + adsl_sum <- adsl_entry |> + msumstat( + dptvar = "AGE", + a_subset = "SEX == 'F'", + statvar = "mean(sd)~median(minmaxc)~q3", + sigdec = "3(2)~2(0)~1", + sparsebyvalyn = "Y" + ) + expect_equal(unique(adsl_sum$gsum$BYVAR1), c("F", "M")) +}) diff --git a/tests/testthat/test-occ_tier_summary.R b/tests/testthat/test-occ_tier_summary.R index 9f83672..5fb62fc 100644 --- a/tests/testthat/test-occ_tier_summary.R +++ b/tests/testthat/test-occ_tier_summary.R @@ -1,9 +1,26 @@ -ae_pre_process <- ae_pre_processor( - datain = adae, - ae_filter = "Any Event", - obs_residual = 0, - fmq_data = FMQ_Consolidated_List +data("adae") +data("adsl") +data("ae_pre_process") +adae1 <- adae |> + mutate(ASEVN = recode(.data[["AESEV"]], "MILD" = 1, "MODERATE" = 2, "SEVERE" = 3)) +ae_pre <- ae_pre_processor( + adae1, + subset = "TRTEMFL == 'Y'", + max_sevctc = "SEV", + pt_total = "Y" ) +ae_entry_max <- ae_pre[["data"]] |> + mentry( + subset = NA, + byvar = "AEBODSYS", + trtvar = "TRTA", + trtsort = "TRTAN", + trttotalyn = "N", + add_grpmiss = "N", + subgrpvar = "AESEV", + sgtotalyn = "N", + pop_fil = "Overall Population" + ) ae_entry <- ae_pre_process[["data"]] |> mentry( subset = NA, @@ -16,7 +33,7 @@ ae_entry <- ae_pre_process[["data"]] |> pop_fil = "Overall Population" ) -test_that("Standard inputs for occ_tier works", { +test_that("occ_tier standard inputs works", { output <- occ_tier_summary( ae_entry, a_subset = ae_pre_process[["a_subset"]], @@ -24,18 +41,17 @@ test_that("Standard inputs for occ_tier works", { hterm = "AEBODSYS", lterm = "AEDECOD", pctdisp = "TRT", - cutoff = 5, + cutoff_where = "PCT > 10 & FREQ > 20", apply_hrow_cutoff = "N", sort_opt = "Ascending", sort_var = "Count" ) expect_s3_class(output, "data.frame") - # Check treatments are accounted: - all(unique(output$TRTVAR) %in% unique(ae_entry$TRTVAR)) - expect_snapshot(output) + expect_true(unique(trimws(output$SUBGRPVARX)) == "n (%)") + expect_snapshot(print(output, n = Inf, width = Inf)) }) -test_that("Standard inputs for occ_tier works", { +test_that("occ_tier modified inputs works", { output <- occ_tier_summary( ae_entry, a_subset = ae_pre_process[["a_subset"]], @@ -43,80 +59,65 @@ test_that("Standard inputs for occ_tier works", { hterm = "AEBODSYS", lterm = "AEDECOD", pctdisp = "TRT", - cutoff = 5, - apply_hrow_cutoff = "N", - sort_opt = "Ascending", - sort_var = "Count" + cutoff_where = "PCT > 10 & FREQ > 20", + apply_hrow_cutoff = "Y", + nolwrtierdispyn = "Y", + htermctyn = "N", + sort_opt = "Alphabetical", + sum_row = "Y", + sum_row_label = "Any Adverse Event" ) expect_s3_class(output, "data.frame") - # Check treatments are accounted: - all(unique(output$TRTVAR) %in% unique(ae_entry$TRTVAR)) - expect_snapshot(output) + expect_true(unique(trimws(output$SUBGRPVARX)) == "n (%)") + expect_snapshot(print(output, n = Inf, width = Inf)) }) -test_that("Cut off applied for occ_tier works", { +test_that("Empty Outputs", { output <- occ_tier_summary( - ae_entry, + data.frame(), a_subset = ae_pre_process[["a_subset"]], summary_by = "Patients", hterm = "AEBODSYS", lterm = "AEDECOD", pctdisp = "TRT", - cutoff = 5, apply_hrow_cutoff = "Y", - sort_opt = "Alphabetical", - sort_var = "Count" + nolwrtierdispyn = "N", + htermctyn = "N", + sort_opt = "Alphabetical" ) expect_s3_class(output, "data.frame") - # Check treatments are accounted: - all(unique(output$TRTVAR) %in% unique(ae_entry$TRTVAR)) - expect_snapshot(output) -}) - -test_that("Cut off too high", { - output <- occ_tier_summary( + expect_equal(nrow(output), 0) + output1 <- occ_tier_summary( ae_entry, a_subset = ae_pre_process[["a_subset"]], summary_by = "Patients", hterm = "AEBODSYS", lterm = "AEDECOD", pctdisp = "TRT", - cutoff = 40, + cutoff_where = "PCT > 50", apply_hrow_cutoff = "Y", - sort_opt = "Alphabetical", - sort_var = "Count" + htermctyn = "N", + sort_opt = "Alphabetical" ) - expect_s3_class(output, "data.frame") - expect_equal(output, data.frame("Note" = "No low term data available under these conditions")) + expect_equal(nrow(output1), 0) }) -test_that("Errors resolved correctly", { - expect_error( - occ_tier_summary( - data.frame(), - a_subset = ae_pre_process[["a_subset"]], - summary_by = "Patients", - hterm = "AEBODSYS", - lterm = "AEDECOD", - pctdisp = "TRT", - cutoff = 0, - apply_hrow_cutoff = "Y", - sort_opt = "Alphabetical", - sort_var = "Count" - ), "Input data is empty" - ) - expect_error( - occ_tier_summary( - ae_entry, - a_subset = ae_pre_process[["a_subset"]], - summary_by = "Patients", - hterm = "RACE", - lterm = "AEDECOD", - pctdisp = "TRT", - cutoff = 0, - apply_hrow_cutoff = "Y", - sort_opt = "Alphabetical", - sort_var = "Count" - ) +test_that("occ_tier standard inputs works max severity", { + output <- occ_tier_summary( + ae_entry_max, + a_subset = ae_pre[["a_subset"]], + summary_by = "Patients", + hterm = "AEBODSYS", + lterm = "AEDECOD", + cutoff_where = "FREQ > 5", + pctdisp = "TRT", + sum_row = "Y", + sum_row_label = "Any Adverse Event", + nolwrtierdispyn = "N", + sort_opt = "Alphabetical", + stathead = "n" ) + expect_s3_class(output, "data.frame") + expect_true(unique(trimws(output$SUBGRPVARX)) == "n") + expect_snapshot(print(output, n = Inf, width = Inf)) }) diff --git a/tests/testthat/test-process_vx_bar_plot.R b/tests/testthat/test-process_vx_bar_plot.R new file mode 100644 index 0000000..7a923c1 --- /dev/null +++ b/tests/testthat/test-process_vx_bar_plot.R @@ -0,0 +1,28 @@ +bar_pre <- process_vx_bar_plot( + dataset_adsl = vx_bar_data$adsl, + adsl_subset = "SAFFL=='Y'", + dataset_analysis = vx_bar_data$adfacevd, + analysis_subset = "ATPTN <= 14 & toupper(FAOBJ) == 'PAIN AT INJECTION SITE' & + !(AVAL %in% c(0, 0.5)) & FATESTCD != 'OCCUR' & !is.na(AVAL)", + denom_subset = "ATPTN <= 14 & toupper(FAOBJ) == 'PAIN AT INJECTION SITE' & + !(AVAL %in% c(0, 0.5))", + overall_subset = NA, + split_by = "SEX", + trtvar = "TRT01A", + trtsort = "TRT01AN", + xvar = "ATPTN", + yvar = "PCT", + pctdisp = "DPTVAR", + legendbign = "Y" +) + + +test_that("process_vs_bar_data works as expected", { + expect_s3_class(bar_pre, "data.frame") + actual_trt <- unique(bar_pre$TRTVAR) + actual_subgrp <- unique(bar_pre$SUBGRPVAR1) + + expect_equal(actual_subgrp, sort(unique(vx_bar_data$adsl$SEX))) + expect_equal(levels(actual_trt), c("Drug1", "Drug2", "Drug3")) + expect_snapshot(print(bar_pre, width = Inf, n = Inf)) +}) diff --git a/tests/testthat/test-risk_stat.R b/tests/testthat/test-risk_stat.R index 723380b..e01f8d1 100644 --- a/tests/testthat/test-risk_stat.R +++ b/tests/testthat/test-risk_stat.R @@ -22,35 +22,25 @@ denom <- dsin1 |> freq <- dsin1 |> filter(TRTVAR %in% c("Placebo", "Xanomeline High Dose") & - eval(parse(text = ae_pre_process$a_subset))) |> + eval(parse(text = ae_pre_process$a_subset))) |> group_by(TRTVAR, AEBODSYS, AEDECOD) |> summarise(n = length(unique(USUBJID))) |> ungroup() exp <- left_join(denom, freq, by = "TRTVAR") -idvar <- c("AEBODSYS", "AEDECOD") -exp1 <- exp |> - mutate(TRTVAR = case_when( - TRTVAR == "Placebo" ~ "ctrlgrp", - TRTVAR == "Xanomeline High Dose" ~ "trtgrp" - )) |> - tidyr::pivot_wider(id_cols = any_of(c(idvar)), names_from = TRTVAR, values_from = c(N, n)) |> - mutate( - temp1 = N_ctrlgrp - n_ctrlgrp, - temp2 = N_trtgrp - n_trtgrp - ) mat <- matrix(c(2, 7, 3, 6), nrow = 2) + # testcase 1 test_that("Test Case 1: Check if the function gives expected statistic values", { - risk <- suppressWarnings(epitools::riskratio.wald(mat, conf.level = 1 - 0.05)) - + risk <- suppressWarnings(epitools::riskratio.wald(mat, conf.level = 1 - 0.05, correction = TRUE)) + risk_val <- round(risk$measure[2, 1], 3) pval <- round(risk$p.value[2, 3], 4) cil <- round(risk$measure[2, 2], 2) ciu <- round(risk$measure[2, 3], 2) - + expected <- exp |> filter(AEDECOD == "NAUSEA") |> mutate( @@ -58,10 +48,9 @@ test_that("Test Case 1: Check if the function gives expected statistic values", PVALUE = pval, RISKCIL = cil, RISKCIU = ciu, - PCT = round((n * 100) / N, 2), - TRTVAR = as.character(TRTVAR) + PCT = (n * 100) / N ) - + risk_s <- risk_stat( datain = dsin1, a_subset = ae_pre_process$a_subset, @@ -71,11 +60,11 @@ test_that("Test Case 1: Check if the function gives expected statistic values", trtgrp = "Xanomeline High Dose", statistics = "Risk Ratio", alpha = 0.05, - cutoff = 2, + cutoff_where = "PCT > 2", sort_opt = "Ascending", sort_var = "Count" ) - + actual <- risk_s |> rename(AEBODSYS = BYVAR1, AEDECOD = DPTVAL, N = TOTAL_N, n = FREQ) |> filter(AEDECOD == "NAUSEA") |> @@ -84,31 +73,30 @@ test_that("Test Case 1: Check if the function gives expected statistic values", n = as.integer(n) ) |> select(TRTVAR, N, AEBODSYS, AEDECOD, n, RISK, PVALUE, RISKCIL, RISKCIU, PCT) - + expect_equal(actual$RISK, expected$RISK) expect_equal(actual$PVALUE, expected$PVALUE) expect_equal(actual, expected, ignore_attr = TRUE) }) -# testcae 2 +# test case 2 test_that("Test Case 2: Check if the function works as expected for risk difference", { risk <- suppressWarnings(riskdiff_wald(mat, conf.level = 1 - 0.05)) - + risk_val <- round(risk$measure[2, 1], 3) pval <- round(risk$p.value[2, 3], 4) ciu <- round(risk$measure[2, 2], 4) cil <- round(risk$measure[2, 3], 4) - + expected <- exp |> filter(AEDECOD == "NAUSEA") |> mutate( RISK = risk_val, - PVALUE = pval, - TRTVAR = as.character(TRTVAR) + PVALUE = pval ) |> - arrange(desc(RISK)) - + arrange(AEDECOD) + risk_s <- risk_stat( datain = dsin1, a_subset = ae_pre_process$a_subset, @@ -118,11 +106,9 @@ test_that("Test Case 2: Check if the function works as expected for risk differe trtgrp = "Xanomeline High Dose", statistics = "Risk Difference", alpha = 0.05, - cutoff = 0, - sort_opt = "Descending", - sort_var = "RiskValue" + sort_opt = "Alphabetical" ) - + actual <- risk_s |> rename(AEBODSYS = BYVAR1, AEDECOD = DPTVAL, N = TOTAL_N, n = FREQ) |> filter(AEDECOD == "NAUSEA") |> @@ -131,50 +117,29 @@ test_that("Test Case 2: Check if the function works as expected for risk differe n = as.integer(n) ) |> select(TRTVAR, N, AEBODSYS, AEDECOD, n, RISK, PVALUE) - + + expected$RISK <- -1 * (expected$RISK) expect_equal(actual$RISK, expected$RISK) expect_equal(actual$PVALUE, expected$PVALUE) expect_equal(actual, expected, ignore_attr = TRUE) }) -# test case 1 +# test case 3 test_that("riskdiff_wald: check if the function works as expected", { evts <- 4 non_evts <- 6 control_evts <- 3 cne <- 8 - - expected_output <- (evts / (evts + non_evts)) - (control_evts / (control_evts + cne)) + + expected_output <- (control_evts / (control_evts + cne)) - (evts / (evts + non_evts)) actual <- suppressWarnings(riskdiff_wald(matrix(c(evts, control_evts, non_evts, cne), nrow = 2))) actual_output <- actual$measure[2, 1] - + expect_equal(actual_output, expected_output, ignore_attr = TRUE) }) -# test case 2 - -test_that("riskdiff_wald: check for error if `y` argument is not NULL", { - evts <- 4 - non_evts <- 6 - control_evts <- 3 - cne <- 8 - input <- matrix(c(evts, control_evts, non_evts, cne), nrow = 2) - - expect_error( - suppressWarnings( - riskdiff_wald( - x = input, - y = 2, - conf.level = 0.95, - rev = c("neither", "rows", "columns", "both"), - correction = FALSE, - verbose = FALSE - ) - ), - regexp = paste("y argument should be NULL") - ) -}) +# test case 4 test_that("risk_stat: returns empty data frame when cutoff is too high", { actual <- risk_stat( @@ -186,11 +151,11 @@ test_that("risk_stat: returns empty data frame when cutoff is too high", { trtgrp = "Xanomeline High Dose", statistics = "Risk Ratio", alpha = 0.05, - cutoff = 500, + cutoff_where = "FREQ > 500", sort_opt = "Ascending", sort_var = "Count" ) - + expected <- data.frame(NULL) expect_identical(actual, expected) }) diff --git a/tests/testthat/test-riskdiff_wald.R b/tests/testthat/test-riskdiff_wald.R deleted file mode 100644 index 81de577..0000000 --- a/tests/testthat/test-riskdiff_wald.R +++ /dev/null @@ -1,36 +0,0 @@ -# test case 1 - -test_that("check if the function works as expected", { - evts <- 4 - non_evts <- 6 - control_evts <- 3 - cne <- 8 - - expected_output <- (evts / (evts + non_evts)) - (control_evts / (control_evts + cne)) - actual <- riskdiff_wald(matrix(c(evts, control_evts, non_evts, cne), nrow = 2)) - actual_output <- actual$measure[2, 1] - - expect_equal(expected_output, actual_output) -}) - -# test case 2 - -test_that("check for error if `y` argument is not NULL", { - evts <- 4 - non_evts <- 6 - control_evts <- 3 - cne <- 8 - input <- matrix(c(evts, control_evts, non_evts, cne), nrow = 2) - - expect_error( - riskdiff_wald( - x = input, - y = 2, - conf.level = 0.95, - rev = c("neither", "rows", "columns", "both"), - correction = FALSE, - verbose = FALSE - ), - regexp = paste("y argument should be NULL") - ) -}) diff --git a/tests/testthat/test-stat_utils.R b/tests/testthat/test-stat_utils.R new file mode 100644 index 0000000..e3ca7a4 --- /dev/null +++ b/tests/testthat/test-stat_utils.R @@ -0,0 +1,161 @@ +# Test stat Utils functions + +data("survival") +sh_pre <- surv_pre_processor( + dataset_adsl = survival$adsl, + adsl_subset = "RANDFL=='Y'", + dataset_analysis = survival$adtte, + split_by = NA_character_, + analysis_subset = NA_character_, + trtsort = "TRT01PN", + censor_var = "CNSR", + censor_val = 1, + trtvar = "TRT01P", + time_var = "AVAL" +) +pairs <- combn(unique(sort((sh_pre$TRTSORT))), 2) +pair_data <- sh_pre |> + dplyr::filter(TRTSORT %in% c(pairs[, 1])) |> + dplyr::mutate(trt = ifelse(TRTSORT == pairs[1, 1], 0, 1)) |> + dplyr::arrange(trt) + +cp <- custom_cox_ph( + datain = pair_data, + ties_method = "efron", + df = 4, + pvalue_decimal = 4 +) + + +test_that("custom_cox_ph works as expected", { + # check the output names + expect_equal(names(cp), c("out", "pval")) + # find the class of the output generated + expect_type(cp$out, "list") + expect_type(cp, "list") + # check if the output working fine + expect_snapshot(print(tibble::as_tibble(cp), n = Inf)) +}) + +test_that("custom_cox_ph works with expected error when given wrong inputs", { + cp_error <- custom_cox_ph( + datain = pair_data, + ties_method = "efron", + df = "xyz", + pvalue_decimal = 4 + ) + expect_match(cp_error$err, + regex = "Spline fit is singular, try with smaller degrees of freedom" + ) + expect_equal(names(cp_error), c("out", "pval", "err")) + + + dt <- data.frame( + n = 100, + timevar = rep(10, 100), + cnsrvar = rep(0, 100), + TRTVAR = sample(c("A", "B"), 100, replace = TRUE) + ) + + new_cox <- custom_cox_ph(datain = dt) + expect_match(new_cox$err, + regex = "Not enough data to fit the Proportional Hazards Regression Model" + ) + expect_equal(names(new_cox), c("out", "pval", "err")) +}) + + +# Function: fmtrd +test_that("Test fmtrd function for calculation and precision", { + # 1: Applying fmtrd function with mean + # Test Data + sample_data <- c(10.23, 20, NA, 40, 50, 75.567) + mean_1 <- round_f(mean(sample_data, na.rm = TRUE), 2) + max_1 <- round_f(max(sample_data, na.rm = TRUE), 2) + expect_equal(fmtrd("mean")(sample_data), mean_1) + expect_equal(fmtrd("max")(sample_data), max_1) + # Significant decimal resolved correctly + mean_2 <- round_f(mean(sample_data, na.rm = TRUE), 3) + expect_equal(fmtrd("mean", d = 3)(sample_data), mean_2) +}) + +test_that("Test parse_stats", { + actual <- parse_stats( + statvar = c("mean(sd)", "median(minmax)", "stderr"), + statdec = c("3(2)", "2(1)", "3") + ) + expected <- setNames( + c("3", "2", "2", "1", "1", "3"), + c("mean", "sd", "median", "min", "max", "stderr") + ) + expect_equal(actual, expected) + actual1 <- parse_stats( + statvar = c("mean(sd)", "sd"), + statdec = "" + ) + expect_equal(actual1, c("mean" = "2", "sd" = "2")) + actual2 <- parse_stats( + statvar = c("mean", "sd"), + statdec = 2 + ) + expect_equal(actual2, c("mean" = 2, "sd" = 2)) +}) + +dist <- rnorm(20) +test_that("Test summary_functions", { + stats <- c( + "mean", "q1", "geomean", "geosd", "geomean_lowci", "geomean_upci", + "outliers", "n", "newstat" + ) + actual <- summary_functions( + statvar = stats, + statdec = rep(2, 8) + ) + expect_named(actual, stats) + expect_equal(actual$newstat(), "_NO_STAT") + logx <- log(dist) + margin <- qt(0.975, df = length(logx) - 1) * sd(logx, na.rm = TRUE) / sqrt(length(logx)) + expected <- round_f(exp(mean(logx, na.rm = TRUE) + c(-1, 1) * margin), 2) + expect_equal(actual$geomean_lowci(dist), expected[1]) + expect_equal(actual$geomean_upci(dist), expected[2]) + expect_true(is.character(actual$outliers(dist))) +}) + +test_that("Test summary_functions outputs", { + stats <- c( + "mean", "q1", "geomean", "geosd", "geomean_lowci", "geomean_upci", + "outliers", "n", "newstat" + ) + actual <- summary_functions( + statvar = stats, + statdec = rep(2, 8) + ) + expect_named(actual, stats) + expect_equal(actual$newstat(), "_NO_STAT") +}) + +test_that("Test Tukey's stats", { + exp <- min(dist[(dist >= (quantile(dist, 0.25, na.rm = TRUE) - 1.5 * IQR(dist, na.rm = TRUE))) & + (dist <= quantile(dist, 0.25, na.rm = TRUE))], na.rm = TRUE) + expect_equal(whiskerlow(dist), exp) + exp2 <- max(dist[(dist <= (quantile(dist, 0.75, na.rm = TRUE) + 1.5 * IQR(dist, na.rm = TRUE))) & + (dist >= quantile(dist, 0.75, na.rm = TRUE))], na.rm = TRUE) + expect_equal(whiskerup(dist), exp2) +}) + +test_that("Test derv_stats", { + actual <- msumstat(adsl, + dptvar = "AGE", + statvar = "stderr~mean(sd)", + sigdec = "2~3(2)" + ) + testout <- msumstat(adsl, + dptvar = "AGE", + statvar = "stderr~mean~sd", + sigdec = "2~3~2" + ) + expected <- testout$gsum |> + dplyr::mutate(`mean(sd)` = paste0(.data[["mean"]], " (", .data[["sd"]], ")")) |> + select(all_of(names(actual$gsum))) + expect_equal(actual$gsum, expected) +}) diff --git a/tests/testthat/test-surv_utils.R b/tests/testthat/test-surv_utils.R new file mode 100644 index 0000000..fa873e0 --- /dev/null +++ b/tests/testthat/test-surv_utils.R @@ -0,0 +1,68 @@ +data("survival") +sh_pre <- surv_pre_processor( + dataset_adsl = survival$adsl, + adsl_subset = "RANDFL=='Y'", + dataset_analysis = survival$adtte, + split_by = "SEX", + analysis_subset = "PARAMCD=='OS' & FASFL=='Y'", + trtsort = "TRT01PN", + censor_var = "CNSR", + censor_val = 1, + trtvar = "TRT01P", + time_var = "AVAL" +) + +data_list <- split_data_by_var( + datain = sh_pre, + split_by_prefix = "SUBGRPVAR" +) + +test_that("surv_pre_processor works as expected", { + actual_trt <- unique(sh_pre$TRTVAR) + actual_subgrp <- unique(data_list[[1]]$SUBGRPVAR1) + + expect_equal(actual_subgrp, unique(survival$adsl$SEX)) + expect_equal(levels(actual_trt), c("Drug1", "Drug2", "Drug3")) + expect_equal(levels(actual_trt), sort(as.character(actual_trt))) + expect_equal(nrow(sh_pre), 120) + expect_equal(unique(sh_pre$TRTVAR), actual_trt) + expect_error( + surv_pre_processor( + dataset_adsl = survival$adsl, + adsl_subset = "RANDFL=='Y'", + dataset_analysis = survival$adtte, + split_by = NA_character_, + analysis_subset = "PARAMCD=='OS' & FASFL=='Y'", + trtsort = "TRT01PN", + censor_var = "xxxx", + censor_val = 1, + trtvar = "TRT01P", + time_var = "AVAL" + ), + "Please provide a valid Censoring variable" + ) + + expect_error( + surv_pre_processor( + dataset_adsl = survival$adsl, + adsl_subset = "RANDFL=='Y'", + dataset_analysis = survival$adtte, + split_by = NA_character_, + analysis_subset = "PARAMCD=='OS' & FASFL=='Y'", + trtsort = "TRT01PN", + censor_var = "CNSR", + censor_val = 1, + trtvar = "TRT01P", + time_var = "xxxx" + ), + "Invalid Duration variable" + ) +}) + +test_that("pairwise_surv_stats returns survival statistics as text output", { + actual <- sh_pre |> + pairwise_surv_stats() + expected <- + "HR (Drug1 vs Drug2) = 1.60, 95% CI (0.127, 20.180), 2-sided p = 0.7152, 1-sided p = 0.6424\nHR (Drug1 vs Drug3) = 1.21, 95% CI (0.342, 4.297), 2-sided p = 0.7654, 1-sided p = 0.6173\nHR (Drug2 vs Drug3) = 0.94, 95% CI (0.098, 9.048), 2-sided p = 0.9579, 1-sided p = 0.4790" # nolint + expect_equal(actual, expected) +}) diff --git a/tests/testthat/test-tbl_display.R b/tests/testthat/test-tbl_display.R index ee2b100..a28f08b 100644 --- a/tests/testthat/test-tbl_display.R +++ b/tests/testthat/test-tbl_display.R @@ -12,7 +12,7 @@ adsl_entry <- mentry( adsl_sum <- adsl_summary( datain = adsl_entry, vars = "AGEGR1/AGEGR1N~AGE-S~SEX/SEXN~RACE/RACEN", - stat_vars = "N~Meansd" + stat_vars = "n~mean(sd)" ) tbl_data <- adsl_sum |> display_bign_head( @@ -50,7 +50,7 @@ tbl_data1 <- adsl_cat |> addrowvars = NA ) tbl1 <- tbl_data1 |> - tbl_display(bylabel = "Ethnicity") + tbl_display(bylabel = "Ethnicity", boldheadyn = "Y") test_that("tbl_processor works standard", { expect_s3_class(tbl_data, "data.frame") @@ -63,3 +63,26 @@ test_that("tbl_processor works without trt/dpt", { expect_snapshot(print(tbl_data1, n = Inf, width = Inf)) expect_true(class(tbl1) == "flextable") }) + +test_that("tbl_processor works with keepvars", { + testdata <- adsl_cat |> + mutate(NewCol = "Keepthis") + tbl_data2 <- testdata |> + display_bign_head( + mentry_data = adsl_entry1, + notrthead = "Participants, n (%)" + ) |> + tbl_processor( + disp_value_col = "N", + addrowvars = NA, + keepvars = "NewCol" + ) + expect_s3_class(tbl_data2, "data.frame") + expect_true("NewCol" %in% colnames(tbl_data2)) + expect_true(unique(tbl_data2$NewCol) == "Keepthis") +}) + +test_that("Empty_tbl works", { + tbl_empty <- empty_tbl() + expect_snapshot(tbl_empty) +}) diff --git a/tests/testthat/test-tornado_plot.R b/tests/testthat/test-tornado_plot.R index b545076..bcd7315 100644 --- a/tests/testthat/test-tornado_plot.R +++ b/tests/testthat/test-tornado_plot.R @@ -4,15 +4,13 @@ tornado_df <- process_tornado_data( dataset_adsl = tornado_plot_data[["adsl"]], dataset_analysis = tornado_plot_data[["adae"]], adsl_subset = "SAFFL == 'Y'", - analysis_subset = NA_character_, - ae_filter = "Treatment emergent", + analysis_subset = "TRTEMFL == 'Y'", obs_residual = "30", fmq_data = NA, - ae_catvar = "AESEV", + ae_catvar = "AESEV/AESEVN", trtvar = "ARMCD", trt_left = "A", trt_right = "A", - pop_fil = "Overall Population", pctdisp = "TRT", denom_subset = NA_character_, legendbign = "N", @@ -49,15 +47,13 @@ test_that("Test Case 1: process_tornado_data throws expected error message", { dataset_adsl = tornado_plot_data[["adsl"]], dataset_analysis = data.frame(), adsl_subset = "SAFFL == 'Y'", - analysis_subset = NA_character_, - ae_filter = "Treatment emergent", + analysis_subset = "TRTEMFL == 'Y'", obs_residual = "30", fmq_data = NA, ae_catvar = "AESEV", trtvar = "ARMCD", trt_left = "A", trt_right = "A", - pop_fil = "Overall Population", pctdisp = "TRT", denom_subset = NA_character_, legendbign = "N", @@ -76,7 +72,7 @@ test_that("Test Case 2: tornado data works with expected inputs", { test_that("Test Case 3: tornado_plot works with expected inputs", { legendgroups <- unique(plot_out[["data"]][["BYVAR1"]]) - + expect_true(is.ggplot(plot_out)) expect_type(plot_out, "list") expect_equal(legendgroups, unique(plot_out[["data"]][["BYVAR1"]])) @@ -98,7 +94,7 @@ test_that("Test Case 4: tornado_plot throws expected error message", { test_that("Test Case 5: tornado_plot creates tornado plot", { purrr::walk( - plot_out, c("mapping", "theme", "labels"), + c("mapping", "theme", "labels"), \(y) expect_snapshot(plot_out[[y]]) ) }) diff --git a/tests/testthat/test-utils.R b/tests/testthat/test-utils.R index 48dd687..790b309 100644 --- a/tests/testthat/test-utils.R +++ b/tests/testthat/test-utils.R @@ -47,78 +47,7 @@ test_that("Case 2: With or without 'N'", { expect_equal(agen_start, c("AGEGR1N")) }) - -############################################################################# -# summary_functions - -test_that("Test 1: Summary functions return all functions", { - ui_sigDec <- 2 - list_stats <- summary_functions(ui_sigDec) - # Test - expect_named(list_stats, c( - "mean", "min", "max", "median", "iqr", "var", "sum", "sd", "q25", - "q75", "p1", "p10", "p5", "p90", "p95", "p99", "meansd", "range", - "q1q3", "medianrange", "whiskerlow", "whiskerup", "outliers", - "geom_lowci", "geom_upci", "geommean", "n" - )) -}) - -test_that("Test 2: Summary function returns expected values", { - list_stats <- summary_functions(2)[c("meansd", "q1q3", "n", "outliers", "range")] - actstats <- iris |> - group_by(Species) |> - summarise_at(.vars = "Sepal.Width", .funs = list_stats) - fn_outlier <- function(x) { - x <- x[!is.na(x)] - paste(unique(x[x < min( - x[(x >= (quantile(x, 0.25) - 1.5 * IQR(x))) & (x <= quantile(x, 0.25))] - ) | - x > max( - x[(x <= (quantile(x, 0.75) + 1.5 * IQR(x))) & (x >= quantile(x, 0.75))] - )]), collapse = "~") - } - expstats <- iris |> - group_by(Species) |> - summarise( - meansd = paste0( - round_f(mean(.data[["Sepal.Width"]]), 2), - " (", round_f(sd(.data[["Sepal.Width"]]), 3), ")" - ), - q1q3 = paste0( - "(", round_f(quantile(.data[["Sepal.Width"]], 0.25), 2), ", ", - round_f(quantile(.data[["Sepal.Width"]], 0.75), 2), ")" - ), - n = as.character(n()), - outliers = fn_outlier(.data[["Sepal.Width"]]), - range = paste0( - "(", round_f(min(.data[["Sepal.Width"]]), 2), ", ", - round_f(max(.data[["Sepal.Width"]]), 2), ")" - ) - ) - expect_equal(actstats, expstats) -}) - -test_that("Test 3: Summary function returns expected values", { - set.seed(123) - test_data <- rnorm(100, mean = 10, sd = 2) - expected_geom_lowci <- exp(mean(log(test_data), na.rm = TRUE) - - qt(0.975, df = length(test_data) - 1) * - sd(log(test_data)) / sqrt(length(test_data))) - expected_geom_upci <- exp(mean(log(test_data), na.rm = TRUE) + - qt(0.975, df = length(test_data) - 1) * - sd(log(test_data)) / sqrt(length(test_data))) - expected_geommean <- exp(mean(log(test_data), na.rm = TRUE)) - - expect_equal(summary_functions()$geom_lowci(test_data), paste(expected_geom_lowci)) - expect_equal(summary_functions()$geom_upci(test_data), paste(expected_geom_upci)) - expect_equal(summary_functions()$geommean(test_data), paste(expected_geommean)) -}) - -############################################################################# - # Function: split_section_headers - - # Test Data adsl_entry <- mentry( @@ -176,7 +105,7 @@ test_that("Test 2: Check for exceptions", { sep = " & " ), "No variables with split_by_prefix" ) - + expect_error( split_section_headers( datain = adsl_entry, @@ -228,7 +157,7 @@ test_that("Test 2: Check for exceptions", { split_by_prefix = "TSTVAR" ), "No variables with split_by_prefix" ) - + expect_error( split_data_by_var( datain = adsl_entry, @@ -242,28 +171,15 @@ test_that("Test round_f() works", { expect_equal(round_f(13.4, 2), "13.40") expect_equal(round_f(12.243, 1), "12.2") }) -# Function: fmtrd -test_that("Test fmtrd function for calculation and precision", { - # 1: Applying fmtrd function with mean - # Test Data - sample_data <- c(10.23, 20, NA, 40, 50, 75.567) - mean_1 <- round_f(mean(sample_data, na.rm = TRUE), 2) - max_1 <- round_f(max(sample_data, na.rm = TRUE), 2) - expect_equal(fmtrd("mean")(sample_data), mean_1) - expect_equal(fmtrd("max")(sample_data), max_1) - # Significant decimal resolved correctly - mean_2 <- round_f(mean(sample_data, na.rm = TRUE), 3) - expect_equal(fmtrd("mean", d = 3)(sample_data), mean_2) -}) test_that("ord_summ_df works as expected", { actual <- iris |> ord_summ_df("Sepal.Length", "Descending") expected <- iris |> arrange(desc(.data[["Sepal.Length"]])) - + expect_identical(actual, expected) - + actual1 <- iris |> ord_summ_df("Petal.Width", "Alphabetical") expected1 <- iris |> @@ -276,9 +192,9 @@ test_that("ord_summ_df works as expected", { group_by(across(any_of("BYVAR1"))) |> arrange(.data[["Sepal.Length"]], .by_group = TRUE) |> ungroup() - + expect_identical(actual, expected) - + expect_identical(actual1, expected1) expect_identical(actual2, expected2) }) @@ -287,12 +203,12 @@ test_that("get_sort_var works as expected", { actual <- map_chr(c("Count", "Percent", "RiskValue", "Alphabetical", "abc"), \(x) get_sort_var(x)) expected <- c("CTRL_N", "CTRL_PCT", "RISK", "DPTVAL", "abc") - + expect_identical(actual, expected) }) mcat_data <- mcatstat(adsl_entry, - dptvar = "RACE" + dptvar = "RACE" ) test_that("add_bigN works as expected", { @@ -335,7 +251,7 @@ test_that("display_bign_head works as expected", { select(-all_of("TRTVAR")) |> rename("TRTVAR" = "TRTVAR_BIGN") expect_equal(actual, exp, ignore_attr = TRUE) - + # Only treatment no subgrp actual1 <- display_bign_head( datain = ae_pre, @@ -400,3 +316,97 @@ test_that("display_bign_head works as expected without treatment/subgrp", { select(-all_of("CVALUE")) expect_equal(actual, exp, ignore_attr = TRUE) }) + +test_that("sparse_vals works as expected", { + data_entry <- mentry( + adsl, + byvar = "SEX", + trtvar = "TRT01A", + trtsort = "TRT01AN", + subset = "SAFFL == 'Y'" + ) |> + mutate(DPTVAL = .data[["AGEGR1"]], DPTVALN = .data[["AGEGR1N"]]) + count <- data_entry |> + dplyr::filter(.data[["SEX"]] == "F") |> + group_by(across(all_of(c("BYVAR1", "TRTVAR", "DPTVAL")))) |> + summarise(FREQ = length(unique(.data[["USUBJID"]]))) + actual <- sparse_vals( + count, + data_sparse = data_entry, + sparseyn = "N", + sparsebyvalyn = "Y", + "BYVAR1", + character(0), + "BYVAR1N", + character(0) + ) + expect_s3_class(actual, "data.frame") + expect_equal(setdiff(unique(actual$BYVAR1), unique(count$BYVAR1)), "M") + expect_equal(unique(actual$FREQ[actual$BYVAR1 == "M"]), 0) + count1 <- data_entry |> + dplyr::filter(.data[["AGEGR1"]] != "<65") |> + group_by(across(all_of(c("BYVAR1", "TRTVAR", "DPTVAL")))) |> + summarise(FREQ = length(unique(.data[["USUBJID"]]))) + actual1 <- sparse_vals( + count1, + data_sparse = data_entry, + sparseyn = "Y", + sparsebyvalyn = "N", + "BYVAR1", + character(0), + "BYVAR1N", + character(0) + ) + expect_s3_class(actual1, "data.frame") + expect_equal(setdiff(unique(actual1$DPTVAL), unique(count1$DPTVAL)), "<65") + actual2 <- sparse_vals( + count1, + data_sparse = data_entry, + sparseyn = "N", + sparsebyvalyn = "N", + "BYVAR1", + character(0), + "BYVAR1N", + character(0) + ) + expect_s3_class(actual2, "data.frame") + expect_equal(actual2, count1) +}) + +test_that("sparse_vals with summary stat", { + data_entry <- mentry( + adsl, + byvar = "SEX", + trtvar = "TRT01A", + trtsort = "TRT01AN", + subset = "SAFFL == 'Y'" + ) + sums <- data_entry |> + dplyr::filter(.data[["SEX"]] == "F") |> + group_by(across(all_of(c("BYVAR1", "TRTVAR")))) |> + summarise(mean = as.character(mean(.data[["AGE"]], na.rm = TRUE))) + actual <- sparse_vals( + sums, + data_sparse = data_entry, + sparseyn = "N", + sparsebyvalyn = "Y", + "BYVAR1", + character(0), + "BYVAR1N", + character(0), + fillvar = "mean", + fill_with = "-" + ) + expect_s3_class(actual, "data.frame") + expect_equal(setdiff(unique(actual$BYVAR1), unique(sums$BYVAR1)), "M") + expect_equal(unique(actual$mean[actual$BYVAR1 == "M"]), "-") +}) + +test_that("dataset_vignette works", { + actual <- dataset_vignette(adsl, c("USUBJID", "TRT01A"), subset = "AGE >= 88") + expdata <- filter(adsl, .data[["AGE"]] >= 88) + expect_true("datatables" %in% class(actual)) + expect_s3_class(actual$x$data, "data.frame") + expect_equal(nrow(actual$x$data), nrow(expdata)) + expect_equal(ncol(actual$x$data), ncol(expdata)) +}) From af1f6883513183f62c8fe2bc8f67a76c6badfdb2 Mon Sep 17 00:00:00 2001 From: kallea03 Date: Tue, 7 Jan 2025 06:52:20 +0000 Subject: [PATCH 05/48] updated documentation(man) --- NAMESPACE | 6 +- man/adae_risk_summary.Rd | 32 ++++++-- man/adsl_merge.Rd | 4 +- man/adsl_summary.Rd | 53 ++++++++++-- man/ae_forest_plot.Rd | 2 +- man/ae_pre_processor.Rd | 27 +++++- man/ae_volcano_plot.Rd | 2 +- man/bar_plot.Rd | 3 +- man/dataset_merge.Rd | 4 +- man/display_bign_head.Rd | 3 +- man/edish_plot.Rd | 19 ++--- man/empty_plot.Rd | 2 +- man/empty_tbl.Rd | 20 +++++ man/event_analysis_plot.Rd | 13 +-- man/forest_display.Rd | 6 +- man/lab_abnormality_summary.Rd | 31 +++++-- man/mcatstat.Rd | 17 +++- man/mentry.Rd | 6 +- man/msumstat.Rd | 26 +++--- man/occ_tier_summary.Rd | 146 +++++++++++++++++++++++++++++++-- man/plot_axis_opts.Rd | 2 +- man/plot_display_bign.Rd | 2 +- man/process_edish_data.Rd | 18 ++-- man/process_event_analysis.Rd | 24 ++++-- man/process_tornado_data.Rd | 25 ++---- man/process_vx_box_data.Rd | 72 ---------------- man/reverselog_trans.Rd | 2 +- man/risk_stat.Rd | 24 ++++-- man/riskdiff_wald.Rd | 41 +-------- man/split_section_headers.Rd | 2 +- man/summary_functions.Rd | 24 +++--- man/summary_row_cat.Rd | 54 ++++++++++++ man/tbl_display.Rd | 11 ++- man/tbl_risk_labels.Rd | 14 ++++ man/tbl_to_plot.Rd | 2 +- man/tornado_plot.Rd | 6 +- man/vx_box_plot.Rd | 93 --------------------- 37 files changed, 501 insertions(+), 337 deletions(-) create mode 100644 man/empty_tbl.Rd delete mode 100644 man/process_vx_box_data.Rd create mode 100644 man/summary_row_cat.Rd create mode 100644 man/tbl_risk_labels.Rd delete mode 100644 man/vx_box_plot.Rd diff --git a/NAMESPACE b/NAMESPACE index c34ee1b..2cd3616 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -1,5 +1,6 @@ # Generated by roxygen2: do not edit by hand +export(adae_risk_summary) export(adsl_merge) export(adsl_summary) export(ae_forest_plot) @@ -15,6 +16,7 @@ export(dataset_vignette) export(display_bign_head) export(edish_plot) export(empty_plot) +export(empty_tbl) export(event_analysis_plot) export(forest_display) export(forest_plot_base) @@ -38,7 +40,6 @@ export(process_edish_data) export(process_event_analysis) export(process_tornado_data) export(process_vx_bar_plot) -export(process_vx_box_data) export(process_vx_scatter_data) export(reverselog_trans) export(risk_stat) @@ -52,15 +53,16 @@ export(split_data_by_var) export(split_section_headers) export(str_to_vec) export(summary_functions) +export(summary_row_cat) export(surv_pre_processor) export(tbl_display) export(tbl_processor) +export(tbl_risk_labels) export(tbl_to_plot) export(theme_cleany) export(theme_std) export(tornado_plot) export(var_start) -export(vx_box_plot) import(dplyr) import(flextable) import(ggplot2) diff --git a/man/adae_risk_summary.Rd b/man/adae_risk_summary.Rd index 8f4fe9e..a99ebdf 100644 --- a/man/adae_risk_summary.Rd +++ b/man/adae_risk_summary.Rd @@ -13,14 +13,21 @@ adae_risk_summary( ctrlgrp, trtgrp, statistics = "Risk Ratio", + riskdiff_pct = "N", alpha = 0.05, - cutoff = 5, + cutoff_where = NA, sort_opt = "Ascending", - sort_var = "Count" + sort_var = "Count", + sum_row = "N", + sum_row_label = "Participants with Any AE", + risklabels = tbl_risk_labels(), + sigdec_cat = 1, + pctsyn = "Y" ) } \arguments{ -\item{datain}{Input dataset after pre_processing and running \code{mentry()} to \emph{ADAE} data} +\item{datain}{Input data from \code{mentry()} output to get counts for each +category} \item{a_subset}{Analysis Subset condition specific to categorical analysis.} @@ -37,17 +44,30 @@ for \code{forest_plot()}.} \item{statistics}{Statistic to be calculated. Values: \verb{'Risk Ratio' or 'Risk Difference'}.} +\item{riskdiff_pct}{To display risk and CI as \% if \code{statistic} = risk difference (Y/N)} + \item{alpha}{Alpha value to determine confidence interval for risk calculation. Default: \code{0.05}} -\item{cutoff}{Incidence Cutoff Value; consider only terms with \verb{incidence percentage > cutoff}.} +\item{cutoff_where}{Filter condition for incidence/pct. Consider only terms with +eg: "FREQ > 5" or "PCT <3". Must contain FREQ or PCT (count or percent)} \item{sort_opt}{How to sort terms, only for table/forest plot. Values: \verb{'Ascending','Descending','Alphabetical'}.} \item{sort_var}{Metric to sort by. Values: \verb{'Count','Percent','RiskValue'}.} + +\item{sum_row}{To show summary/any term row or not. 'Y'/'N'} + +\item{sum_row_label}{Label for Summary Row to be displayed, if Y.} + +\item{risklabels}{List containing labels for table with elements: risk, riskci, p, low, up, lowup} + +\item{sigdec_cat}{Number of decimal places for \% displayed in output} + +\item{pctsyn}{Display Percentage Sign in table or not. Values: \code{"Y"/"N"}} } \value{ -List of summarized data frames for Adverse Events based on high and lower term. +Data frame to be displayed with risk/counts of higher and lower AE terms } \description{ ADAE Summary with Risk Statistics @@ -83,7 +103,7 @@ ae_risk <- ae_entry |> trtgrp = "Xanomeline Low Dose", statistics = "Risk Ratio", alpha = 0.05, - cutoff = 5, + cutoff_where = "PCT > 5", sort_opt = "Ascending", sort_var = "Count" ) diff --git a/man/adsl_merge.Rd b/man/adsl_merge.Rd index fecd93d..cafb5f3 100644 --- a/man/adsl_merge.Rd +++ b/man/adsl_merge.Rd @@ -4,7 +4,7 @@ \alias{adsl_merge} \title{Merge adsl dataset with the analysis dataset} \usage{ -adsl_merge(adsl = NULL, adsl_subset = "", dataset_add = NULL) +adsl_merge(adsl = NULL, adsl_subset = "", dataset_add = NULL, byvars = NULL) } \arguments{ \item{adsl}{adsl dataset} @@ -12,6 +12,8 @@ adsl_merge(adsl = NULL, adsl_subset = "", dataset_add = NULL) \item{adsl_subset}{population variable subset condition} \item{dataset_add}{analysis dataset} + +\item{byvars}{Variables to merge the datasets by} } \value{ merged dataset diff --git a/man/adsl_summary.Rd b/man/adsl_summary.Rd index 43e43c6..3e8b6fb 100644 --- a/man/adsl_summary.Rd +++ b/man/adsl_summary.Rd @@ -7,14 +7,19 @@ adsl_summary( datain, vars, - stat_vars = "N~Range~Meansd~Median~IQR", + stat_vars = "n~minmaxc~mean(sd)~median~q1q3", pctdisp = "TRT", total_catyn = "N", total_catlabel = "Total", miss_catyn = "N", miss_catlabel = "Missing", + pctsyn = "Y", + sigdec_stat = 2, + sigdec_cat = 2, a_subset = NA_character_, - denom_subset = NA_character_ + denom_subset = NA_character_, + sparseyn = "N", + sparsebyvalyn = "N" ) } \arguments{ @@ -25,10 +30,12 @@ tilde-separated} \item{stat_vars}{Statistics to display in table for numeric vars, tilde-separated.} -\item{pctdisp}{Denominator to calculate percentages by. -Values: \verb{"TRT", "VAR", "COL", "SUBGRP", "SGRPN", "CAT", "NONE", "NO", "DPTVAR"}.} +\item{pctdisp}{Method to calculate denominator (for \%) by. +Possible values: \code{"TRT"}, \code{"VAR"}, \code{"COL"}, \code{"SUBGRP"}, \code{"CAT"}, \code{"NONE"}, \code{"NO"}, \code{"DPTVAR"}, +\code{"BYVARxyN"}} -\item{total_catyn}{To return a 'Total' row for categorical analysis in \code{vars}. Values: \code{"Y"/"N"}} +\item{total_catyn}{To return a 'Total' row for the categories of \code{dptvar} +variable or not. Possible values: \code{"Y"/"N"}} \item{total_catlabel}{Label for total category row. eg- "All"/"Total"} @@ -37,10 +44,21 @@ Values: \verb{"TRT", "VAR", "COL", "SUBGRP", "SGRPN", "CAT", "NONE", "NO", "DPTV \item{miss_catlabel}{Label for missing values} +\item{pctsyn}{Display Percentage Sign in table or not. Values: \code{"Y"/"N"}} + +\item{sigdec_stat}{Number of base decimal places to retain in output of summary statistic. +Applies to mean, min, max, sd etc} + +\item{sigdec_cat}{Number of decimal places for \% displayed in output} + \item{a_subset}{Analysis Subset condition; tilde-separated for each variable in \code{vars}.} \item{denom_subset}{Subset condition to be applied to dataset for calculating denominator, tilde-separated for categorical variables within \code{vars}.} + +\item{sparseyn}{To sparse missing categories/treatments or not? \code{"Y"/"N"}} + +\item{sparsebyvalyn}{Sparse missing categories within by groups. \code{"Y"/"N"}} } \value{ \code{data.frame} to be passed on to \code{tbl_processor} and \code{tbl_display} @@ -55,7 +73,7 @@ variables, the remaining being for categorical analysis. eg. for \code{"AGEGR1/AGEGR1N~AGE-S~SEX/SEXN~BMIBL-S"}, \code{AGEGR1} and \code{SEX} will be analysed by category and \code{AGE} and \code{BMIBL} as summary statistics. \item Argument \code{stat_vars} should contain names of statistic to apply to all summary analysis -variables. +variables. \code{sigdec} applies only to statistical analysis of numeric variables (-S) \item Arguments \code{pctdisp}, \code{total_catyn}, \code{miss_catyn}, \code{miss_catlabel} apply to all variables under categorical analyses. \item \code{a_subset} should tilde-separated subset conditions, corresponding to each variable in @@ -88,11 +106,28 @@ adsl_sum <- mentry_df |> a_subset = "AGE<65~AGE>80~SEX=='F'~NA" ) +adsl_sum |> + display_bign_head(mentry_data = mentry_df) |> + tbl_processor( + statlabel = "N~Range~Meansd~Median~Q1Q3", + dptlabel = "Age Group~_NONE_~Sex~Race", + addrowvar = "DPTVAR" + ) |> + tbl_display() |> + flextable::autofit() + +# Same variable with 2 unique subset conditions +adsl_sum <- mentry_df |> + adsl_summary( + vars = "AGEGR1~AGE-S~SEX~SEX~RACE", + a_subset = "AGE<65~AGE>80~SEX=='F'~SEX=='M'~NA" + ) + adsl_sum |> display_bign_head(mentry_data = mentry_df) |> tbl_processor( statlabel = "N~Range~Meansd~Median~IQR", - dptlabel = "Age Group~NONE~Sex~Race", + dptlabel = "Age Group~_NONE_~Sex1~Sex2~Race", addrowvar = "DPTVAR" ) |> tbl_display() |> @@ -174,13 +209,13 @@ adsl_vs <- pharmaverseadam::adsl |> adsl_vs |> adsl_summary( vars = "SEX~AGE-S~AGEGR1~RACE~ETHNIC~HEIGHT-S~WEIGHT-S~BMI-S", - stat_vars = "medianrange~meansd" + stat_vars = "median(minmax)~mean(sd)" ) |> display_bign_head(adsl_vs) |> tbl_processor( dptlabel = "Sex, n(\%)~Age (Years)~Age Category (Years), n(\%)~Race, n(\%)~Ethnicity, n(\%)~Height (cm)~Weight (kg)~BMI (kg/m2)", - statlabel = "Median (Range)~Mean (SD)", + statlabel = "Median (Min, Max)~Mean (SD)", addrowvars = "DPTVAR" ) |> tbl_display() |> diff --git a/man/ae_forest_plot.Rd b/man/ae_forest_plot.Rd index 12e763f..ef6d5d2 100644 --- a/man/ae_forest_plot.Rd +++ b/man/ae_forest_plot.Rd @@ -113,7 +113,7 @@ ae_risk_forest <- risk_stat( trtgrp = "Xanomeline High Dose", statistics = "Risk Ratio", alpha = 0.05, - cutoff = 5, + cutoff_where = "FREQ >5", sort_opt = "Ascending", sort_var = "Count" ) |> diff --git a/man/ae_pre_processor.Rd b/man/ae_pre_processor.Rd index 6367b6f..7430e63 100644 --- a/man/ae_pre_processor.Rd +++ b/man/ae_pre_processor.Rd @@ -9,7 +9,15 @@ ae_pre_processor( fmq_data = NULL, date_vars = c("ASTDT", "AENDT", "TRTSDT", "TRTEDT"), ae_filter = "Any Event", - obs_residual = NA_real_ + subset = NA, + obs_residual = NA_real_, + max_sevctc = NA_character_, + sev_ctcvar = "ASEVN", + hterm = "AEBODSYS", + lterm = "AEDECOD", + rpt_byvar = character(0), + trtvar = "TRTA", + pt_total = "N" ) } \arguments{ @@ -24,9 +32,26 @@ Permissible Values: "ANY", "ANY EVENT", "TREATMENT EMERGENT", "SERIOUS", "DRUG-RELATED", "RELATED", "MILD", "MODERATE", "SEVERE", "RECOVERED/RESOLVED", "RECOVERING/RESOLVING", "NOT RECOVERING/NOT RESOLVING", "FATAL", "GRADE N"} +\item{subset}{Analysis subset condition to be applied to \code{ADAE} dataset prior to ADSL join; +will be appended to \code{ae_filter}} + \item{obs_residual}{If not NA, use this argument to pass a period (numeric) to extend the observation period. If passed as NA, overall study duration is considered for analysis. eg. if 5, only events occurring upto 5 days past the TRTEDT are considered.} + +\item{max_sevctc}{If needed to filter maximum severity/ctc grade rows. Values: NA/"SEV"/"CTC"} + +\item{sev_ctcvar}{Variable to determine max severity. eg: ASEVN, ATOXGRN} + +\item{hterm}{High Level Event Term (req for max Sev tables only)} + +\item{lterm}{Low Level Event Term (req for max Sev tables only)} + +\item{rpt_byvar}{Page/report by variable if any, to identify max sev/ctc} + +\item{trtvar}{Treatment Variable} + +\item{pt_total}{Required to calculate total of preferred terms? Y/N} } \value{ : a list containing 2 objects diff --git a/man/ae_volcano_plot.Rd b/man/ae_volcano_plot.Rd index 13ea3ad..dd94db9 100644 --- a/man/ae_volcano_plot.Rd +++ b/man/ae_volcano_plot.Rd @@ -74,7 +74,7 @@ ae_risk <- risk_stat( trtgrp = "Xanomeline High Dose", statistics = "Risk Ratio", alpha = 0.05, - cutoff = 5, + cutoff_where = "FREQ >5", sort_opt = "Ascending", sort_var = "Count" ) diff --git a/man/bar_plot.Rd b/man/bar_plot.Rd index 92c93ef..173bdfe 100644 --- a/man/bar_plot.Rd +++ b/man/bar_plot.Rd @@ -72,7 +72,8 @@ adsl_entry <- mentry( adsl_sum <- msumstat( datain = adsl_entry, dptvar = "AGE", - statvar = "mean" + statvar = "mean", + figyn = "Y" )[["gsum"]] |> plot_display_bign(adsl_entry) |> dplyr::mutate( diff --git a/man/dataset_merge.Rd b/man/dataset_merge.Rd index 51f76c0..32d24b4 100644 --- a/man/dataset_merge.Rd +++ b/man/dataset_merge.Rd @@ -4,7 +4,7 @@ \alias{dataset_merge} \title{Merge Datasets} \usage{ -dataset_merge(..., byvars, subset = NULL) +dataset_merge(..., byvars, subset = NULL, type = "left") } \arguments{ \item{...}{Datasets to be merged.} @@ -13,6 +13,8 @@ dataset_merge(..., byvars, subset = NULL) \item{subset}{Dataset specific subset conditions as \code{list}, default is \code{NULL}. Has to be specified in the same order of datasets to be merged} + +\item{type}{Type of join to perform. Values: "left", "right", "inner", "full", "semi", "anti"} } \value{ A \code{data.frame} diff --git a/man/display_bign_head.Rd b/man/display_bign_head.Rd index 7a8619c..7560432 100644 --- a/man/display_bign_head.Rd +++ b/man/display_bign_head.Rd @@ -44,8 +44,7 @@ adsl_entry <- mentry( msumstat( adsl_entry, dptvar = "AGE", - statvar = "meansd", - sigdec = 2, + statvar = "mean", dptvarn = 2 )$tsum |> display_bign_head(adsl_entry) diff --git a/man/edish_plot.Rd b/man/edish_plot.Rd index 4ad8fd3..133fd6c 100644 --- a/man/edish_plot.Rd +++ b/man/edish_plot.Rd @@ -58,12 +58,11 @@ order: "upper right~lower right~upper left~lower left" or as a vector of length } } \examples{ -data("adsl") -data("adlb") +data("lab_data") merged_data <- adsl_merge( - adsl = adsl, - dataset_add = adlb + adsl = lab_data$adsl, + dataset_add = lab_data$adlb ) |> mentry( subset = "SAFFL == 'Y'", @@ -74,9 +73,9 @@ merged_data <- adsl_merge( pt_data <- process_edish_data( datain = merged_data, xvar = "both", - alt_paramcd = "ALT", - ast_paramcd = "AST", - bili_paramcd = "BILI" + alt_paramcd = "L00028S", + ast_paramcd = "L00030S", + bili_paramcd = "L00021S" ) series_opts <- plot_aes_opts(pt_data, @@ -88,9 +87,9 @@ edish_plot( datain = pt_data, axis_opts = plot_axis_opts( xlinearopts = list( - breaks = c(0.1, 1, 2, 10), - limits = c(0.1, 10), - labels = c("0.1", "1", "2x ULN", "10") + breaks = c(0.1, 1, 2, 5), + limits = c(0.1, 5), + labels = c("0.1", "1", "2x ULN", "5") ), ylinearopts = list( breaks = c(0.1, 1, 3, 10), diff --git a/man/empty_plot.Rd b/man/empty_plot.Rd index f582b42..6fbc0c4 100644 --- a/man/empty_plot.Rd +++ b/man/empty_plot.Rd @@ -22,6 +22,6 @@ a list containing 2 objects Empty plot with message } \examples{ -library(carver) +library(tlfcarver) empty_plot() } diff --git a/man/empty_tbl.Rd b/man/empty_tbl.Rd new file mode 100644 index 0000000..65c0ee1 --- /dev/null +++ b/man/empty_tbl.Rd @@ -0,0 +1,20 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/tbl_display.R +\name{empty_tbl} +\alias{empty_tbl} +\title{Return table if output is empty} +\usage{ +empty_tbl(text = "No participant meets the reporting criteria") +} +\arguments{ +\item{text}{Text to display under table creation} +} +\value{ +flextable output +} +\description{ +Return table if output is empty +} +\examples{ +empty_tbl() +} diff --git a/man/event_analysis_plot.Rd b/man/event_analysis_plot.Rd index 2ee6dde..f3066b9 100644 --- a/man/event_analysis_plot.Rd +++ b/man/event_analysis_plot.Rd @@ -50,17 +50,12 @@ prep_ae <- adae |> ) ## prepare data for plot -prep_entry <- prep_ae[["data"]] |> - mentry( - trtvar = "TRTA", - trtsort = "TRTAN", - trttotalyn = "N", - byvar = "FMQ_NAM" - ) -## prepare data for plot -prep_event_analysis <- prep_entry |> +prep_event_analysis <- prep_ae[["data"]] |> process_event_analysis( a_subset = glue::glue("AOCCPFL == 'Y' & {prep_ae$a_subset}"), + trtvar = "TRTA", + trtsort = "TRTAN", + trttotalyn = "Y", summary_by = "Events", hterm = "FMQ_NAM", ht_val = "ABDOMINAL PAIN", diff --git a/man/forest_display.Rd b/man/forest_display.Rd index 4596e08..6228c95 100644 --- a/man/forest_display.Rd +++ b/man/forest_display.Rd @@ -9,7 +9,8 @@ forest_display( rel_widths = c(0.25, 0.38, 0.27, 0.1), interactive = "N", plot_height = NULL, - xpos = "top" + xpos = "top", + legend_opts = list(pos = "bottom", dir = "horizontal") ) } \arguments{ @@ -26,6 +27,9 @@ Values: "Y"/"N"} \item{xpos}{Where should X xaxis for \code{splot} and \code{fplot} be displayed in interactive plot? Values: "top"/"bottom". Value for static output is decided prior to passing in this function.} + +\item{legend_opts}{Legend styling option, a \code{list} containing \code{pos}(position) and +\code{dir} (direction).} } \value{ plot_grid object or plotly forest plot object diff --git a/man/lab_abnormality_summary.Rd b/man/lab_abnormality_summary.Rd index 8684674..aeeff04 100644 --- a/man/lab_abnormality_summary.Rd +++ b/man/lab_abnormality_summary.Rd @@ -9,7 +9,11 @@ lab_abnormality_summary( crit_vars = "CRIT3~CRIT4", pctdisp = "SUBGRP", a_subset = NA_character_, - denom_subset = NA_character_ + denom_subset = NA_character_, + sigdec = 2, + sparseyn = "Y", + pctsyn = "N", + stathead = "n (\%)" ) } \arguments{ @@ -17,12 +21,22 @@ lab_abnormality_summary( \item{crit_vars}{Criteria variables} -\item{pctdisp}{Denominator to calculate percentages by. -Values: \verb{"TRT", "VAR","COL", "SUBGRP", "SGRPN", "CAT", "NONE", "NO", "DPTVAR"}} +\item{pctdisp}{Method to calculate denominator (for \%) by. +Possible values: \code{"TRT"}, \code{"VAR"}, \code{"COL"}, \code{"SUBGRP"}, \code{"CAT"}, \code{"NONE"}, \code{"NO"}, \code{"DPTVAR"}, +\code{"BYVARxyN"}} + +\item{a_subset}{Analysis Subset condition specific to categorical analysis.} + +\item{denom_subset}{Subset condition to be applied to data set for calculating denominator.} + +\item{sigdec}{Number of decimal places for \% displayed in output} -\item{a_subset}{Subset conditions for analysis of dependent variable.} +\item{sparseyn}{To sparse missing categories/treatments or not? \code{"Y"/"N"}} -\item{denom_subset}{Subset conditions for denominator eg. \code{"APSBLFL == 'Y'"}} +\item{pctsyn}{Display Percentage Sign in table or not. Values: \code{"Y"/"N"}} + +\item{stathead}{Column label to display \code{n} in the output. Default is \verb{n (\%)} +Values: \verb{"TRT", "VAR","COL", "SUBGRP", "SGRPN", "CAT", "NONE", "NO", "DPTVAR"}} } \value{ \code{data.frame} with summary of laboratory abnormality incidence counts @@ -52,12 +66,11 @@ out <- crit_vars = "CRIT3~CRIT4", pctdisp = "SUBGRP", a_subset = NA_character_, - denom_subset = NA_character_ + denom_subset = NA_character_, + sigdec = 1 ) |> display_bign_head(mentry_data = lb_entry) |> - tbl_processor( - dptlabel = "" - ) + tbl_processor() out diff --git a/man/mcatstat.Rd b/man/mcatstat.Rd index 985205d..927d6f9 100644 --- a/man/mcatstat.Rd +++ b/man/mcatstat.Rd @@ -18,7 +18,11 @@ mcatstat( total_catlabel = "Total", dptvarn = 1, pctsyn = "Y", - denomyn = "N" + sigdec = 2, + denomyn = "N", + sparseyn = "N", + sparsebyvalyn = "N", + return_zero = "N" ) } \arguments{ @@ -29,7 +33,7 @@ category} \item{denom_subset}{Subset condition to be applied to data set for calculating denominator.} -\item{uniqid}{Variable to calculate unique counts of. Expected values: \code{"USUBJID"}, \code{"SITEID"}, +\item{uniqid}{Variable(s) to calculate unique counts of. eg. \code{"USUBJID"}, \code{"SITEID"}, \code{"ALLCT"}} \item{dptvar}{Categorical Analysis variable and ordering variable if exists, @@ -57,7 +61,16 @@ multiple \code{mcatstat()} outputs are created to be combined.} \item{pctsyn}{Display Percentage Sign in table or not. Values: \code{"Y"/"N"}} +\item{sigdec}{Number of decimal places for \% displayed in output} + \item{denomyn}{Display denominator in output or not. Values: \code{"Y"/"N"}} + +\item{sparseyn}{To sparse missing categories/treatments or not? \code{"Y"/"N"}} + +\item{sparsebyvalyn}{Sparse missing categories within by groups. \code{"Y"/"N"}} + +\item{return_zero}{Return rows with zero counts if analysis subset/ non-missing does not +exist in data. \code{"Y"/"N"}} } \value{ a data.frame with counts and/or percentages, passed to diff --git a/man/mentry.Rd b/man/mentry.Rd index d76eb09..6ef6532 100644 --- a/man/mentry.Rd +++ b/man/mentry.Rd @@ -2,7 +2,7 @@ % Please edit documentation in R/mentry.R \name{mentry} \alias{mentry} -\title{Function to read in and process data with subsets and variables.} +\title{Read and process data with subsets and variables} \usage{ mentry( datain, @@ -13,6 +13,7 @@ mentry( trtsort = NA, trttotalyn = "N", trttotlabel = "Total", + trtmissyn = "N", sgtotalyn = "N", add_grpmiss = "N", pop_fil = "Overall Population" @@ -40,6 +41,9 @@ table or category in plot (\code{"Y"/"N"}).} \item{trttotlabel}{Label for total Treatment column/group} +\item{trtmissyn}{Retain Missing treatment counts in Total (if \code{trttotalyn} = Y). Missing +treatment will not be considered as a column in analysis in any case.} + \item{sgtotalyn}{Add total subgroup values to be displayed as column in table or category in plot (\code{"Y"/"N"}).} diff --git a/man/msumstat.Rd b/man/msumstat.Rd index 29a3b01..93ffaaf 100644 --- a/man/msumstat.Rd +++ b/man/msumstat.Rd @@ -9,8 +9,10 @@ msumstat( a_subset = NA_character_, dptvar = NULL, statvar = "", - sigdec = 1, - dptvarn = 1 + sigdec = "", + dptvarn = 1, + sparsebyvalyn = "N", + figyn = "N" ) } \arguments{ @@ -23,10 +25,14 @@ msumstat( \item{statvar}{\code{Tilde} (\code{~})-separated list of statistics to be computed. eg: \code{"mean~median"}} \item{sigdec}{Number of base decimal places to retain in output -Applies to mean, min, max etc and \code{+ 1} for sd} +Applies to mean, min, max, sd etc} \item{dptvarn}{Number to assign as \code{'DPTVARN'}, used for block sorting when multiple blocks are created to be combined.} + +\item{sparsebyvalyn}{Sparse missing categories within by groups. \code{"Y"/"N"}} + +\item{figyn}{Determine if output is for figure or not \code{"Y"/"N"}} } \value{ a list containing 2 elements @@ -42,10 +48,9 @@ Summary statistics for numeric data variable Current available statistics (values for \code{statvar}) : n (count per group), mean, median, sd (standard deviation), min, max, iqr (interquartile range), var (variance), sum, range ("min, max") -meansd ("mean (sd)"), medianrange ("median (range)"), -q25/q1 (25 \% quantile), q75/q3 (75 \% quantile) , p10 (10\% quantile), p5, p1, -p90, p95, p99, q1q3 ("q25, q75"), whiskerlow, whiskerup (box lower/upper -whiskers), outliers (boxplot outliers, tilde-separated output), +mean(sd), median(minmax), q25/q1 (25 \% quantile), q75/q3 (75 \% quantile) , p10 (10\% quantile), +p5, p1, p90, p95, p99, q1q3 ("q25, q75"), whiskerlow, whiskerup (box lower/upper +whiskers), outliers (boxplot outliers, tilde-separated output), geometric mean/sd/CI box = median~q25~q75~whiskerlow~whiskerup~outliers (Tukey's method) } \examples{ @@ -63,9 +68,10 @@ adsl_entry <- mentry( adsl_sum <- adsl_entry |> msumstat( dptvar = "AGE", - a_subset = "BYVAR1 == 'M'", - statvar = "mean", - sigdec = 2 + a_subset = "SEX == 'F'", + statvar = "mean(sd)~median(minmaxc)~q3", + sigdec = "3(2)~2(0)~1", + sparsebyvalyn = "N" ) adsl_sum$tsum diff --git a/man/occ_tier_summary.Rd b/man/occ_tier_summary.Rd index 84cad3f..5bb8649 100644 --- a/man/occ_tier_summary.Rd +++ b/man/occ_tier_summary.Rd @@ -10,11 +10,19 @@ occ_tier_summary( summary_by = "Patients", hterm = "AEBODSYS", lterm = "AEDECOD", + htermctyn = "Y", pctdisp = "TRT", - cutoff = 2, + cutoff_where = NA, + sum_row = "N", + sum_row_label = "Number of Participants with Any AE", apply_hrow_cutoff = "N", sort_opt = "Ascending", - sort_var = "Count" + sort_var = "Count", + sort_col = 1, + nolwrtierdispyn = "N", + sigdec_cat = 2, + pctsyn = "Y", + stathead = "n (\%)" ) } \arguments{ @@ -24,28 +32,48 @@ occ_tier_summary( \item{summary_by}{Measure to construct the summary by. Values: \verb{'Patients' or 'Events'}.} -\item{hterm}{High Level Event term variable, used for analysis} +\item{hterm}{High Level Event term variable, used for analysis (tilde-separated)} \item{lterm}{Low Level Event term variable, used for analysis} +\item{htermctyn}{To show count of high term rows or not. Should correspond to and be same number +of terms passed in \code{hterm} (tilde-separated). To suppress showing counts for any term pass "N"} + \item{pctdisp}{Method to calculate denominator (for \%) by. Possible values: \code{"TRT"}, \code{"VAR"}, \code{"COL"}, \code{"SUBGRP"}, \code{"CAT"}, \code{"NONE"}, \code{"NO"}, \code{"DPTVAR"}, \code{"BYVARxyN"}} -\item{cutoff}{Incidence Cutoff Value; consider only terms with \verb{incidence percentage > cutoff}.} +\item{cutoff_where}{Filter condition for incidence/pct. Consider only terms with +eg: "FREQ > 5" or "PCT <3". Must contain FREQ or PCT (count or percent)} + +\item{sum_row}{To show summary/any term row or not. 'Y'/'N'} + +\item{sum_row_label}{Label for Summary Row to be displayed, if Y.} -\item{apply_hrow_cutoff}{To apply cutoff value to high terms in addition to low term. +\item{apply_hrow_cutoff}{To apply \code{cutoff_where} value to high terms in addition to low term. If set to "Y" same cutoff is applied to remove both high and low level terms that don't meet the criteria. If set to "N" (default), cutoff is applied only to Lower Level term. The terms that do not fit the criteria are then excluded from the counts for High Level term. This does not happen in case -of "N" - all counts are included in high term which is displayed as long as it meets the criteria -as well.} +of "N" - all low terms are included in high term which is displayed as long as it meets the +criteria as well.} \item{sort_opt}{How to sort terms, only for table/forest plot. Values: \verb{'Ascending','Descending','Alphabetical'}.} \item{sort_var}{Metric to sort by. Values: \verb{'Count','Percent','RiskValue'}.} + +\item{sort_col}{Which treatment column to sort by. (Depends on trt levels) eg: 1, 2, 3} + +\item{nolwrtierdispyn}{When\code{apply_hrow_cutoff} = Y, to display high level terms with zero low +level terms satisfying the cutoff threshold or not? If Y, high terms will be displayed even with +no corresponding lower levels in the table.} + +\item{sigdec_cat}{Number of decimal places for \% displayed in output} + +\item{pctsyn}{Display Percentage Sign in table or not. Values: \code{"Y"/"N"}} + +\item{stathead}{Label for sub-column header in output. eg. "n (\%)"} } \value{ Summarized data frame for Adverse Events based on high and lower terms. @@ -53,6 +81,28 @@ Summarized data frame for Adverse Events based on high and lower terms. \description{ Generic Occurrence Summary Tiered Table } +\details{ +\itemize{ +\item \code{cutoff_where} is applied to event lower term only, unless \code{apply_hrow_cutoff} is given. +\item If \code{apply_hrow_cutoff} is Y, cutoff_where is applied to higher terms as well. If it is N, +lower terms which do not meet criteria are removed from higher term count. eg: if \code{cutoff_where} +is set to "PCT >= 2" and \code{hterm} and \code{lterm} are AEBODSYS and AEDECOD: + +EYE DISORDERS 9 (3.1) +Dry eye 3 (1.4) +Wet eye 6 (2.4) + +Here if \code{apply_hrow_cutoff} is set to N then 'Dry eye' row will be excluded and the 3 excluded +from count of EYE DISORDERS as well (9). If Y, then 'Dry eye' will be excluded but EYE DISORDERS +not impacted as it is 4.4\% and its PCT >= 2. + +\item If \code{cutoff_where} is PCT >= 3 and \code{nolwrtierdispyn} set to Y, then +neither Dry eye nor Wet eye will be shown, but EYE DISORDERS will still be displayed. + +If \code{nolwrtierdispyn} is N in this case, EYE DISORDERS will also be removed as no low terms meet +the criteria. +} +} \examples{ ae_entry <- ae_pre_process[["data"]] |> mentry( @@ -72,7 +122,7 @@ output <- occ_tier_summary( hterm = "AEBODSYS", lterm = "AEDECOD", pctdisp = "TRT", - cutoff = 2, + cutoff_where = "PCT > 2", apply_hrow_cutoff = "N", sort_opt = "Ascending", sort_var = "Count" @@ -80,5 +130,85 @@ output <- occ_tier_summary( output |> tbl_processor() |> tbl_display() +# Example 2: ADAE table with max sev/ctc grade: +ae_pre <- ae_pre_processor( + tornado_plot_data$adae, + subset = "TRTEMFL == 'Y'", + max_sevctc = "SEV", + sev_ctcvar = "ASEVN", + pt_total = "Y" +) +ae_entry_max <- adsl_merge( + tornado_plot_data$adsl, + adsl_subset = 'SAFFL == "Y"', + ae_pre[["data"]] +) |> + mentry( + subset = NA, + byvar = "AEBODSYS", + trtvar = "TRTA", + trtsort = "TRTAN", + trttotalyn = "N", + add_grpmiss = "N", + subgrpvar = "ASEV", + sgtotalyn = "N", + pop_fil = "Overall Population" + ) +rpt_data <- occ_tier_summary( + ae_entry_max, + a_subset = ae_pre[["a_subset"]], + summary_by = "Patients", + hterm = "AEBODSYS", + lterm = "AEDECOD", + cutoff_where = "FREQ > 2", + pctdisp = "TRT", + sum_row = "Y", + sum_row_label = "Any Adverse Event", + nolwrtierdispyn = "N", + sort_opt = "Alphabetical", + stathead = "n (\%)" +) +rpt_data |> + tbl_processor() |> + tbl_display(dpthead = "No. of Adverse Events_SOC and PT") |> + flextable::autofit() +## ADPR Example: +\dontrun{ +pr_entry <- adsl |> + adsl_merge( + adsl_subset = "SAFFL == 'Y'", + dataset_add = adpr + ) |> + mentry( + subset = NA, + byvar = "PRSOC", + trtvar = "TRT01A", + trtsort = "TRT01AN", + trttotalyn = "N", + add_grpmiss = "N", + sgtotalyn = "N", + pop_fil = "Overall Population" + ) +output <- occ_tier_summary( + pr_entry, + a_subset = "ONPERFL == 'Y' & PRDECOD != ''", + summary_by = "Patients", + hterm = "PRSOC", + lterm = "PRDECOD", + pctdisp = "TRT", + apply_hrow_cutoff = "N", + sort_opt = "Ascending", + sort_var = "Count", + sum_row = "Y", + sum_row_label = "Participants with 1 term", + htermctyn = "N" +) +output |> + display_bign_head( + mentry_data = pr_entry + ) |> + tbl_processor() |> + tbl_display() +} } diff --git a/man/plot_axis_opts.Rd b/man/plot_axis_opts.Rd index 27e8ea7..9a9672a 100644 --- a/man/plot_axis_opts.Rd +++ b/man/plot_axis_opts.Rd @@ -41,7 +41,7 @@ Combined list of X and Y axis options Plot Axes Options } \examples{ -library(carver) +library(tlfcarver) plot_axis_opts( xlinearopts = list( diff --git a/man/plot_display_bign.Rd b/man/plot_display_bign.Rd index deda4b6..3ef9ccc 100644 --- a/man/plot_display_bign.Rd +++ b/man/plot_display_bign.Rd @@ -32,7 +32,7 @@ adsl_entry <- adsl |> msumstat( adsl_entry, dptvar = "AGE", - statvar = "meansd" + statvar = "mean" )$gsum |> plot_display_bign(adsl_entry) } diff --git a/man/process_edish_data.Rd b/man/process_edish_data.Rd index eef09d1..0cecd7d 100644 --- a/man/process_edish_data.Rd +++ b/man/process_edish_data.Rd @@ -9,7 +9,8 @@ process_edish_data( xvar = "both", alt_paramcd = "L00030S", ast_paramcd = "L00028S", - bili_paramcd = "L00021S" + bili_paramcd = "L00021S", + legendbign = "Y" ) } \arguments{ @@ -26,6 +27,8 @@ variable.} \item{ast_paramcd}{\code{PARAMCD} value for \verb{ASPARTATE AMINOTRANSFERASE} in \code{datain}.} \item{bili_paramcd}{\code{PARAMCD} value for \code{BILIRUBIN} in \code{datain}.} + +\item{legendbign}{(\code{string}) Display BIGN in Legend (\code{Y/N}).} } \value{ A \code{data.frame} required for \code{edish_plot}. @@ -34,12 +37,11 @@ A \code{data.frame} required for \code{edish_plot}. Process data for eDISH plot } \examples{ -data("adlb") -data("adsl") +data("lab_data") merged_data <- adsl_merge( - adsl = adsl, - dataset_add = adlb + adsl = lab_data$adsl, + dataset_add = lab_data$adlb ) |> mentry( subset = "SAFFL == 'Y'", @@ -50,9 +52,9 @@ merged_data <- adsl_merge( merged_data |> process_edish_data( xvar = "both", - alt_paramcd = "ALT", - ast_paramcd = "AST", - bili_paramcd = "BILI" + alt_paramcd = "L00030S", + ast_paramcd = "L00028S", + bili_paramcd = "L00021S" ) } diff --git a/man/process_event_analysis.Rd b/man/process_event_analysis.Rd index 8646cf7..366ca21 100644 --- a/man/process_event_analysis.Rd +++ b/man/process_event_analysis.Rd @@ -7,6 +7,9 @@ process_event_analysis( datain, a_subset = NA_character_, + trtvar, + trtsort = NA_character_, + trttotalyn = "N", summary_by = "Events", hterm, ht_val, @@ -21,6 +24,14 @@ process_event_analysis( \item{a_subset}{Analysis subset condition.} +\item{trtvar}{Treatment variable name string assigned as \code{TRTVAR}.} + +\item{trtsort}{Treatment sorting variable name string - numeric or +character variable assigned as \code{TRTSORT}.} + +\item{trttotalyn}{Add total treatment values to be displayed as column in +table or category in plot (\code{"Y"/"N"}).} + \item{summary_by}{Set to \code{'Events'} or \code{'Patients'}.} \item{hterm}{\code{MedDRA} queries or high level variables; valid values : \code{"FMQ_NAM"}, \code{"AEBODSYS"}} @@ -52,17 +63,14 @@ prep_ae <- adae |> obs_residual = 0, fmq_data = FMQ_Consolidated_List ) -prep_entry <- prep_ae[["data"]] |> - mentry( - trtvar = "TRTA", - trtsort = "TRTAN", - trttotalyn = "N", - byvar = "FMQ_NAM" - ) + ## prepare data for plot -prep_event_analysis <- prep_entry |> +prep_event_analysis <- prep_ae[["data"]] |> process_event_analysis( a_subset = glue::glue("AOCCPFL == 'Y' & {prep_ae$a_subset}"), + trtvar = "TRTA", + trtsort = "TRTAN", + trttotalyn = "Y", summary_by = "Events", hterm = "FMQ_NAM", ht_val = "ABDOMINAL PAIN", diff --git a/man/process_tornado_data.Rd b/man/process_tornado_data.Rd index 977d2ca..8003506 100644 --- a/man/process_tornado_data.Rd +++ b/man/process_tornado_data.Rd @@ -9,7 +9,6 @@ process_tornado_data( dataset_analysis, adsl_subset = NA_character_, analysis_subset = NA_character_, - ae_filter = "Any Event", obs_residual = NA_real_, fmq_data = NULL, split_by = NA_character_, @@ -18,11 +17,11 @@ process_tornado_data( trt_left, trt_right, trtsort = NA_character_, - pop_fil = "Overall Population", + subset = NA_character_, pctdisp = "TRT", denom_subset = NA_character_, legendbign = "N", - yvar + yvar = "AESOC" ) } \arguments{ @@ -32,12 +31,7 @@ process_tornado_data( \item{adsl_subset}{(\code{string}) Subset condition to be applied on \code{dataset_adsl}.} -\item{analysis_subset}{Subset conditions for overall data.} - -\item{ae_filter}{Vector of adverse event types to be used as filter conditions. -Permissible Values: "ANY", "ANY EVENT", "TREATMENT EMERGENT", "SERIOUS", -"DRUG-RELATED", "RELATED", "MILD", "MODERATE", "SEVERE", "RECOVERED/RESOLVED", -"RECOVERING/RESOLVING", "NOT RECOVERING/NOT RESOLVING", "FATAL", "GRADE N"} +\item{analysis_subset}{Subset conditions for \code{dataset_analysis}} \item{obs_residual}{If not NA, use this argument to pass a period (numeric) to extend the observation period. If passed as NA, overall study duration is considered for analysis. @@ -47,7 +41,7 @@ eg. if 5, only events occurring upto 5 days past the TRTEDT are considered.} \item{split_by}{(\code{string}) By variable for stratification.} -\item{ae_catvar}{Categorical variable for severity analysis.} +\item{ae_catvar}{Categorical variable for severity analysis and order variable. eg; "ASEV/ASEVN"} \item{trtvar}{(\code{string}) Treatment Variable to be created for analysis.} @@ -57,7 +51,7 @@ eg. if 5, only events occurring upto 5 days past the TRTEDT are considered.} \item{trtsort}{(\code{string}) Variable to sort treatment variable by.} -\item{pop_fil}{Population Filter for data set: Name of flag variable. +\item{subset}{Overall subset for data set. eg: "EFFFL == 'Y'" eg: \code{"SAFFL"}, \code{"EFFFL"} or \code{NA} for Overall Population.} \item{pctdisp}{Method to calculate denominator (for \%) by @@ -79,7 +73,8 @@ Pre-process data for tornado plot \details{ \itemize{ \item ae_catvar grouping variable for severity like AESEV(MILD, MODERATE, -SEVERE). It must also have it's numeric variable in the dataset. +SEVERE). It must be passed "/" separated with its numeric variable. +eg: ASEV/ASEVN; ATOXGR/ATOXGRN \item yvar(dptvar) Adverse Event category, derived term from AE. Possible Values: AEBODSYS, AEDECOD, AEHLT, AEHLGT. } @@ -91,15 +86,13 @@ process_tornado_data( dataset_adsl = tornado_plot_data[["adsl"]], dataset_analysis = tornado_plot_data[["adae"]], adsl_subset = "SAFFL == 'Y'", - analysis_subset = NA_character_, - ae_filter = "Treatment emergent", + analysis_subset = "TRTEMFL == 'Y'", obs_residual = "30", fmq_data = NA, - ae_catvar = "AESEV", + ae_catvar = "AESEV/AESEVN", trtvar = "ARMCD", trt_left = "A", trt_right = "A", - pop_fil = "Overall Population", pctdisp = "TRT", denom_subset = NA_character_, legendbign = "N", diff --git a/man/process_vx_box_data.Rd b/man/process_vx_box_data.Rd deleted file mode 100644 index 79577e6..0000000 --- a/man/process_vx_box_data.Rd +++ /dev/null @@ -1,72 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/vx_boxplot.R -\name{process_vx_box_data} -\alias{process_vx_box_data} -\title{Process data for vaccine boxplot} -\usage{ -process_vx_box_data( - dataset_adsl, - dataset_analysis, - adsl_subset = "SAFFL == 'Y'", - analysis_subset = "((ANL08FL == 'Y')|(ANL09FL=='Y'))&(ARMCD!='')", - split_by = NA_character_, - trtvar = "TRT01A", - trtsort = "TRT01AN", - xvar = "AVISIT", - yvar = "AVAL", - ystat = "mean", - legendbign = "Y", - ada_nab_opts = list(N = "1~2", LAB = "ADA Titer (log2)~NAb Titer (log2)", REF = - "6.23~1.58", DIL = "75~3") -) -} -\arguments{ -\item{dataset_adsl}{(\code{data.frame}) ADSL dataset.} - -\item{dataset_analysis}{(\code{data.frame}) Analysis Dataset.} - -\item{adsl_subset}{(\code{string}) Subset condition to be applied on \code{dataset_adsl}.} - -\item{analysis_subset}{(\code{string}) Subset Condition to be applied on \code{dataset_analysis}.} - -\item{split_by}{(\code{string}) By variable for stratification.} - -\item{trtvar}{(\code{string}) Treatment Variable to be created for analysis.} - -\item{trtsort}{(\code{string}) Variable to sort treatment variable by.} - -\item{xvar}{(\code{string}) Values for X axis, determined by filter condition for -analysis visit.} - -\item{yvar}{(\code{string}) Values for Y axis, determined by filter condition for -analysis visit.} - -\item{ystat}{Additional Statistic to be calculated and plotted as markers. -Values: 'mean', 'sum', 'sd' etc} - -\item{legendbign}{(\code{string}) Display count as (N = ..) in Treatment legend? Values: "Y"/"N"} - -\item{ada_nab_opts}{List of values of : \emph{PARAMN}, Y axis Label, Reference -line value and Dilution (for footnote) corresponding to ADA and NAb titers -respectively. Format: list(N = "1~2", -LAB = "ADA Titer (log2)~NAb Titer (log2)", REF = "6.23~1.58", DIL = "75~3")} -} -\value{ -Dataframe containing analysis values for requisite box plot statistics -} -\description{ -Process data for vaccine boxplot -} -\examples{ -data(vx_box_data) -process_vx_box_data( - dataset_adsl = vx_box_data$adsl, - dataset_analysis = vx_box_data$adisda, - adsl_subset = "RANDFL == 'Y'", - analysis_subset = "((ANL08FL == 'Y')|(ANL09FL=='Y'))&(ARMCD!='')&(PARAMN \%in\% c(1, 2))", - trtvar = "TRTA", - trtsort = "TRTAN", - xvar = "AVISIT", - ystat = "mean" -) -} diff --git a/man/reverselog_trans.Rd b/man/reverselog_trans.Rd index 1049e80..8b973a2 100644 --- a/man/reverselog_trans.Rd +++ b/man/reverselog_trans.Rd @@ -16,7 +16,7 @@ Transformation object per given base Reverse Log transformation of value to pass to scale options } \examples{ -library(carver) +library(tlfcarver) library(ggplot2) ggplot(data = mtcars, mapping = aes(x = mpg, y = hp)) + geom_point() + diff --git a/man/risk_stat.Rd b/man/risk_stat.Rd index e7fceca..ddea189 100644 --- a/man/risk_stat.Rd +++ b/man/risk_stat.Rd @@ -13,14 +13,19 @@ risk_stat( trtgrp, statistics = "Risk Ratio", alpha = 0.05, - cutoff = 2, + cutoff_where = NA, sort_opt, sort_var, - g_sort_by_ht = "N" + g_sort_by_ht = "N", + riskdiff_pct = "N", + sigdec = 1, + pctsyn = "Y", + hoveryn = "Y" ) } \arguments{ -\item{datain}{Input dataset after pre_processing and running \code{mentry()} to \emph{ADAE} data} +\item{datain}{Input data from \code{mentry()} output to get counts for each +category} \item{a_subset}{Analysis Subset condition specific to categorical analysis.} @@ -37,7 +42,8 @@ for \code{forest_plot()}.} \item{alpha}{Alpha value to determine confidence interval for risk calculation. Default: \code{0.05}} -\item{cutoff}{Incidence Cutoff Value; consider only terms with \verb{incidence percentage > cutoff}.} +\item{cutoff_where}{Filter condition for incidence/pct. Consider only terms with +eg: "FREQ > 5" or "PCT <3". Must contain FREQ or PCT (count or percent)} \item{sort_opt}{How to sort terms, only for table/forest plot. Values: \verb{'Ascending','Descending','Alphabetical'}.} @@ -47,6 +53,14 @@ Values: \verb{'Ascending','Descending','Alphabetical'}.} \item{g_sort_by_ht}{For Forest Plot only - include sorting by high term/\emph{BYVAR1}? Values: "Y"/"N". In the output, terms will be sorted by group first, then term. To be used along with \code{ht_dispyn} = Y in \code{ae_forest_plot()}} + +\item{riskdiff_pct}{To display risk and CI as \% if \code{statistic} = risk difference (Y/N)} + +\item{sigdec}{Number of decimal places for \% displayed in output} + +\item{pctsyn}{Display Percentage Sign in table or not. Values: \code{"Y"/"N"}} + +\item{hoveryn}{Include hover information (for graphs) Y/N} } \value{ A dataset containing risk statistic calculations for given treatment pair(s). @@ -80,7 +94,7 @@ risk_stat( trtgrp = "Xanomeline High Dose", statistics = "Risk Ratio", alpha = 0.05, - cutoff = 2, + cutoff_where = "PCT > 2", sort_opt = "Ascending", sort_var = "Count" ) diff --git a/man/riskdiff_wald.Rd b/man/riskdiff_wald.Rd index 0f01e24..13d17e8 100644 --- a/man/riskdiff_wald.Rd +++ b/man/riskdiff_wald.Rd @@ -1,26 +1,10 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/risk_stat.R, R/riskdiff_wald.R +% Please edit documentation in R/risk_stat.R \name{riskdiff_wald} \alias{riskdiff_wald} \title{Calculate Risk difference} \usage{ -riskdiff_wald( - x, - y = NULL, - conf.level = 0.95, - rev = c("neither", "rows", "columns", "both"), - correction = FALSE, - verbose = FALSE -) - -riskdiff_wald( - x, - y = NULL, - conf.level = 0.95, - rev = c("neither", "rows", "columns", "both"), - correction = FALSE, - verbose = FALSE -) +riskdiff_wald(x, conf.level = 0.95) } \arguments{ \item{x}{input data @@ -28,26 +12,12 @@ input data can be one of the following: r x 2 table, vector of numbers from a contigency table (will be transformed into r x 2 table in row-wise order), or single factor or character vector that will be combined with y into a table.} -\item{y}{single factor or character vector that will be combined with x into a table -(default is NULL)} - \item{conf.level}{confidence level (default is 0.95)} - -\item{rev}{reverse order of "rows", "colums", "both", or "neither" (default)} - -\item{correction}{Yate's continuity correction} - -\item{verbose}{To return more detailed results} } \value{ -a list containg a data,measure,p.value,correction - a list containg a data,measure,p.value,correction } \description{ -Function to calculate risk difference by unconditional maximum likelihood estimation (Wald) -for any given treatment pairs. - Function to calculate risk difference by unconditional maximum likelihood estimation (Wald) for any given treatment pairs. } @@ -56,11 +26,4 @@ riskdiff_wald( x = matrix(c(178, 79, 1411, 1486), 2, 2), conf.level = 0.95 ) -riskdiff_wald( - x = matrix(c(178, 79, 1411, 1486), 2, 2), - conf.level = 0.95, - rev = c("neither", "rows", "columns", "both"), - correction = FALSE, - verbose = FALSE -) } diff --git a/man/split_section_headers.Rd b/man/split_section_headers.Rd index 631172f..48e2a8d 100644 --- a/man/split_section_headers.Rd +++ b/man/split_section_headers.Rd @@ -8,7 +8,7 @@ split_section_headers( datain, split_by = "", split_by_prefix = "", - split_lab = "", + split_lab = " ", sep = "~" ) } diff --git a/man/summary_functions.Rd b/man/summary_functions.Rd index f89f763..23b4176 100644 --- a/man/summary_functions.Rd +++ b/man/summary_functions.Rd @@ -1,21 +1,25 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/utils.R +% Please edit documentation in R/stat_utils.R \name{summary_functions} \alias{summary_functions} -\title{Create summary stats function for use within \code{msumstat()}} +\title{List of Summary Functions} \usage{ -summary_functions(sigdec = 2) +summary_functions(statvar, statdec) } \arguments{ -\item{sigdec}{Number of significant decimal places (base)} +\item{statvar}{Input statistics} + +\item{statdec}{Corresponding number of decimal places for each statistic} } \value{ -a named list containing function definition for all defined summary -statistics - mean, min, max, median, iqr, var, sum, sd, q25, q75, p1, p5, -p10, p90, p95, p99 (where last digits represent \% of quantile), meansd, -range, q1q3, medianrange (concatenation of indicated names), whiskerlow, -whiskerup, outliers in the Tukey method for box statistics +A named list containing function definition for all defined summary +statistics - mean, min, max, median, mode iqr, var, sum, sd, q25, q75, p1, p5, +p10, p90, p95, p99 (where last digits represent \% of quantile), whiskerlow, +whiskerup, outliers in the Tukey method for box statistics, geometric mean/sd/CI } \description{ -Create summary stats function for use within \code{msumstat()} +List of Summary Functions +} +\examples{ +summary_functions(c("mean", "mode"), c(2, 1)) } diff --git a/man/summary_row_cat.Rd b/man/summary_row_cat.Rd new file mode 100644 index 0000000..1cce857 --- /dev/null +++ b/man/summary_row_cat.Rd @@ -0,0 +1,54 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/occ_tier_summary.R +\name{summary_row_cat} +\alias{summary_row_cat} +\title{Insert Overall/Summary Row} +\usage{ +summary_row_cat( + datain, + sum_row_label = "Any Term", + byvaryn = "N", + a_subset = NA, + pctdisp = "TRT", + sigdec = 2, + pctsyn = "Y", + var = "ANY", + uniqid = "USUBJID" +) +} +\arguments{ +\item{datain}{Input dataset \code{ADAM} or intermediate within summary function} + +\item{sum_row_label}{Label for Summary Row to be displayed, if Y.} + +\item{byvaryn}{Include by variable or not? For single overally row, "N"} + +\item{a_subset}{Analysis Subset condition specific to categorical analysis.} + +\item{pctdisp}{Method to calculate denominator (for \%) by. +Possible values: \code{"TRT"}, \code{"VAR"}, \code{"COL"}, \code{"SUBGRP"}, \code{"CAT"}, \code{"NONE"}, \code{"NO"}, \code{"DPTVAR"}, +\code{"BYVARxyN"}} + +\item{sigdec}{Number of decimal places for \% displayed in output} + +\item{pctsyn}{Display Percentage Sign in table or not. Values: \code{"Y"/"N"}} + +\item{var}{Flag Variable to identify Any/Summary Rows} + +\item{uniqid}{Variable(s) to calculate unique counts of. eg. \code{"USUBJID"}, \code{"SITEID"}, +\code{"ALLCT"}} +} +\value{ +dataframe with single overall row count +} +\description{ +Insert Overall/Summary Row +} +\examples{ +data("adae") +summary_row_cat( + adae, + a_subset = "TRTEMFL == 'Y'" +) + +} diff --git a/man/tbl_display.Rd b/man/tbl_display.Rd index 9385032..4384b70 100644 --- a/man/tbl_display.Rd +++ b/man/tbl_display.Rd @@ -4,7 +4,14 @@ \alias{tbl_display} \title{Create flextable output from display templates} \usage{ -tbl_display(datain, bylabel, dpthead = " ", font = "Arial", fontsize = 10) +tbl_display( + datain, + bylabel = NA, + dpthead = " ", + font = "Arial", + fontsize = 10, + boldheadyn = "N" +) } \arguments{ \item{datain}{Input dataframe} @@ -16,6 +23,8 @@ tbl_display(datain, bylabel, dpthead = " ", font = "Arial", fontsize = 10) \item{font}{Font face for text inside table} \item{fontsize}{Font size for text inside table} + +\item{boldheadyn}{Y/N to determine if table header should be bold} } \value{ flextable object diff --git a/man/tbl_risk_labels.Rd b/man/tbl_risk_labels.Rd new file mode 100644 index 0000000..b8d422c --- /dev/null +++ b/man/tbl_risk_labels.Rd @@ -0,0 +1,14 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/adae_risk_summary.R +\name{tbl_risk_labels} +\alias{tbl_risk_labels} +\title{Labels for AE risk table} +\usage{ +tbl_risk_labels() +} +\value{ +list of labels +} +\description{ +Labels for AE risk table +} diff --git a/man/tbl_to_plot.Rd b/man/tbl_to_plot.Rd index 6b1fdd0..39e5c3a 100644 --- a/man/tbl_to_plot.Rd +++ b/man/tbl_to_plot.Rd @@ -40,7 +40,7 @@ ggplot object Convert dataframe into ggplot object table } \examples{ -library(carver) +library(tlfcarver) MPG <- ggplot2::mpg MPG[["cyl"]] <- as.character(MPG[["cyl"]]) tbl_to_plot( diff --git a/man/tornado_plot.Rd b/man/tornado_plot.Rd index 915ea3c..3eb452e 100644 --- a/man/tornado_plot.Rd +++ b/man/tornado_plot.Rd @@ -49,15 +49,13 @@ tornado_df <- process_tornado_data( dataset_adsl = tornado_plot_data[["adsl"]], dataset_analysis = tornado_plot_data[["adae"]], adsl_subset = "SAFFL == 'Y'", - analysis_subset = NA_character_, - ae_filter = "Treatment emergent", + analysis_subset = "TRTEMFL == 'Y'", obs_residual = "30", fmq_data = NA, - ae_catvar = "AESEV", + ae_catvar = "AESEV/AESEVN", trtvar = "ARMCD", trt_left = "A", trt_right = "A", - pop_fil = "Overall Population", pctdisp = "TRT", denom_subset = NA_character_, legendbign = "N", diff --git a/man/vx_box_plot.Rd b/man/vx_box_plot.Rd deleted file mode 100644 index f339ea4..0000000 --- a/man/vx_box_plot.Rd +++ /dev/null @@ -1,93 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/vx_boxplot.R -\name{vx_box_plot} -\alias{vx_box_plot} -\title{Generate Vaccine Boxplots for antibody titer using analysed data} -\usage{ -vx_box_plot( - datalist, - axis_opts, - series_opts, - legend_opts = list(lab = "", pos = "bottom", dir = "horizontal"), - box_opts = c(0.7, 0.9), - ystat = "mean", - griddisplay = "N" -) -} -\arguments{ -\item{datalist}{List of Input datasets, retrieved from -\code{process_vx_box_data()} and \code{split_data_by_var()}} - -\item{axis_opts}{A \code{list} of axis specific options retrieved from \code{plot_axis_opts()}.} - -\item{series_opts}{Series Variable styling options, a \code{list} containing -\code{shape}, \code{color} and \code{size}.} - -\item{legend_opts}{Legend styling option, a \code{list} containing \code{label}, \code{pos}(position) and -\code{dir} (direction).} - -\item{box_opts}{Vector containing: -\enumerate{ -\item Width of individual boxes in plot and -\item Width of the interval between box-groups of X axis. eg. c(0.9, 0.9) -}} - -\item{ystat}{Additional statistic to be plotted as markers. Default: \emph{mean}} - -\item{griddisplay}{Display Grid \code{(Y/N)}.} -} -\value{ -a list of lists, each of 2 elements: -\itemize{ -\item \code{plot} Plot output -\item \code{footnote} Text to be considered as first line of footnote in report -} -} -\description{ -Creates 2 similar plots with slightly different specifications according to -parameter i.e., ADA or NaB titer values are plotted in 2 separate graphs -} -\details{ -Input data should come from output of \code{process_vx_box_data()} and -is expected to have the standardised variable XVAR and ada_nab_opts -} -\examples{ -data(vx_box_data) -plot_data <- process_vx_box_data( - dataset_adsl = vx_box_data$adsl, - dataset_analysis = vx_box_data$adisda, - adsl_subset = "RANDFL == 'Y'", - analysis_subset = "((ANL08FL == 'Y')|(ANL09FL=='Y'))&(ARMCD!='')&(PARAMN \%in\% c(1, 2))", - trtvar = "TRTA", - trtsort = "TRTAN", - xvar = "AVISIT", - ystat = "mean", - legendbign = "Y" -) -series_opts <- plot_aes_opts( - datain = plot_data, - series_color = c("red", "blue", "green"), - series_shape = c("circlefilled", "trianglefilled", "squarefilled"), - series_size = c(2, 2, 2) -) - -# Splitting data to generate separate plots by `split_by` variable -data_list <- split_data_by_var( - datain = plot_data, - split_by_prefix = "SUBGRPVAR" -) - -vx_box_plot( - datalist = data_list, - axis_opts = plot_axis_opts( - xaxis_label = "Visits" - ), - series_opts = series_opts, - legend_opts = list( - lab = "Treatment", - pos = "bottom", - dir = "horizontal" - ), - ystat = "mean" -)[[1]][[1]] -} From 7de2db1d3b388f614a58373fbf6422d786908b4d Mon Sep 17 00:00:00 2001 From: kallea03 Date: Tue, 7 Jan 2025 07:01:38 +0000 Subject: [PATCH 06/48] Apply automatic changes --- R/adae_risk_summary.R | 4 +- R/adlb_r301.R | 4 +- R/adsl_r001.R | 2 +- R/ae_forestplot.R | 8 +-- R/ae_pre_processor.R | 8 +-- R/ae_volcano_plot.R | 2 +- R/bar_plot.R | 6 +- R/data_read.R | 5 +- R/dataset_merge.R | 8 +-- R/edish_plot.R | 16 ++--- R/event_analysis.R | 20 +++--- R/graph_utils.R | 76 ++++++++++++----------- R/line_plot.R | 2 +- R/mcatstat.R | 26 ++++---- R/mentry.R | 20 +++--- R/msumstat.R | 8 +-- R/occ_tier_summary.R | 12 ++-- R/process_vx_bar_plot.R | 12 ++-- R/ptly_utils.R | 8 +-- R/risk_stat.R | 18 +++--- R/run_app.R | 3 +- R/scatter_plot.R | 20 +++--- R/stat_utils.R | 4 +- R/surv_utils.R | 10 +-- R/tbl_display.R | 27 ++++---- R/tornado_plot.R | 28 ++++----- R/utils.R | 20 +++--- tests/testthat/test-adsl_r001.R | 34 +++++----- tests/testthat/test-ae_pre_processor.R | 2 +- tests/testthat/test-bar_plot.R | 8 +-- tests/testthat/test-edish_plot.R | 14 ++--- tests/testthat/test-event_analysis.R | 2 +- tests/testthat/test-graph_utils.R | 12 ++-- tests/testthat/test-km_plot.R | 6 +- tests/testthat/test-mcatstat.R | 20 +++--- tests/testthat/test-mentry.R | 8 +-- tests/testthat/test-msumstat.R | 2 +- tests/testthat/test-process_vx_bar_plot.R | 2 +- tests/testthat/test-risk_stat.R | 28 ++++----- tests/testthat/test-stat_utils.R | 26 ++++---- tests/testthat/test-surv_utils.R | 4 +- tests/testthat/test-tornado_plot.R | 2 +- tests/testthat/test-utils.R | 18 +++--- 43 files changed, 284 insertions(+), 281 deletions(-) diff --git a/R/adae_risk_summary.R b/R/adae_risk_summary.R index a2794fa..06b3eed 100644 --- a/R/adae_risk_summary.R +++ b/R/adae_risk_summary.R @@ -128,7 +128,7 @@ adae_risk_summary <- function(datain, if (!is.na(cutoff_where) && str_detect(cutoff_where, "PCT|FREQ")) { datain <- datain |> left_join(select(ae_lsumm, all_of(c("BYVAR1", "CUTFL")), {{ lterm }} := "DPTVAL"), - by = c("BYVAR1", lterm) + by = c("BYVAR1", lterm) ) a_subset <- paste(na.omit(c(a_subset, "CUTFL == 'Y'")), collapse = "&") } @@ -163,7 +163,7 @@ adae_risk_summary <- function(datain, mutate(DPTVAR = term, CN = "C") }) |> set_names(h_terms) - + ## retrun empty flextable if (nrow(ae_hsumm[[1]]) < 1) { return(data.frame()) diff --git a/R/adlb_r301.R b/R/adlb_r301.R index 893a192..c32c8e4 100644 --- a/R/adlb_r301.R +++ b/R/adlb_r301.R @@ -78,7 +78,7 @@ lab_abnormality_summary <- function(datain, byvars <- var_start(datain, "BYVAR") byvarsN <- glue("{byvars}N") stopifnot("Criteria Variables/Flags not present in `datain`" = all(dptvars %in% names(datain)) || - all(dptvars_fl %in% names(datain))) + all(dptvars_fl %in% names(datain))) # handle denom_subset when not specified if (is.na(denom_subset) || str_squish(denom_subset) == "") { if ("APSBLFL" %in% names(datain)) { @@ -102,7 +102,7 @@ lab_abnormality_summary <- function(datain, map(\(dptval) { asubset <- glue("{dptvars_fl[dptval]} == 'Y'") if (!is.na(a_subset) && - str_squish(a_subset) != "") { + str_squish(a_subset) != "") { asubset <- glue("{a_subset} & {asubset}") } ## add lab abnormality counts diff --git a/R/adsl_r001.R b/R/adsl_r001.R index c52f7cd..0df89f1 100644 --- a/R/adsl_r001.R +++ b/R/adsl_r001.R @@ -280,7 +280,7 @@ adsl_summary <- function(datain, #' split_var_types <- function(vars) { num_vars <- vars[str_which(vars, "-S")] - + list( num_vars = str_replace_all(num_vars, "-S", ""), cat_vars = vars[!vars %in% num_vars], diff --git a/R/ae_forestplot.R b/R/ae_forestplot.R index c29eecf..42c0795 100644 --- a/R/ae_forestplot.R +++ b/R/ae_forestplot.R @@ -155,8 +155,8 @@ ae_forest_plot <- per_page <- split( events, rep(seq_along(events), - each = terms_perpg, - length.out = length(events) + each = terms_perpg, + length.out = length(events) ) ) } else { @@ -180,7 +180,7 @@ ae_forest_plot <- dat_out <- dat_out |> mutate( YCAT = ifelse(.data[["BYVAR1"]] == lead(.data[["BYVAR1"]], default = "last"), - "", .data[["BYVAR1"]] + "", .data[["BYVAR1"]] ), XVAR = "HT" ) @@ -273,7 +273,7 @@ ae_forest_hlt_sig <- function(plotin, TRUE ~ NA_character_ )) |> filter(!is.na(.data[["EFFECT"]])) - + plotin + geom_point( data = hltpts, diff --git a/R/ae_pre_processor.R b/R/ae_pre_processor.R index d6a92d9..e0a8e15 100644 --- a/R/ae_pre_processor.R +++ b/R/ae_pre_processor.R @@ -85,7 +85,7 @@ ae_pre_processor <- function(datain, mutate(PT = str_trim(toupper(.data[["AEDECOD"]]))) |> left_join(fmq, by = "PT") } - + # Standardizing date format to common format data_pro <- datain |> mutate(across( @@ -98,7 +98,7 @@ ae_pre_processor <- function(datain, if ("ASTDT" %in% names(data_pro) && any(is.na(data_pro[["AEDECOD"]]))) { data_pro <- data_pro |> mutate(AEDECOD = if_else(!is.na(.data[["ASTDT"]]) & is.na(.data[["AEDECOD"]]), - "Not Yet Coded", .data[["AEDECOD"]] + "Not Yet Coded", .data[["AEDECOD"]] )) } # AE-Specific filter conditions @@ -120,7 +120,7 @@ ae_pre_processor <- function(datain, c(filters, glue("(ASTDT > TRTSDT) & (ASTDT < (TRTEDT + {obs_residual}))")) ), collapse = " & ") } - + # Apply AE filters if exist: if (!is.na(filters) && filters != "") { data_pro <- data_pro |> @@ -164,7 +164,7 @@ ae_pre_processor <- function(datain, filters <- paste(na.omit(c(filters, "MAX_SEVCTC == 1")), collapse = " & ") } ################### ENDax SEV/CTC############## - + # Return processed dataframe and filter conditions return(list(data = ungroup(data_pro), a_subset = filters)) } diff --git a/R/ae_volcano_plot.R b/R/ae_volcano_plot.R index ea11712..bbf96d3 100644 --- a/R/ae_volcano_plot.R +++ b/R/ae_volcano_plot.R @@ -120,7 +120,7 @@ ae_volcano_plot <- function(datain, ) ) + # color code by SOC geom_point(aes(size = .data[["CTRL_N"]]), pch = 21, alpha = 0.5) - + # Error when there are no adjusted p-values <= 0.05 so remove FDR adjusted P when no # adjusted p <= p value cutoff check_sig <- datain |> diff --git a/R/bar_plot.R b/R/bar_plot.R index 67de73a..a295ccd 100644 --- a/R/bar_plot.R +++ b/R/bar_plot.R @@ -95,7 +95,7 @@ bar_plot <- function(datain, # Remove empty rows datain <- datain |> mutate(YVAR = as.numeric(YVAR)) - + # Bar plot: # Legend Labels if based on other variable: series_labels <- series_leg_lab(datain, series_var, series_labelvar) @@ -111,7 +111,7 @@ bar_plot <- function(datain, values = series_opts$color, labels = series_labels ) - + if (bar_pos == "stacked") { g_plot <- g_plot + geom_bar(stat = "identity", width = bar_width) @@ -141,7 +141,7 @@ bar_plot <- function(datain, y = axis_opts$yaxis_label ) + theme_std(axis_opts, legend_opts, griddisplay) - + # Rotate plot if needed: if (flip_plot == "Y") { g_plot <- g_plot + coord_flip() diff --git a/R/data_read.R b/R/data_read.R index c6568d5..de427da 100644 --- a/R/data_read.R +++ b/R/data_read.R @@ -36,8 +36,9 @@ #' df <- data_read(ui_data_source = "Default", ui_adam_data = "ADSL") #' dplyr::slice_head(df$adam$adsl, n = 10) data_read <- function( - ui_data_source, - ui_adam_data) { + ui_data_source, + ui_adam_data +) { adam <- list() adam_attrib <- list() ### Reading data from local folders, based on the format of the file loaded in the shiny interface diff --git a/R/dataset_merge.R b/R/dataset_merge.R index 0d27b70..1a52a02 100644 --- a/R/dataset_merge.R +++ b/R/dataset_merge.R @@ -75,13 +75,13 @@ dataset_merge <- function(..., byvars, subset = NULL, type = "left") { "Type should be one of left, right, inner, full" = type %in% c("left", "right", "inner", "full") ) - + if (type == "full") { warning("For full join, subsets will not work as expected. Consider using adsl_merge() instead") } byvars <- str_to_vec(byvars) if (!every(dfs, \(x) all(byvars %in% names(x)))) stop("`byvars` not present") - + if (length(subset) > 0) { stopifnot("Length of subsets and datasets should be equal" = length(dfs) == length(subset)) if (every(subset, is.na)) stop("All subsets cannot be `NA`, use `subset = NULL` instead") @@ -94,7 +94,7 @@ dataset_merge <- function(..., byvars, subset = NULL, type = "left") { df_sub }) } - + df_list <- map(seq_along(dfs), \(x) { out <- dfs[[x]] if (x < length(dfs)) { @@ -137,7 +137,7 @@ adsl_merge <- function(adsl = NULL, adsl_subset = "", dataset_add = NULL, byvars if (is.null(byvars)) { byvars <- intersect(colnames(adsl), colnames(dataset_add)) } - + outdata <- full_join(adsl, dataset_add, by = byvars) if (adsl_subset != "" && !is.na(adsl_subset)) { outdata <- filter(outdata, !!!parse_exprs(adsl_subset)) diff --git a/R/edish_plot.R b/R/edish_plot.R index 184738a..18bb83b 100644 --- a/R/edish_plot.R +++ b/R/edish_plot.R @@ -63,7 +63,7 @@ process_edish_data <- function(datain, "Please provide valid PARAMCD" = all(c(alt_paramcd, ast_paramcd, bili_paramcd) %in% datain$PARAMCD) ) - + hy_data <- datain |> filter( .data$PARAMCD %in% c(alt_paramcd, ast_paramcd, bili_paramcd), @@ -77,7 +77,7 @@ process_edish_data <- function(datain, TRUE ~ "bili" ) ) - + hy <- hy_data |> group_by(across(all_of(c("USUBJID", "TRTVAR", "PARAMCD", "PARAM", "PARM")))) |> summarise(x = max(.data$maxv)) |> @@ -86,13 +86,13 @@ process_edish_data <- function(datain, names_from = PARM, values_from = x ) - + if (xvar %in% c("alt", "ast")) { hy <- hy |> mutate(XVAR = .data[[xvar]]) } else { hy <- hy |> mutate(XVAR = pmax(.data$ast, .data$alt)) } - + hy |> mutate( text = paste0( @@ -100,7 +100,7 @@ process_edish_data <- function(datain, USUBJID, "\n", ifelse(xvar == "both", "Max of ALT/AST = ", - paste("value of", toupper(xvar), "=") + paste("value of", toupper(xvar), "=") ), round(XVAR, 3), "\n", @@ -230,9 +230,9 @@ edish_plot <- function(datain, max(as.numeric(yrefline[1]), max(axis_opts$Ybrks)), as.numeric(yrefline[1]) - 0.2 ) - + # for ploting values per subject - + sp <- datain |> scatter_plot( axis_opts = axis_opts, @@ -278,7 +278,7 @@ edish_plot <- function(datain, y = quad_labels_opts_y[4], label = quad_labels[4] ) - + # ggplotly if interactive if (interactive == "Y") { sp <- as_plotly(plot = sp, hover = c("text")) diff --git a/R/event_analysis.R b/R/event_analysis.R index e15454c..7c23370 100644 --- a/R/event_analysis.R +++ b/R/event_analysis.R @@ -127,7 +127,7 @@ event_analysis_plot <- query_plot <- query_plot + geom_hline(yintercept = ref_line, linetype = "dashed") + ggtitle(query_title) - + p <- cowplot::plot_grid( pt_plot, query_plot, @@ -140,7 +140,7 @@ event_analysis_plot <- event_plotly(ref_line, pt_title) query_plot <- query_plot |> event_plotly(ref_line, query_title) - + p <- subplot( pt_plot, query_plot, @@ -219,7 +219,7 @@ process_event_analysis <- ) ht_val <- get_event_scope(hterm, ht_val, ht_scope) lt_val <- get_event_scope(lterm, lt_val, lt_scope, ht_val) - + ae_counts <- datain |> mentry( byvar = hterm, @@ -235,7 +235,7 @@ process_event_analysis <- sparseyn = "N", pctsyn = "Y" ) - + hl_summ <- ae_counts |> filter_events(hterm, ht_val, "BYVAR1") |> mutate( @@ -249,18 +249,18 @@ process_event_analysis <- toupper(.data$DPTVAL), toupper(lt_val) ), 9999, rank(.data$PCT))) |> ungroup() - + stopifnot("No data available for higher terms" = nrow(hl_summ) > 0) - + ll_summ <- ae_counts |> filter_events(lterm, lt_val, "DPTVAL") |> mutate( Percent = .data$CPCT ) |> arrange(.data$TRTVAR, .data$PCT) - + stopifnot("No data available for preferred terms" = nrow(ll_summ) > 0) - + list(hl_summ, ll_summ) |> set_names(c("query_df", "pt_df")) |> map(\(x) { @@ -333,7 +333,7 @@ query_plot <- unique(df[["LVAL"]]), pt_color ) - + df |> mutate(across("DPTVAL", ~ toupper(.x))) |> ggplot( @@ -435,7 +435,7 @@ get_yscales <- function(df) { .keep = "none" ) |> pull() - + list(ymax = ymax, ybreak = ifelse(ymax > 40, 5, 2)) } diff --git a/R/graph_utils.R b/R/graph_utils.R index f73818f..63029d9 100644 --- a/R/graph_utils.R +++ b/R/graph_utils.R @@ -30,8 +30,8 @@ reverselog_trans <- function(base = exp(1)) { trans <- function(x) -log(x, base) inv <- function(x) base^(-x) scales::trans_new(paste0("reverselog-", format(base)), trans, inv, - scales::log_breaks(base = base), - domain = c(1e-100, Inf) + scales::log_breaks(base = base), + domain = c(1e-100, Inf) ) } @@ -63,10 +63,10 @@ g_seriescol <- function(gdata, "aquamarine1", "tan4", "skyblue1", "orchid3", "brown", "pink", "black" ) } - + # If not factor, convert it: if (!is.factor(gdata[[SERIESVAR]])) gdata[[SERIESVAR]] <- as.factor(gdata[[SERIESVAR]]) - + # Set names as factor levels levs <- levels(unique(gdata[[SERIESVAR]])) vals <- levs[levs %in% unique(gdata[[SERIESVAR]])] @@ -131,12 +131,12 @@ g_seriessym <- function(gdata, # Standard shapes shapelist <- c(16, 17, 15, 1, 18, 2, 0, 8, 10, 3, 4, 5) } - + # If not factor, convert it: if (!is.factor(gdata[[SERIESVAR]])) { gdata[[SERIESVAR]] <- as.factor(gdata[[SERIESVAR]]) } - + # Set names as factor levels # Set names as factor levels levs <- levels(unique(gdata[[SERIESVAR]])) @@ -165,8 +165,8 @@ empty_plot <- function(message = "No data available for these values", fontsize = 8) { g_plot <- ggplot() + annotate("text", - x = 1, y = 1, size = fontsize, - label = message + x = 1, y = 1, size = fontsize, + label = message ) + theme_void() fig <- ggplotly(g_plot, height = 200) |> @@ -241,37 +241,39 @@ def_axis_spec <- function(arg, vec, val) { #' ) #' plot_axis_opts <- - function(xlinearopts = list( - breaks = waiver(), - limits = NULL, - labels = waiver() - ), - ylinearopts = list( - breaks = waiver(), - limits = NULL, - labels = waiver() - ), - xaxis_scale = "identity", - yaxis_scale = "identity", - xaxis_label = "", - yaxis_label = "", - xopts = list( - labelsize = 12, - labelface = "plain", - ticksize = 8, - tickface = "plain", - angle = 0 - ), - yopts = list( - labelsize = 12, - labelface = "plain", - ticksize = 8, - tickface = "plain", - angle = 0 - )) { + function( + xlinearopts = list( + breaks = waiver(), + limits = NULL, + labels = waiver() + ), + ylinearopts = list( + breaks = waiver(), + limits = NULL, + labels = waiver() + ), + xaxis_scale = "identity", + yaxis_scale = "identity", + xaxis_label = "", + yaxis_label = "", + xopts = list( + labelsize = 12, + labelface = "plain", + ticksize = 8, + tickface = "plain", + angle = 0 + ), + yopts = list( + labelsize = 12, + labelface = "plain", + ticksize = 8, + tickface = "plain", + angle = 0 + ) + ) { stopifnot(is.list(xlinearopts)) stopifnot(is.list(ylinearopts)) - + list( Ybrks = def_axis_spec( arg = ylinearopts, diff --git a/R/line_plot.R b/R/line_plot.R index 5c6e784..8d5c8ae 100644 --- a/R/line_plot.R +++ b/R/line_plot.R @@ -87,7 +87,7 @@ line_plot <- function(datain, )) + geom_line(aes(color = .data[[series_var]])) + geom_point(aes(color = .data[[series_var]]), - shape = 16 + shape = 16 ) + labs( title = plot_title, diff --git a/R/mcatstat.R b/R/mcatstat.R index f4a059e..6c73602 100644 --- a/R/mcatstat.R +++ b/R/mcatstat.R @@ -247,9 +247,9 @@ mcatstat <- function(datain = NULL, DPTVAR = dptvars$vars, XVAR = .data[["DPTVAL"]], DPTVARN = dptvarn, CN = "C" ) |> select(any_of(c(BYVAR, "TRTVAR", SUBGRP, "DPTVAR", "DPTVAL", "CVALUE")), everything()) - + message("mcatstat success") - + df } @@ -282,7 +282,7 @@ calc_denom <- function(counts, stopifnot( "Invalid pctdisp" = str_remove(pctdisp, "[[:digit:]]+") %in% - c("TRT", "VAR", "COL", "SUBGRP", "SGRPN", "CAT", "NONE", "NO", "DPTVAR", "BYVARN") + c("TRT", "VAR", "COL", "SUBGRP", "SGRPN", "CAT", "NONE", "NO", "DPTVAR", "BYVARN") ) # Set denominator values for percentage if (pctdisp %in% c("NONE", "NO")) { @@ -295,15 +295,15 @@ calc_denom <- function(counts, df <- counts |> mutate(DENOMN = nrow(unique(data_denom[uniqid]))) } else { percgrp <- switch(gsub("[[:digit:]]", "", pctdisp), - "TRT" = "TRTVAR", - "CAT" = c(BYVAR, "DPTVAL"), - "COL" = c("TRTVAR", SUBGRP), - "SUBGRP" = c("TRTVAR", SUBGRP, BYVAR), - "SGRPN" = SUBGRP, - "DPTVAR" = c("TRTVAR", SUBGRP, BYVAR, "DPTVAL"), - "BYVARN" = c("TRTVAR", paste0("BYVAR", str_to_vec( - str_extract(pctdisp, "[[:digit:]]+"), "" - ))) + "TRT" = "TRTVAR", + "CAT" = c(BYVAR, "DPTVAL"), + "COL" = c("TRTVAR", SUBGRP), + "SUBGRP" = c("TRTVAR", SUBGRP, BYVAR), + "SGRPN" = SUBGRP, + "DPTVAR" = c("TRTVAR", SUBGRP, BYVAR, "DPTVAL"), + "BYVARN" = c("TRTVAR", paste0("BYVAR", str_to_vec( + str_extract(pctdisp, "[[:digit:]]+"), "" + ))) ) |> intersect(names(data_denom)) # Get denominator count per above variables df <- data_denom |> @@ -311,7 +311,7 @@ calc_denom <- function(counts, summarise(DENOMN = n_distinct(across(any_of(uniqid)))) |> inner_join(counts, by = percgrp, multiple = "all") } - + # Calculate percentage as PCT and concatenate as CVALUE p <- ifelse(pctsyn == "N", "", "%") # nolint df <- df |> diff --git a/R/mentry.R b/R/mentry.R index cfeb3db..391675c 100644 --- a/R/mentry.R +++ b/R/mentry.R @@ -188,10 +188,10 @@ create_grpvars <- function(dsin, vars, varN, new_var = "BYVAR", totalyn = "N", t grpvarN <- paste0(grpvar, "N") var <- vars[x] varN <- varN[x] - + df <- dsin |> mutate(!!grpvar := as.character(.data[[var]])) - + if (varN %in% names(df)) { df <- df |> mutate(!!grpvarN := .data[[varN]]) @@ -199,7 +199,7 @@ create_grpvars <- function(dsin, vars, varN, new_var = "BYVAR", totalyn = "N", t df <- df |> mutate(!!grpvarN := as.numeric(factor(.data[[var]]))) } - + if (totalyn == "Y") { df <- df |> bind_rows(mutate(df, !!grpvar := totlabel, !!grpvarN := 9999)) @@ -226,7 +226,7 @@ create_grpvars <- function(dsin, vars, varN, new_var = "BYVAR", totalyn = "N", t #' @noRd create_trtvar <- function(dsin, trtvar, trtsort, trttotalyn, trttotlabel = "Total", trtmissyn) { map <- c(trt = "TRTVAR", sort = "TRTSORT") - + df <- dsin |> mutate(!!unname(map["trt"]) := .data[[trtvar]]) # keep missing treatments in total if trtmissyn is given (always removed in post) @@ -238,7 +238,7 @@ create_trtvar <- function(dsin, trtvar, trtsort, trttotalyn, trttotlabel = "Tota if (is.na(trtsort) || str_squish(trtsort) == "") { trtsort <- trtvar } - + if (trtsort %in% names(df) && is.numeric(df[[trtsort]])) { df <- df |> mutate(!!unname(map["sort"]) := .data[[trtsort]]) @@ -246,16 +246,16 @@ create_trtvar <- function(dsin, trtvar, trtsort, trttotalyn, trttotlabel = "Tota df <- df |> mutate(!!unname(map["sort"]) := as.numeric(factor(.data[[trtsort]]))) } - + if (trttotalyn == "Y") { df <- df |> bind_rows(mutate(df, !!unname(map["trt"]) := trttotlabel, !!unname(map["sort"]) := 999)) } - + df |> mutate(!!unname(map["trt"]) := factor(.data[[unname(map["trt"])]], - levels = unique(.data[[unname(map["trt"])]][order(.data[[unname(map["sort"])]])]), - ordered = TRUE + levels = unique(.data[[unname(map["trt"])]][order(.data[[unname(map["sort"])]])]), + ordered = TRUE )) } @@ -279,7 +279,7 @@ sep_var_order <- function(vars, var_sep = "~", ord_sep = "/") { } sep_vars }) - + bind_cols(map(set_names(unique(names(flatten(var_list)))), \(col) { map_chr(var_list, \(df) pluck(df, col)) })) diff --git a/R/msumstat.R b/R/msumstat.R index 72e494c..aaab885 100644 --- a/R/msumstat.R +++ b/R/msumstat.R @@ -91,7 +91,7 @@ msumstat <- function(datain = NULL, } else { datapro <- datain } - + # Available and customized statistics if (is.null(statvar) || all(statvar == "")) { statinput <- c("n", "mean", "min", "median", "max", "sd") @@ -157,11 +157,11 @@ msumstat <- function(datain = NULL, # Tidy into long dataframe for use in tabular display data_long <- data_wide |> pivot_longer(all_of(tolower(statinput)), - names_to = "DPTVAL", - values_to = "CVALUE" + names_to = "DPTVAL", + values_to = "CVALUE" ) |> mutate(DPTVALN = as.numeric(fct_inorder(.data[["DPTVAL"]]))) - + message("msum success") return(list(tsum = data_long, gsum = data_wide)) } diff --git a/R/occ_tier_summary.R b/R/occ_tier_summary.R index 4702ba0..a04ec7e 100644 --- a/R/occ_tier_summary.R +++ b/R/occ_tier_summary.R @@ -264,7 +264,7 @@ occ_tier_summary <- function(datain, } h }) - + # Summary/Any Row Output if (sum_row != "N") { sum_data <- summary_row_cat( @@ -280,7 +280,7 @@ occ_tier_summary <- function(datain, } else { sum_data <- NULL } - + if ("PT_CNT" %in% names(datain)) { pt_data <- datain |> summary_row_cat( @@ -319,7 +319,7 @@ occ_tier_summary <- function(datain, ord_summ_df(sort_var, sort_opt) }) |> post_occ_tier(ctrlgrp = ctrlgrp, sum_row = sum_data, pt_row = pt_data, stathead = stathead) - + # To suppress any high term percentage counts, per variable htermctyn if (any(str_to_vec(htermctyn) == "N")) { htermctyn <- str_to_vec(htermctyn) @@ -371,7 +371,7 @@ post_occ_tier <- final_cts <- final_cts |> mutate( DPTVAL = ifelse(.data[["DPTVALN"]] == 0, - .data[["DPTVAL"]], paste0("\t\t\t", .data[["DPTVAL"]]) + .data[["DPTVAL"]], paste0("\t\t\t", .data[["DPTVAL"]]) ), SUBGRPVARX = paste0(stathead, strrep(" ", as.numeric(as.factor(.data[[repvar]])))), SUBGRPVARXN = 1 @@ -409,7 +409,7 @@ ord_by_ht <- function(df, ctrlgrp) { filter(df[["hterm_summ"]], .data[["TRTVAR"]] != ctrlgrp), "DPTVAL" )) ) - + map(names(df), \(x) { match_var <- recode(x, "hterm_summ" = "DPTVAL", "lterm_summ" = "BYVAR1") df_out <- df[[x]] |> @@ -417,7 +417,7 @@ ord_by_ht <- function(df, ctrlgrp) { distinct() |> mutate(DPTVARN = match(.data[[match_var]], uniqHT)) |> filter(!is.na(.data[["DPTVARN"]])) - + if (x == "hterm_summ") { df_out <- df_out |> mutate(DPTVALN = 0) diff --git a/R/process_vx_bar_plot.R b/R/process_vx_bar_plot.R index de6cbc2..08b7f84 100644 --- a/R/process_vx_bar_plot.R +++ b/R/process_vx_bar_plot.R @@ -79,17 +79,17 @@ process_vx_bar_plot <- function(dataset_adsl, stopifnot(nrow(dataset_analysis) > 0) stopifnot(trtvar %in% toupper(names(dataset_adsl))) stopifnot("AVAL" %in% toupper(names(dataset_analysis))) - + if (!is.na(split_by) && str_squish(split_by) != "") { stopifnot(all(str_to_vec(split_by) %in% toupper(names(dataset_adsl)))) } - + adsl_out <- adsl_merge( dataset_adsl, adsl_subset, dataset_analysis ) - + mentry_out <- mentry( datain = adsl_out, subset = overall_subset, @@ -99,7 +99,7 @@ process_vx_bar_plot <- function(dataset_adsl, trtsort = trtsort, add_grpmiss = "N" ) - + mcatstat_out <- mcatstat( datain = mentry_out, a_subset = analysis_subset, @@ -112,7 +112,7 @@ process_vx_bar_plot <- function(dataset_adsl, mutate( YVAR = as.numeric(.data[[yvar]]), XVAR = factor(XVAR, - levels = unique(XVAR[order(DPTVALN)]) + levels = unique(XVAR[order(DPTVALN)]) ) ) plotdata <- plot_title_nsubj( @@ -121,6 +121,6 @@ process_vx_bar_plot <- function(dataset_adsl, var_start(mcatstat_out, "SUBGRP") ) |> plot_display_bign(mentry_out, bignyn = legendbign) - + return(plotdata) } diff --git a/R/ptly_utils.R b/R/ptly_utils.R index f62fa4d..794ec0a 100644 --- a/R/ptly_utils.R +++ b/R/ptly_utils.R @@ -49,10 +49,10 @@ plotly_legend <- function(fig, lg_pos %in% c("top", "bottom", "right", "left") ) opts <- switch(lg_pos, - "bottom" = list(0.3, -0.2, "left", "top", "right", "top"), - "top" = list(0.3, 1.1, "left", "bottom", "right", "bottom"), - "left" = list(-0.2, 0.8, "right", "top", "right", "bottom"), - "right" = list(1, 0.8, "left", "top", "left", "bottom") + "bottom" = list(0.3, -0.2, "left", "top", "right", "top"), + "top" = list(0.3, 1.1, "left", "bottom", "right", "bottom"), + "left" = list(-0.2, 0.8, "right", "top", "right", "bottom"), + "right" = list(1, 0.8, "left", "top", "left", "bottom") ) } # Legend title as annotation if exists diff --git a/R/risk_stat.R b/R/risk_stat.R index caaf95b..fdb739a 100644 --- a/R/risk_stat.R +++ b/R/risk_stat.R @@ -96,9 +96,9 @@ risk_stat <- ## getting equivalent data variable for given summary by selection summ_var <- recode(tolower(summary_by), - "participants" = "USUBJID", - "patients" = "USUBJID", - "events" = eventvar + "participants" = "USUBJID", + "patients" = "USUBJID", + "events" = eventvar ) ## get sort variables to apply sorting post risk statistics calculation if (sort_opt == "Alphabetical") { @@ -106,7 +106,7 @@ risk_stat <- } else { sort_var <- get_sort_var(sort_var) } - + id_vars <- c("BYVAR1", "DPTVAL") value_vars <- c("FREQ", "PCT", "DENOMN") mcat_out <- mcatstat( @@ -151,7 +151,7 @@ risk_stat <- if (nrow(rout) > 0) { rout <- mcat_out |> filter(.data[["TRTVAR"]] %in% c(ctrlgrp, trt) | - str_detect(.data[["TRTVAR"]], "Total")) |> + str_detect(.data[["TRTVAR"]], "Total")) |> left_join(rout, by = intersect(names(mcat_out), names(rout))) |> mutate(across(any_of(c(value_vars)), \(x) as.double(x)), TOTAL_N = DENOMN) |> filter(!.data[["RISK"]] %in% c(NA, Inf, NaN)) @@ -164,7 +164,7 @@ risk_stat <- risk_out <- risk_out |> ord_summ_df(sort_var, sort_opt, g_sort_by_ht) risk_out[["TRTVAR"]] <- factor(risk_out[["TRTVAR"]], - levels = trt_list, ordered = TRUE + levels = trt_list, ordered = TRUE ) if (hoveryn == "Y") { risk_out <- risk_hover_text(risk_out, summary_by, eventvar) @@ -274,7 +274,7 @@ calc_risk_stat <- ), nrow = 2 ) - + if (statistic == "Risk Difference") { risk_mat <- suppressWarnings(riskdiff_wald(risk_mat, conf.level = 1 - alpha)) @@ -335,7 +335,7 @@ riskdiff_wald <- "C.I." ) names(dimnames(wald)) <- c(names(dimnames(x))[1], cn2) - + rrs <- list( data = tmx, measure = wald, @@ -403,7 +403,7 @@ extract_riskstats <- function(risk_mat, statistic) { pval <- risk_mat$p.value[2, 3] low_ci <- risk_mat$measure[2, 2] upp_ci <- risk_mat$measure[2, 3] - + if (statistic == "Risk Difference") { out <- list( risk = 0 - risk, diff --git a/R/run_app.R b/R/run_app.R index b1a8d3a..7ef3a58 100644 --- a/R/run_app.R +++ b/R/run_app.R @@ -22,7 +22,8 @@ #' #' @return No return value, called to run the application. run_app <- function( - ...) { + ... +) { with_golem_options( app = shinyApp( ui = app_ui, diff --git a/R/scatter_plot.R b/R/scatter_plot.R index 2bb8244..fb31a40 100644 --- a/R/scatter_plot.R +++ b/R/scatter_plot.R @@ -140,10 +140,10 @@ scatter_plot <- stopifnot(series_var %in% names(datain)) stopifnot(length(series_opts$shape) == length(series_opts$color)) stopifnot(length(series_opts$size) == length(series_opts$color)) - + legend_label <- legend_opts$label series_labels <- series_leg_lab(datain, series_var, series_labelvar) - + g <- ggplot( datain, aes( @@ -246,15 +246,15 @@ process_vx_scatter_data <- if (!is.na(split_by) && str_squish(split_by) != "") { stopifnot(all(str_to_vec(split_by) %in% toupper(names(dataset_adsl)))) } - + adsl_sub <- adsl_merge( dataset_adsl, adsl_subset, dataset_analysis ) - + stopifnot(nrow(adsl_sub) > 0) - + mentry_df <- adsl_sub |> mentry( subset = analysis_subset, @@ -264,19 +264,19 @@ process_vx_scatter_data <- add_grpmiss = "N", pop_fil = "Overall Population" ) - + stopifnot(nrow(mentry_df) > 0) - + a_dsin <- mentry_df |> mutate(Vars = case_when( !!!parse_exprs(xvar) ~ "XVAR", !!!parse_exprs(yvar) ~ "YVAR" )) - + if (all(is.na(a_dsin[["Vars"]]))) { stop("`xvar/yvar` are invalid") } - + a_dsin_ <- pivot_wider( a_dsin, id_cols = c(SUBJID, TRTVAR, starts_with("SUBGRPVAR")), @@ -284,6 +284,6 @@ process_vx_scatter_data <- values_from = AVAL ) |> plot_display_bign(mentry_df, bignyn = legendbign) - + return(a_dsin_) } diff --git a/R/stat_utils.R b/R/stat_utils.R index 3e38400..2d83d18 100644 --- a/R/stat_utils.R +++ b/R/stat_utils.R @@ -167,7 +167,7 @@ summary_functions <- function(statvar, statdec) { #' @noRd whiskerlow <- function(x, na.rm = TRUE) { min(x[(x >= (quantile(x, 0.25, na.rm = TRUE) - 1.5 * IQR(x, na.rm = TRUE))) & - (x <= quantile(x, 0.25, na.rm = TRUE))], na.rm = na.rm) + (x <= quantile(x, 0.25, na.rm = TRUE))], na.rm = na.rm) } #' Upper Box Whiskers @@ -179,7 +179,7 @@ whiskerlow <- function(x, na.rm = TRUE) { #' @noRd whiskerup <- function(x, na.rm = TRUE) { max(x[(x <= (quantile(x, 0.75, na.rm = TRUE) + 1.5 * IQR(x, na.rm = TRUE))) & - (x >= quantile(x, 0.75, na.rm = TRUE))], na.rm = na.rm) + (x >= quantile(x, 0.75, na.rm = TRUE))], na.rm = na.rm) } #' Concatenate to create complex statistics diff --git a/R/surv_utils.R b/R/surv_utils.R index df78992..0f92fe6 100644 --- a/R/surv_utils.R +++ b/R/surv_utils.R @@ -60,7 +60,7 @@ surv_pre_processor <- function(dataset_adsl, stopifnot( "Please provide a valid Censoring variable" = censor_var %in% toupper(names(dataset_analysis)) ) - + if (!is.na(split_by) && str_squish(split_by) != "") { stopifnot(all(str_to_vec(split_by) %in% toupper(names(dataset_adsl)))) } @@ -80,8 +80,8 @@ surv_pre_processor <- function(dataset_adsl, subgrpvar = str_remove_all(split_by, " ") ) plot_display_bign(mentry_out, - mentry_data = mentry_out, - bignyn = "N" + mentry_data = mentry_out, + bignyn = "N" ) } @@ -94,7 +94,7 @@ surv_pre_processor <- function(dataset_adsl, #' pairwise_surv_stats <- function(datain) { pairs <- combn(sort(unique(datain[["TRTSORT"]])), 2) - + pair_stat <- map_chr(seq_len(ncol(pairs)), \(i) { trt_index <- pairs[, i] pair_data <- datain |> @@ -120,6 +120,6 @@ pairwise_surv_stats <- function(datain) { "HR ({trt_pair[1]} vs {trt_pair[2]}) = {HR}, 95% CI ({cil}, {ciu}), 2-sided p = {round_f(pval_2s, 4)}, 1-sided p = {pval_1s}" # nolint ) }) - + paste0(pair_stat, collapse = "\n") } diff --git a/R/tbl_display.R b/R/tbl_display.R index fafc907..c62bf04 100644 --- a/R/tbl_display.R +++ b/R/tbl_display.R @@ -90,22 +90,22 @@ tbl_processor <- function(datain, rep <- rep |> select( any_of(c(BYVAR, "DPTVAR", "DPTVAL", - "DPTVAL" = "STAT", "TRTVAR", SUBGRP, BYVARN, SUBGRPN, - "DPTVARN", "DPTVALN", "DPTVALN" = "STATN", "CVALUE", "CN", keepvars + "DPTVAL" = "STAT", "TRTVAR", SUBGRP, BYVARN, SUBGRPN, + "DPTVARN", "DPTVALN", "DPTVALN" = "STATN", "CVALUE", "CN", keepvars )) ) } - + # IF treatment, subgroup exists, pivot and perform operations if (any(c("TRTVAR", SUBGRP) %in% names(rep))) { # Workaround for pivot_wider to accept "duplicate" spanned column names (Total) rep <- rep |> mutate(across(any_of(SUBGRP), ~ - ifelse( - get(paste0(cur_column(), "N")) == 9999, - paste0(.x, paste(rep(" ", which(SUBGRP == cur_column())), collapse = "")), - .x - ))) |> + ifelse( + get(paste0(cur_column(), "N")) == 9999, + paste0(.x, paste(rep(" ", which(SUBGRP == cur_column())), collapse = "")), + .x + ))) |> arrange(across(any_of(c("TRTVAR", SUBGRPN)))) |> select(-any_of(SUBGRPN)) |> pivot_wider( @@ -125,7 +125,7 @@ tbl_processor <- function(datain, rep <- rep |> mutate(across( -any_of(c(BYVAR, "DPTVAR", "DPTVAL")) & where(is.character), ~ if_else(.data[["DPTVAL"]] %in% c("n", "nmiss", "nobs"), - gsub("^-$", "0", .x), .x + gsub("^-$", "0", .x), .x ) )) } @@ -146,8 +146,8 @@ tbl_processor <- function(datain, rep <- rep |> mutate( DPTVAL = ifelse(.data[["CN"]] == "N", - recode(.data[["DPTVAL"]], !!!statn), - .data[["DPTVAL"]] + recode(.data[["DPTVAL"]], !!!statn), + .data[["DPTVAL"]] ) ) } @@ -346,8 +346,8 @@ tbl_display <- function(datain, ))) tout <- rep |> flextable(col_keys = grep("DPTVARN|^CN$|DPTVALN", - names(rep), - invert = TRUE, value = TRUE + names(rep), + invert = TRUE, value = TRUE )) |> ftExtra::span_header("_") |> font(fontname = font, part = "all") |> @@ -395,4 +395,3 @@ empty_tbl <- function(text = "No participant meets the reporting criteria") { align(align = "center", part = "all") |> autofit() } - diff --git a/R/tornado_plot.R b/R/tornado_plot.R index 6a153c1..3750bce 100644 --- a/R/tornado_plot.R +++ b/R/tornado_plot.R @@ -89,7 +89,7 @@ tornado_plot <- function(datain, "XVAR Treatment values not in data" = all(c("XVAR", "trt_left", "trt_right") %in% names(datain)) ) - + # Tornado plot - Flipped left and right Bar plots to ref line at 0 g_plot <- datain |> ggplot(aes(x = XVAR)) + @@ -105,7 +105,7 @@ tornado_plot <- function(datain, width = bar_width ) + coord_flip() - + names(series_opts) <- rev(names(series_opts)) # Adding Labels, Breaks, Colors, Ticks, Themes g_plot + @@ -234,7 +234,7 @@ process_tornado_data <- "Given Subsets not present in Analysis Data" = nrow(mentry_out) != 0 ) - + # Summary analysis for tornado plot dataset mcatstat_out <- mcatstat( datain = mentry_out, @@ -246,21 +246,21 @@ process_tornado_data <- sparseyn = "N" ) |> (\(x) { mutate(x, - YVAR = as.numeric(PCT), - XVAR = factor(x$XVAR, - levels = rev(x |> - group_by(XVAR) |> - mutate(XVARPCTS = sum(as.numeric(PCT))) |> - arrange(desc(.data$XVARPCTS)) |> - distinct(XVAR) |> - pull(XVAR)) - ), - BYVAR1 = factor(x$BYVAR1, levels = rev(unique(x$BYVAR1))) + YVAR = as.numeric(PCT), + XVAR = factor(x$XVAR, + levels = rev(x |> + group_by(XVAR) |> + mutate(XVARPCTS = sum(as.numeric(PCT))) |> + arrange(desc(.data$XVARPCTS)) |> + distinct(XVAR) |> + pull(XVAR)) + ), + BYVAR1 = factor(x$BYVAR1, levels = rev(unique(x$BYVAR1))) ) |> pivot_wider(names_from = "TRTVAR", values_from = "YVAR") |> mutate(trt_left = !!sym(trt_left), trt_right = !!sym(trt_right)) })() - + # Dataset for tornado plot plot_title_nsubj( mentry_out, diff --git a/R/utils.R b/R/utils.R index 731362b..5099912 100644 --- a/R/utils.R +++ b/R/utils.R @@ -34,7 +34,7 @@ data_attrib <- function(datain) { names(datain), function(x) { ifelse(is.null(attr(datain[[x]], "label")), - x, attr(datain[[x]], "label") + x, attr(datain[[x]], "label") ) } )) @@ -194,17 +194,17 @@ split_section_headers <- function(datain, split_by <- var_start(datain, split_by_prefix) stopifnot("No variables with split_by_prefix" = length(split_by) > 0) } - + if (split_lab != "") { split_lab <- str_to_vec(split_lab) } else { split_lab <- NA_character_ } - + header_list <- datain |> group_by(!!!syms(split_by)) |> group_keys() - + map(seq_along(split_by), \(x) { header_list |> mutate( @@ -267,14 +267,14 @@ dataset_vignette <- function(df = NULL, disp_vars = NULL, subset = NA_character_ out <- out |> filter(!!!parse_exprs(subset)) } - + if (!is.null(disp_vars)) { hide_columns <- which(!(colnames(out) %in% str_to_vec(disp_vars))) cols_to_hide <- list(list(targets = hide_columns - 1, visible = FALSE)) } else { cols_to_hide <- list() } - + DT::datatable( out, rownames = FALSE, @@ -324,7 +324,7 @@ add_bigN <- function(data, dsin, grpvar, modvar, subjid = "USUBJID") { if (length(modvar) == 1 && is.factor(data[[modvar]])) { newvar <- paste0(modvar, "_BIGN") data[[newvar]] <- factor(data[[newvar]], - levels = unique(data[[newvar]][order(data[[modvar]])]) + levels = unique(data[[newvar]][order(data[[modvar]])]) ) } data @@ -395,13 +395,13 @@ display_bign_head <- function(datain, mutate(across(any_of(lastvar), ~ paste0(.x, colformat))) if (lastvar == "TRTVAR") { datain[["TRTVAR"]] <- factor(datain[["TRTVAR"]], - levels = unique(datain[["TRTVAR"]][ord]) + levels = unique(datain[["TRTVAR"]][ord]) ) } } } else { notrthead <- ifelse(any(c(trtbignyn, subbignyn) == "Y"), - paste0(notrthead, " (N = ", length(unique(mentry_data[["USUBJID"]])), ")"), notrthead + paste0(notrthead, " (N = ", length(unique(mentry_data[["USUBJID"]])), ")"), notrthead ) datain <- datain |> mutate(!!notrthead := as.character(.data[["CVALUE"]])) |> @@ -486,7 +486,7 @@ sparse_vals <- function(datain, df_exp <- data_sparse |> tidyr::expand(!!!rlang::syms(c(TRTVAR, SUBGRP)), tidyr::nesting(DPTVAL, DPTVALN)) |> left_join(distinct(data_sparse, across(any_of(starts_with(c("DPTVAL", "BYVAR"))))), - by = c("DPTVAL", "DPTVALN") + by = c("DPTVAL", "DPTVALN") ) } data_sparse <- ungroup(data_sparse) diff --git a/tests/testthat/test-adsl_r001.R b/tests/testthat/test-adsl_r001.R index f0c5c78..2d28a79 100644 --- a/tests/testthat/test-adsl_r001.R +++ b/tests/testthat/test-adsl_r001.R @@ -17,7 +17,7 @@ test_that("split_var_types works", { actual <- split_var_types(c("AGE-S", "RACE", "SEX")) expected <- list(num_vars = "AGE", cat_vars = c("RACE", "SEX"), all_vars = c("AGE", "RACE", "SEX")) - + actual_ <- split_var_types(c("AGE", "RACE", "SEX")) expected_ <- list( @@ -25,7 +25,7 @@ test_that("split_var_types works", { cat_vars = c("AGE", "RACE", "SEX"), all_vars = c("AGE", "RACE", "SEX") ) - + expect_identical(actual, expected) expect_identical(actual_, expected_) }) @@ -35,26 +35,26 @@ test_that("adsl_summary works as expected", { datain = mentry_df, vars = "AGEGR1~AGE-S~RACE" ) - + dataf <- adsl_sum |> display_bign_head(mentry_df) |> tbl_processor( dptlabel = str_to_vec("Age Group~Age~Race"), statlabel = str_to_vec("N~Range~Mean (SD)~Median~Interquartile Range") ) - + adsl_sum_ <- adsl_summary( datain = mentry_df, vars = "AGEGR1~AGE~RACE" ) - + dataf_ <- adsl_sum_ |> display_bign_head(mentry_df) |> tbl_processor( statlabel = str_to_vec("N~Range~Meansd~Median~IQR"), dptlabel = str_to_vec("Age Group~Age~Race") ) - + expect_false(identical(dataf, dataf_)) expect_true(nrow(dataf) < nrow(dataf_)) expect_snapshot(print(tibble::as_tibble(dataf), n = Inf, width = Inf)) @@ -66,14 +66,14 @@ test_that("adsl_summary gives returns correct summary statistics", { datain = mentry_df, vars = "AGEGR1~AGE-S~RACE" ) - + actual <- adsl_sum |> display_bign_head(mentry_df) |> tbl_processor( statlabel = str_to_vec("N~Range~Meansd~Median~IQR"), dptlabel = str_to_vec("Age Group~Age~Race") ) - + agegr1 <- adsl |> dplyr::filter(SAFFL == "Y") |> dplyr::select(dplyr::all_of(c("TRT01A", "SEX", "AGEGR1"))) |> @@ -81,7 +81,7 @@ test_that("adsl_summary gives returns correct summary statistics", { dplyr::summarise(N = n()) |> dplyr::filter(TRT01A == "Placebo") |> dplyr::pull(N) - + exp_agegr1 <- actual |> dplyr::filter(DPTVAR == "Age Group") |> dplyr::arrange(BYVAR1, DPTVAL) |> @@ -90,7 +90,7 @@ test_that("adsl_summary gives returns correct summary statistics", { \(x) as.integer(stringr::str_squish(stringr::str_sub(x, 1, 2))) )) |> dplyr::pull(dplyr::starts_with("Placebo")) - + age_stat <- adsl |> dplyr::filter(SAFFL == "Y") |> dplyr::select(dplyr::all_of(c("TRT01A", "SEX", "AGE"))) |> @@ -100,11 +100,11 @@ test_that("adsl_summary gives returns correct summary statistics", { ) |> dplyr::arrange(.data[["SEX"]]) |> dplyr::pull(Mean) - + exp_age_stat <- actual |> dplyr::filter(DPTVAL == "Meansd") |> dplyr::relocate(`Xanomeline Low Dose (N=84)`, .after = `Xanomeline High Dose (N=84)`) - + expect_identical(unique(actual[["DPTVAR"]]), c("Age Group", "Age", "Race")) expect_identical(agegr1, exp_agegr1) expect_identical(age_stat, unname(unlist(c(exp_age_stat[1, 7:9], exp_age_stat[2, 7:9])))) @@ -116,30 +116,30 @@ test_that("adsl_summary works with subsets", { vars = "AGEGR1~AGE-S~SEX~RACE", a_subset = "AGE<65~AGE>80~NA~NA" ) - + actual <- adsl_sum |> display_bign_head(mentry_df) |> tbl_processor( statlabel = str_to_vec("N~Range~Meansd~Median~IQR"), dptlabel = str_to_vec("Age Group~Age~Sex~Race") ) - + adsl_sum_ <- mentry_df |> adsl_summary( vars = "AGEGR1~AGE-S~SEX~RACE", denom_subset = "RACE=='WHITE'~NA~NA~NA" ) - + actual_ <- adsl_sum_ |> display_bign_head(mentry_df) |> tbl_processor( statlabel = str_to_vec("N~Range~Meansd~Median~IQR"), dptlabel = str_to_vec("Age Group~Age~Sex~Race") ) - + expect_snapshot(print(actual, n = Inf, width = Inf)) expect_snapshot(print(tibble::as_tibble(actual_), n = Inf, width = Inf)) - + expect_error( adsl_summary( datain = mentry_df, diff --git a/tests/testthat/test-ae_pre_processor.R b/tests/testthat/test-ae_pre_processor.R index 78417f3..f199538 100644 --- a/tests/testthat/test-ae_pre_processor.R +++ b/tests/testthat/test-ae_pre_processor.R @@ -22,7 +22,7 @@ test_that("Test Case 1: With standard arguments", { ) |> tidyr::drop_na(all_of("TRTSDT")) |> mutate(AEDECOD = if_else(is.na(.data[["AEDECOD"]]) & !is.na(.data[["ASTDT"]]), - "Not Yet Coded", .data[["AEDECOD"]] + "Not Yet Coded", .data[["AEDECOD"]] )) expect_named(actual, c("data", "a_subset")) expect_equal(actual$data, expected) diff --git a/tests/testthat/test-bar_plot.R b/tests/testthat/test-bar_plot.R index c02e944..6b57e1a 100644 --- a/tests/testthat/test-bar_plot.R +++ b/tests/testthat/test-bar_plot.R @@ -37,9 +37,9 @@ test_that("Test Case 1: bar_plot works with expected inputs", { griddisplay = "N", plot_title = NULL ) - + legendgroups <- unique(bar_out[["data"]][["TRTVAR"]]) - + expect_type(bar_out, "list") expect_true(is.ggplot(bar_out)) expect_equal(legendgroups, unique(bar_out[["data"]][["TRTVAR"]])) @@ -68,9 +68,9 @@ test_that("Test Case 2: bar_plot works with modified inputs", { griddisplay = "N", plot_title = NULL ) - + legendgroups <- unique(bar_out[["data"]][["TRTVAR"]]) - + expect_type(bar_out, "list") expect_equal(legendgroups, unique(bar_out[["data"]][["TRTVAR"]])) expect_true(nrow(bar_out$data) > 0) diff --git a/tests/testthat/test-edish_plot.R b/tests/testthat/test-edish_plot.R index 1001910..fcb1c89 100644 --- a/tests/testthat/test-edish_plot.R +++ b/tests/testthat/test-edish_plot.R @@ -25,8 +25,8 @@ dt_xvar <- process_edish_data( bili_paramcd = "L00021S" ) series_opts <- plot_aes_opts(pt_data, - series_size = c(2, 2), - series_shape = "circle~square" + series_size = c(2, 2), + series_shape = "circle~square" ) e_plot <- edish_plot( datain = pt_data, @@ -58,17 +58,17 @@ e_plot <- edish_plot( test_that("edish data Works with standard inputs", { actual_trt <- unique(pt_data$TRTVAR) - + expect_equal(levels(actual_trt), c("Drug1")) expect_equal(nrow(pt_data), 57) - + expect_error(process_edish_data( datain = merged_data, alt_paramcd = "L00030S", ast_paramcd = "L00028S", bili_paramcd = "wefewf" ), "Please provide valid PARAMCD") - + # test xvar for "ast/alt" expect_equal(dt_xvar$XVAR, dt_xvar$ast) }) @@ -85,8 +85,8 @@ test_that("edish_plot works with expected output", { "Peak Total Bilirubin (x ULN)" ) expect_true(is.ggplot(e_plot)) - - + + # plotly output comparison ptly <- edish_plot( datain = pt_data, diff --git a/tests/testthat/test-event_analysis.R b/tests/testthat/test-event_analysis.R index 582d155..9bd5744 100644 --- a/tests/testthat/test-event_analysis.R +++ b/tests/testthat/test-event_analysis.R @@ -52,6 +52,6 @@ test_that("Test Case 2: event_analysis_plot works with expected inputs", { pt_color = "royalblue3", interactive = "Y" ) - + expect_snapshot(plot$x$data) }) diff --git a/tests/testthat/test-graph_utils.R b/tests/testthat/test-graph_utils.R index c6cca5c..36b42d8 100644 --- a/tests/testthat/test-graph_utils.R +++ b/tests/testthat/test-graph_utils.R @@ -27,7 +27,7 @@ test_that("Case 1: Transformation works with expected input", { test_that("Case 1: Works with expected input", { trt_cols <- g_seriescol(ae_pre, "red~cyan~forestgreen~black~pink~green", "TRTVAR") - + # Correct number of levels and colors assigned expect_equal(unname(trt_cols), c("red", "cyan", "forestgreen")) # Names as expected @@ -40,7 +40,7 @@ test_that("Case 1: Works with expected input", { test_that("Case 2: Works with default", { trt_na <- g_seriescol(ae_pre, NA, "TRTVAR") # Default colors: - + expect_equal( unname(trt_na), c("firebrick2", "blue4", "forestgreen") @@ -72,7 +72,7 @@ test_that("Case 1: Works with expected input", { ae_pre, "triangle~circle~square~asterisk", "TRTVAR" ) - + # Correct number of levels and colors assigned expect_equal(unname(trt_shp), c(2, 1, 0)) # Names as expected @@ -80,7 +80,7 @@ test_that("Case 1: Works with expected input", { trt_shp, c("Placebo", "Xanomeline Low Dose", "Xanomeline High Dose") ) - + # Numeric Input: trt_shp1 <- g_seriessym( ae_pre, @@ -96,7 +96,7 @@ test_that("Case 1: Works with expected input", { test_that("Case 2: Works with default", { trt_nashp <- g_seriessym(ae_pre, NA, "TRTVAR") # Default colors: - + expect_equal( unname(trt_nashp), c(16, 17, 15) @@ -253,7 +253,7 @@ test_that("empty_plot works as expected", { actual <- empty_plot() static_label <- "No data available for these values" exp_ptly_obj <- actual$ptly$x$data - + expect_length(actual, 2) expect_length(actual$plot$data, 0) expect_equal(actual[["plot"]][["layers"]][[1]][["aes_params"]][["label"]], static_label) diff --git a/tests/testthat/test-km_plot.R b/tests/testthat/test-km_plot.R index 9616690..ce9c0f3 100644 --- a/tests/testthat/test-km_plot.R +++ b/tests/testthat/test-km_plot.R @@ -12,7 +12,7 @@ test_that("km_plot works with default options", { surv_df <- purrr::list_modify(p$data, survfit = NULL) p$theme$legend.background <- NULL fit <- purrr::list_modify(p[["data"]][["survfit"]][[1]], call = NULL, .Environment = NULL) - + expect_snapshot(print(tibble::as_tibble(surv_df), n = Inf, width = Inf)) expect_snapshot(p$theme) purrr::walk(p$labels, ~ expect_snapshot(.x)) @@ -37,7 +37,7 @@ test_that("km_plot works with different options", { surv_df <- purrr::list_modify(p$data, survfit = NULL) p$theme$legend.background <- NULL fit <- p[["data"]][["survfit"]][[1]] - + expect_snapshot(print(tibble::as_tibble(surv_df), n = Inf, width = Inf)) expect_snapshot(p$theme) purrr::walk(p$labels, ~ expect_snapshot(.x)) @@ -60,7 +60,7 @@ test_that("km_plot returns empty plot when `datain` is empty", { yaxis_label = "Probability of Progression Free Survival" ) ) - + expected <- empty_plot("No data available")[["plot"]] expect_identical(actual[["data"]], expected[["data"]]) expect_identical(actual[["mapping"]], expected[["mapping"]]) diff --git a/tests/testthat/test-mcatstat.R b/tests/testthat/test-mcatstat.R index 8cdd80d..5b9e8fb 100644 --- a/tests/testthat/test-mcatstat.R +++ b/tests/testthat/test-mcatstat.R @@ -99,26 +99,26 @@ test_that("Case 3: Unique ID and sign variation", { pctdisp = "TRT", pctsyn = "N" ) - + # All groups and order except actual count values should be equal expect_equal( m_subj |> select(-all_of(c("DENOMN", "FREQ", "PCT", "CVALUE", "CPCT"))), m_na |> select(-all_of(c("DENOMN", "FREQ", "PCT", "CVALUE", "CPCT"))) ) - + # Counts are different expect_false(setequal(unique(m_subj$DENOMN), unique(m_na$DENOMN))) - + # Check values (for denominator, should also apply to numerator) check_na <- ae_entry |> ungroup() |> group_by(TRTVAR) |> summarise(DENOMN = n()) |> ungroup() - + expect_equal(m_na |> distinct(across(all_of(c("TRTVAR", "DENOMN")))), - check_na, - ignore_attr = TRUE + check_na, + ignore_attr = TRUE ) expect_false(any(grep("%", m_subj$CVALUE))) }) @@ -130,17 +130,17 @@ test_that("Case 4: Percentage denominator variation and denomyn", { dptvar = "SEX", pctdisp = "ABC" ), "Invalid pctdisp") - + m_none <- mcatstat( datain = ad_entry, dptvar = "SEX", pctdisp = "NONE" ) - + # No percentage columns: expect_false(any(c("PCT", "DENOMN") %in% names(m_none))) expect_equal(as.character(m_none$FREQ), m_none$CVALUE) - + # Variable total as denominator m_var <- mcatstat( datain = ad_entry, @@ -162,7 +162,7 @@ test_that("Case 5: Cumulative Frequency and Total", { cum_ctyn = "Y" ) expect_equal(m_cum[, c("TRTVAR", "FREQ", "DPTVAL")], ad_cum) - + m_total <- mcatstat( datain = ad_entry, dptvar = "SEX", diff --git a/tests/testthat/test-mentry.R b/tests/testthat/test-mentry.R index 2ae3e43..6a1b048 100644 --- a/tests/testthat/test-mentry.R +++ b/tests/testthat/test-mentry.R @@ -17,15 +17,15 @@ test_that("Test Case:1 mentry works with the inputs given and returns the expect ) # it returns a dataframe expect_s3_class(data_out, "data.frame") - + # testing asubset filter expect_equal(unique(data_out$EFFFL), "Y") - + # testing population filter expect_equal(unique(data_out$SAFFL), "Y") # Treatment check expect_s3_class(data_out$TRTVAR, "factor") - + # testing missing logic in byvar and subgrpvar expect_equal( unique(data_out$BYVAR1), @@ -74,7 +74,7 @@ test_that("Total and Treatment Values", { ) chdata <- data_out |> filter(.data[["TRTVAR"]] %in% - c("NOT ASSIGNED", "SCREEN FAILURE", "SCRNFAIL", "NOTRT", "NOTASSGN")) + c("NOT ASSIGNED", "SCREEN FAILURE", "SCRNFAIL", "NOTRT", "NOTASSGN")) expect_equal(nrow(chdata), 0) exp <- data_out |> mutate( diff --git a/tests/testthat/test-msumstat.R b/tests/testthat/test-msumstat.R index 37ac8e4..3acc9d6 100644 --- a/tests/testthat/test-msumstat.R +++ b/tests/testthat/test-msumstat.R @@ -145,7 +145,7 @@ test_that("Test 5: Sparse by value", { trtsort = "TRT01AN", pop_fil = NA ) - + adsl_sum <- adsl_entry |> msumstat( dptvar = "AGE", diff --git a/tests/testthat/test-process_vx_bar_plot.R b/tests/testthat/test-process_vx_bar_plot.R index 7a923c1..afa2fd1 100644 --- a/tests/testthat/test-process_vx_bar_plot.R +++ b/tests/testthat/test-process_vx_bar_plot.R @@ -21,7 +21,7 @@ test_that("process_vs_bar_data works as expected", { expect_s3_class(bar_pre, "data.frame") actual_trt <- unique(bar_pre$TRTVAR) actual_subgrp <- unique(bar_pre$SUBGRPVAR1) - + expect_equal(actual_subgrp, sort(unique(vx_bar_data$adsl$SEX))) expect_equal(levels(actual_trt), c("Drug1", "Drug2", "Drug3")) expect_snapshot(print(bar_pre, width = Inf, n = Inf)) diff --git a/tests/testthat/test-risk_stat.R b/tests/testthat/test-risk_stat.R index e01f8d1..733ae5e 100644 --- a/tests/testthat/test-risk_stat.R +++ b/tests/testthat/test-risk_stat.R @@ -22,7 +22,7 @@ denom <- dsin1 |> freq <- dsin1 |> filter(TRTVAR %in% c("Placebo", "Xanomeline High Dose") & - eval(parse(text = ae_pre_process$a_subset))) |> + eval(parse(text = ae_pre_process$a_subset))) |> group_by(TRTVAR, AEBODSYS, AEDECOD) |> summarise(n = length(unique(USUBJID))) |> ungroup() @@ -35,12 +35,12 @@ mat <- matrix(c(2, 7, 3, 6), nrow = 2) test_that("Test Case 1: Check if the function gives expected statistic values", { risk <- suppressWarnings(epitools::riskratio.wald(mat, conf.level = 1 - 0.05, correction = TRUE)) - + risk_val <- round(risk$measure[2, 1], 3) pval <- round(risk$p.value[2, 3], 4) cil <- round(risk$measure[2, 2], 2) ciu <- round(risk$measure[2, 3], 2) - + expected <- exp |> filter(AEDECOD == "NAUSEA") |> mutate( @@ -50,7 +50,7 @@ test_that("Test Case 1: Check if the function gives expected statistic values", RISKCIU = ciu, PCT = (n * 100) / N ) - + risk_s <- risk_stat( datain = dsin1, a_subset = ae_pre_process$a_subset, @@ -64,7 +64,7 @@ test_that("Test Case 1: Check if the function gives expected statistic values", sort_opt = "Ascending", sort_var = "Count" ) - + actual <- risk_s |> rename(AEBODSYS = BYVAR1, AEDECOD = DPTVAL, N = TOTAL_N, n = FREQ) |> filter(AEDECOD == "NAUSEA") |> @@ -73,7 +73,7 @@ test_that("Test Case 1: Check if the function gives expected statistic values", n = as.integer(n) ) |> select(TRTVAR, N, AEBODSYS, AEDECOD, n, RISK, PVALUE, RISKCIL, RISKCIU, PCT) - + expect_equal(actual$RISK, expected$RISK) expect_equal(actual$PVALUE, expected$PVALUE) expect_equal(actual, expected, ignore_attr = TRUE) @@ -83,12 +83,12 @@ test_that("Test Case 1: Check if the function gives expected statistic values", test_that("Test Case 2: Check if the function works as expected for risk difference", { risk <- suppressWarnings(riskdiff_wald(mat, conf.level = 1 - 0.05)) - + risk_val <- round(risk$measure[2, 1], 3) pval <- round(risk$p.value[2, 3], 4) ciu <- round(risk$measure[2, 2], 4) cil <- round(risk$measure[2, 3], 4) - + expected <- exp |> filter(AEDECOD == "NAUSEA") |> mutate( @@ -96,7 +96,7 @@ test_that("Test Case 2: Check if the function works as expected for risk differe PVALUE = pval ) |> arrange(AEDECOD) - + risk_s <- risk_stat( datain = dsin1, a_subset = ae_pre_process$a_subset, @@ -108,7 +108,7 @@ test_that("Test Case 2: Check if the function works as expected for risk differe alpha = 0.05, sort_opt = "Alphabetical" ) - + actual <- risk_s |> rename(AEBODSYS = BYVAR1, AEDECOD = DPTVAL, N = TOTAL_N, n = FREQ) |> filter(AEDECOD == "NAUSEA") |> @@ -117,7 +117,7 @@ test_that("Test Case 2: Check if the function works as expected for risk differe n = as.integer(n) ) |> select(TRTVAR, N, AEBODSYS, AEDECOD, n, RISK, PVALUE) - + expected$RISK <- -1 * (expected$RISK) expect_equal(actual$RISK, expected$RISK) expect_equal(actual$PVALUE, expected$PVALUE) @@ -131,11 +131,11 @@ test_that("riskdiff_wald: check if the function works as expected", { non_evts <- 6 control_evts <- 3 cne <- 8 - + expected_output <- (control_evts / (control_evts + cne)) - (evts / (evts + non_evts)) actual <- suppressWarnings(riskdiff_wald(matrix(c(evts, control_evts, non_evts, cne), nrow = 2))) actual_output <- actual$measure[2, 1] - + expect_equal(actual_output, expected_output, ignore_attr = TRUE) }) @@ -155,7 +155,7 @@ test_that("risk_stat: returns empty data frame when cutoff is too high", { sort_opt = "Ascending", sort_var = "Count" ) - + expected <- data.frame(NULL) expect_identical(actual, expected) }) diff --git a/tests/testthat/test-stat_utils.R b/tests/testthat/test-stat_utils.R index e3ca7a4..050df75 100644 --- a/tests/testthat/test-stat_utils.R +++ b/tests/testthat/test-stat_utils.R @@ -45,21 +45,21 @@ test_that("custom_cox_ph works with expected error when given wrong inputs", { pvalue_decimal = 4 ) expect_match(cp_error$err, - regex = "Spline fit is singular, try with smaller degrees of freedom" + regex = "Spline fit is singular, try with smaller degrees of freedom" ) expect_equal(names(cp_error), c("out", "pval", "err")) - - + + dt <- data.frame( n = 100, timevar = rep(10, 100), cnsrvar = rep(0, 100), TRTVAR = sample(c("A", "B"), 100, replace = TRUE) ) - + new_cox <- custom_cox_ph(datain = dt) expect_match(new_cox$err, - regex = "Not enough data to fit the Proportional Hazards Regression Model" + regex = "Not enough data to fit the Proportional Hazards Regression Model" ) expect_equal(names(new_cox), c("out", "pval", "err")) }) @@ -136,23 +136,23 @@ test_that("Test summary_functions outputs", { test_that("Test Tukey's stats", { exp <- min(dist[(dist >= (quantile(dist, 0.25, na.rm = TRUE) - 1.5 * IQR(dist, na.rm = TRUE))) & - (dist <= quantile(dist, 0.25, na.rm = TRUE))], na.rm = TRUE) + (dist <= quantile(dist, 0.25, na.rm = TRUE))], na.rm = TRUE) expect_equal(whiskerlow(dist), exp) exp2 <- max(dist[(dist <= (quantile(dist, 0.75, na.rm = TRUE) + 1.5 * IQR(dist, na.rm = TRUE))) & - (dist >= quantile(dist, 0.75, na.rm = TRUE))], na.rm = TRUE) + (dist >= quantile(dist, 0.75, na.rm = TRUE))], na.rm = TRUE) expect_equal(whiskerup(dist), exp2) }) test_that("Test derv_stats", { actual <- msumstat(adsl, - dptvar = "AGE", - statvar = "stderr~mean(sd)", - sigdec = "2~3(2)" + dptvar = "AGE", + statvar = "stderr~mean(sd)", + sigdec = "2~3(2)" ) testout <- msumstat(adsl, - dptvar = "AGE", - statvar = "stderr~mean~sd", - sigdec = "2~3~2" + dptvar = "AGE", + statvar = "stderr~mean~sd", + sigdec = "2~3~2" ) expected <- testout$gsum |> dplyr::mutate(`mean(sd)` = paste0(.data[["mean"]], " (", .data[["sd"]], ")")) |> diff --git a/tests/testthat/test-surv_utils.R b/tests/testthat/test-surv_utils.R index fa873e0..3a3dc9a 100644 --- a/tests/testthat/test-surv_utils.R +++ b/tests/testthat/test-surv_utils.R @@ -20,7 +20,7 @@ data_list <- split_data_by_var( test_that("surv_pre_processor works as expected", { actual_trt <- unique(sh_pre$TRTVAR) actual_subgrp <- unique(data_list[[1]]$SUBGRPVAR1) - + expect_equal(actual_subgrp, unique(survival$adsl$SEX)) expect_equal(levels(actual_trt), c("Drug1", "Drug2", "Drug3")) expect_equal(levels(actual_trt), sort(as.character(actual_trt))) @@ -41,7 +41,7 @@ test_that("surv_pre_processor works as expected", { ), "Please provide a valid Censoring variable" ) - + expect_error( surv_pre_processor( dataset_adsl = survival$adsl, diff --git a/tests/testthat/test-tornado_plot.R b/tests/testthat/test-tornado_plot.R index bcd7315..69d5845 100644 --- a/tests/testthat/test-tornado_plot.R +++ b/tests/testthat/test-tornado_plot.R @@ -72,7 +72,7 @@ test_that("Test Case 2: tornado data works with expected inputs", { test_that("Test Case 3: tornado_plot works with expected inputs", { legendgroups <- unique(plot_out[["data"]][["BYVAR1"]]) - + expect_true(is.ggplot(plot_out)) expect_type(plot_out, "list") expect_equal(legendgroups, unique(plot_out[["data"]][["BYVAR1"]])) diff --git a/tests/testthat/test-utils.R b/tests/testthat/test-utils.R index 790b309..1953176 100644 --- a/tests/testthat/test-utils.R +++ b/tests/testthat/test-utils.R @@ -105,7 +105,7 @@ test_that("Test 2: Check for exceptions", { sep = " & " ), "No variables with split_by_prefix" ) - + expect_error( split_section_headers( datain = adsl_entry, @@ -157,7 +157,7 @@ test_that("Test 2: Check for exceptions", { split_by_prefix = "TSTVAR" ), "No variables with split_by_prefix" ) - + expect_error( split_data_by_var( datain = adsl_entry, @@ -177,9 +177,9 @@ test_that("ord_summ_df works as expected", { ord_summ_df("Sepal.Length", "Descending") expected <- iris |> arrange(desc(.data[["Sepal.Length"]])) - + expect_identical(actual, expected) - + actual1 <- iris |> ord_summ_df("Petal.Width", "Alphabetical") expected1 <- iris |> @@ -192,9 +192,9 @@ test_that("ord_summ_df works as expected", { group_by(across(any_of("BYVAR1"))) |> arrange(.data[["Sepal.Length"]], .by_group = TRUE) |> ungroup() - + expect_identical(actual, expected) - + expect_identical(actual1, expected1) expect_identical(actual2, expected2) }) @@ -203,12 +203,12 @@ test_that("get_sort_var works as expected", { actual <- map_chr(c("Count", "Percent", "RiskValue", "Alphabetical", "abc"), \(x) get_sort_var(x)) expected <- c("CTRL_N", "CTRL_PCT", "RISK", "DPTVAL", "abc") - + expect_identical(actual, expected) }) mcat_data <- mcatstat(adsl_entry, - dptvar = "RACE" + dptvar = "RACE" ) test_that("add_bigN works as expected", { @@ -251,7 +251,7 @@ test_that("display_bign_head works as expected", { select(-all_of("TRTVAR")) |> rename("TRTVAR" = "TRTVAR_BIGN") expect_equal(actual, exp, ignore_attr = TRUE) - + # Only treatment no subgrp actual1 <- display_bign_head( datain = ae_pre, From 5a0312aa27ba90ff09b02159c3e618961d22aa2c Mon Sep 17 00:00:00 2001 From: kallea03 Date: Wed, 8 Jan 2025 10:46:52 +0000 Subject: [PATCH 07/48] fixed adlb,edish,tornado,adsl test cases --- R/adlb_r301.R | 8 +- R/adsl_r001.R | 2 +- R/edish_plot.R | 6 +- R/stat_utils.R | 2 +- data/adae.rda | Bin 26550 -> 26807 bytes data/adlb.rda | Bin 448043 -> 449941 bytes tests/testthat/_snaps/adlb_r301.md | 171 +++++++++----- tests/testthat/_snaps/adsl_r001.md | 318 ++++++++++++++++++++++++++ tests/testthat/_snaps/edish_plot.md | 164 +++++++++++++ tests/testthat/_snaps/tornado_plot.md | 150 ++++++++++++ tests/testthat/test-adlb_r301.R | 22 +- tests/testthat/test-edish_plot.R | 19 +- tests/testthat/test-tornado_plot.R | 21 +- 13 files changed, 791 insertions(+), 92 deletions(-) create mode 100644 tests/testthat/_snaps/adsl_r001.md create mode 100644 tests/testthat/_snaps/edish_plot.md create mode 100644 tests/testthat/_snaps/tornado_plot.md diff --git a/R/adlb_r301.R b/R/adlb_r301.R index 893a192..3269ca7 100644 --- a/R/adlb_r301.R +++ b/R/adlb_r301.R @@ -24,9 +24,9 @@ #' @export #' #' @examples -#' data("lab_data") +#' data("adlb") #' -#' lb_entry <- lab_data$adlb |> +#' lb_entry <- adlb |> #' mentry( #' subset = NA_character_, #' byvar = "PARCAT1~PARAM", @@ -42,7 +42,7 @@ #' out <- #' lb_entry |> #' lab_abnormality_summary( -#' crit_vars = "CRIT3~CRIT4", +#' crit_vars = "CRIT1~CRIT2", #' pctdisp = "SUBGRP", #' a_subset = NA_character_, #' denom_subset = NA_character_, @@ -61,7 +61,7 @@ #' ) #' lab_abnormality_summary <- function(datain, - crit_vars = "CRIT3~CRIT4", + crit_vars = "CRIT1~CRIT2", pctdisp = "SUBGRP", a_subset = NA_character_, denom_subset = NA_character_, diff --git a/R/adsl_r001.R b/R/adsl_r001.R index c52f7cd..3d3cf38 100644 --- a/R/adsl_r001.R +++ b/R/adsl_r001.R @@ -279,7 +279,7 @@ adsl_summary <- function(datain, #' @noRd #' split_var_types <- function(vars) { - num_vars <- vars[str_which(vars, "-S")] + num_vars <- vars[stringr::str_which(vars, "-S")] list( num_vars = str_replace_all(num_vars, "-S", ""), diff --git a/R/edish_plot.R b/R/edish_plot.R index 184738a..a5b3ade 100644 --- a/R/edish_plot.R +++ b/R/edish_plot.R @@ -51,9 +51,9 @@ #' process_edish_data <- function(datain, xvar = "both", - alt_paramcd = "L00030S", - ast_paramcd = "L00028S", - bili_paramcd = "L00021S", + alt_paramcd = "ALT", + ast_paramcd = "AST", + bili_paramcd = "BILI", legendbign = "Y") { if (!is.data.frame(datain) || nrow(datain) == 0) { return(data.frame()) diff --git a/R/stat_utils.R b/R/stat_utils.R index 3e38400..1feb3b6 100644 --- a/R/stat_utils.R +++ b/R/stat_utils.R @@ -80,7 +80,7 @@ parse_stats <- function(statvar, statdec) { filter(Stat == statvar[s]) |> pull(base) |> str_to_vec("/") - d <- unlist(str_extract_all(statdec[s], "[0-9]+")) + d <- unlist(stringr::str_extract_all(statdec[s], "[0-9]+")) last <- d[length(d)] length(d) <- length(st) d[is.na(d)] <- last diff --git a/data/adae.rda b/data/adae.rda index 35000472be04b03a59a0ce551ddd32b6c82b666f..0ef5f9c10daa98d3db91ee8b7e74488d4db1e118 100644 GIT binary patch literal 26807 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 literal 26550 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�ZKhK#xfs*q8>B?_Nl(kcZ{8>08 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 EfVe(;=l}o! diff --git a/data/adlb.rda b/data/adlb.rda index 98e403c142881228cc40059d3d2ec7be663ef1cc..4be02b6ddc525d03ae83f2f327fd1481731bdca2 100644 GIT binary patch literal 449941 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