| 1 |
#' @title Create TableTree as DataFrame via gentlg |
|
| 2 |
#' |
|
| 3 |
#' @param tt (`TableTree`)\cr TableTree object to convert to a data frame |
|
| 4 |
#' @param fontspec (`font_spec`)\cr Font specification object |
|
| 5 |
#' @param string_map (`list`)\cr Unicode mapping for special characters |
|
| 6 |
#' @param markup_df (`data.frame`)\cr Data frame containing markup information |
|
| 7 |
#' @param round_type (`character(1)`)\cr the type of rounding to perform. |
|
| 8 |
#' See [formatters::format_value()] for more details. |
|
| 9 |
#' @param validate logical(1). Whether to validate the table structure using |
|
| 10 |
#' `rtables::validate_table_struct()`. Defaults to `TRUE`. If `FALSE`, a message |
|
| 11 |
#' will be displayed instead of stopping with an error when validation fails. |
|
| 12 |
#' @return `tt` represented as a `tbl` data.frame suitable for passing |
|
| 13 |
#' to [tidytlg::gentlg] via the `huxme` argument. |
|
| 14 |
#' @export |
|
| 15 |
tt_to_tbldf <- function( |
|
| 16 |
tt, |
|
| 17 |
fontspec = font_spec("Times", 9L, 1),
|
|
| 18 | ||
| 19 |
string_map = default_str_map, |
|
| 20 |
markup_df = dps_markup_df, |
|
| 21 |
round_type = obj_round_type(tt), |
|
| 22 |
validate = TRUE |
|
| 23 |
) {
|
|
| 24 | 53x |
if (validate) {
|
| 25 | 51x |
if (!validate_table_struct(tt)) {
|
| 26 | 2x |
stop( |
| 27 | 2x |
"Invalid table structure detected. summarize_row_groups without ", |
| 28 | 2x |
"analyze below it in layout structure?" |
| 29 |
) |
|
| 30 |
} |
|
| 31 |
} else {
|
|
| 32 | 2x |
if (!validate_table_struct(tt)) {
|
| 33 | 1x |
message( |
| 34 | 1x |
"Invalid table structure detected. This may cause issues in the output. ", |
| 35 | 1x |
"The validation process failed, proceed with caution." |
| 36 |
) |
|
| 37 |
} else {
|
|
| 38 | 1x |
message( |
| 39 | 1x |
"Table structure validation succeeded. You should not need to set validate=FALSE." |
| 40 |
) |
|
| 41 |
} |
|
| 42 |
} |
|
| 43 | 51x |
mpf <- matrix_form( |
| 44 | 51x |
tt, |
| 45 | 51x |
indent_rownames = FALSE, |
| 46 | 51x |
expand_newlines = FALSE, |
| 47 | 51x |
fontspec = fontspec, |
| 48 | 51x |
round_type = round_type |
| 49 |
) |
|
| 50 | ||
| 51 | 51x |
strmat <- mf_strings(mpf) |
| 52 | 51x |
strmat[, 1] <- gsub("^[[:space:]]+", "", strmat[, 1])
|
| 53 | 51x |
nhl <- mf_nlheader(mpf) |
| 54 | 51x |
strbody <- strmat[-(1:nhl), , drop = FALSE] |
| 55 | 51x |
row_type <- apply( |
| 56 | 51x |
strbody, |
| 57 | 51x |
1, |
| 58 | 51x |
function(x) {
|
| 59 | 320x |
nonempty <- nzchar(x) |
| 60 | 320x |
if (all(!nonempty)) {
|
| 61 |
## shouldn't ever happen |
|
| 62 | ! |
"EMPTY" |
| 63 | 320x |
} else if (nonempty[1] && all(!nonempty[-1])) {
|
| 64 |
# only 1st cell nonempty |
|
| 65 | 15x |
"HEADER" |
| 66 |
} else {
|
|
| 67 | 305x |
if (x[1] == "N") {
|
| 68 | ! |
"N" |
| 69 |
} else {
|
|
| 70 | 305x |
"VALUE" |
| 71 |
} |
|
| 72 |
} |
|
| 73 |
} |
|
| 74 |
) |
|
| 75 | 51x |
rowdf <- mf_rinfo(mpf) |
| 76 | 51x |
rinds <- mf_lgrouping(mpf)[-(1:nhl)] - |
| 77 | 51x |
mf_nrheader(mpf) |
| 78 | 51x |
indentme <- rowdf$indent[rinds] |
| 79 | 51x |
anbr <- cumsum(!is.na(c(0, utils::head(rowdf$trailing_sep, -1))))[rinds] + 1 ## so it starts at 1 |
| 80 | 51x |
roworder <- seq_len(NROW(rowdf)) - (anbr - 1) |
| 81 | 51x |
newrows <- c(0, ifelse(utils::tail(anbr, -1) == utils::head(anbr, -1), 0, 1)) |
| 82 | ||
| 83 | 51x |
strbody <- prep_strs_for_rtf(strbody, string_map, markup_df) |
| 84 | 51x |
tbldf <- as.data.frame(strbody) |
| 85 | ||
| 86 | 51x |
names(tbldf) <- c("label", paste("col", seq_len(ncol(tbldf) - 1)))
|
| 87 | 51x |
emptycols <- tbldf[1, ] |
| 88 | 51x |
emptycols[1, ] <- "" |
| 89 | ||
| 90 | 51x |
tbldf <- cbind( |
| 91 | 51x |
tbldf, |
| 92 | 51x |
data.frame( |
| 93 | 51x |
row_type = row_type, |
| 94 | 51x |
anbr = anbr, |
| 95 | 51x |
indentme = indentme, |
| 96 | 51x |
roworder = roworder, |
| 97 | 51x |
newrows = newrows, |
| 98 | 51x |
stringsAsFactors = FALSE |
| 99 |
) |
|
| 100 |
) |
|
| 101 | 51x |
tbldf |
| 102 |
} |
|
| 103 | ||
| 104 | ||
| 105 |
gutter_width <- .12 # inches |
|
| 106 | ||
| 107 |
## this is for Times New Roman 9 |
|
| 108 |
mar_plus_gutters <- 2 + gutter_width |
|
| 109 | ||
| 110 |
pg_width_by_orient <- function(landscape = FALSE) {
|
|
| 111 | 65x |
fullpg <- ifelse(landscape, 11, 8.5) |
| 112 | 65x |
fullpg - mar_plus_gutters |
| 113 |
} |
|
| 114 | ||
| 115 |
get_colwidths_as_proportions <- function(colwidths, tlgtype, label_width_ins, pg_width) {
|
|
| 116 | 45x |
if (tlgtype == "Table") {
|
| 117 | 41x |
colwidths <- cwidths_final_adj( |
| 118 | 41x |
labwidth_ins = label_width_ins, |
| 119 | 41x |
total_width = pg_width, |
| 120 | 41x |
colwidths = colwidths[-1] |
| 121 |
) |
|
| 122 |
} |
|
| 123 | 45x |
colwidths <- colwidths / sum(colwidths) |
| 124 | 45x |
if (sum(colwidths) > 1) {
|
| 125 | ! |
colwidths <- colwidths - .Machine$double.eps ## much smaller than a twip = 1/20 printing point |
| 126 |
} |
|
| 127 | 45x |
return(colwidths) |
| 128 |
} |
|
| 129 | ||
| 130 |
get_output_csv_filename <- function(output_csv_directory, fpath, fname) {
|
|
| 131 | 6x |
if (is.null(output_csv_directory)) {
|
| 132 | 4x |
output_csv_filename <- file.path(fpath, paste0(tolower(fname), ".csv")) |
| 133 | 2x |
} else if (!dir.exists(output_csv_directory)) {
|
| 134 | 1x |
output_csv_filename <- file.path(fpath, paste0(tolower(fname), ".csv")) |
| 135 | 1x |
message("Output dir for csv ", output_csv_directory,
|
| 136 | 1x |
" does not exist; csv will be saved as ", |
| 137 | 1x |
output_csv_filename) |
| 138 |
} else {
|
|
| 139 | 1x |
output_csv_filename <- file.path(output_csv_directory, |
| 140 | 1x |
paste0(tolower(fname), ".csv")) |
| 141 | 1x |
message("Saving csv as ", output_csv_filename)
|
| 142 |
} |
|
| 143 | 6x |
return(output_csv_filename) |
| 144 |
} |
|
| 145 | ||
| 146 |
tlg_type <- function(tt) {
|
|
| 147 | 64x |
if (methods::is(tt, "list") && !methods::is(tt, "listing_df")) {
|
| 148 | ! |
tt <- tt[[1]] |
| 149 |
} |
|
| 150 | 64x |
if (methods::is(tt, "ggplot")) {
|
| 151 | ! |
ret <- "Figure" |
| 152 | 64x |
} else if (methods::is(tt, "VTableTree")) {
|
| 153 | 53x |
ret <- "Table" |
| 154 | 11x |
} else if (methods::is(tt, "listing_df")) {
|
| 155 | 10x |
ret <- "Listing" |
| 156 |
} else {
|
|
| 157 | 1x |
stop( |
| 158 | 1x |
"unable to determine tlg type for object of class: ", |
| 159 | 1x |
paste(class(tt), collapse = ",") |
| 160 |
) |
|
| 161 |
} |
|
| 162 | 63x |
ret |
| 163 |
} |
|
| 164 | ||
| 165 |
mpf_to_colspan <- function( |
|
| 166 |
mpf, |
|
| 167 |
string_map = default_str_map, |
|
| 168 |
markup_df = dps_markup_df |
|
| 169 |
) {
|
|
| 170 | 45x |
if (!methods::is(mpf, "MatrixPrintForm")) {
|
| 171 | ! |
stop("figure out how to make this an mpf (MatrixPrintForm) first.")
|
| 172 |
} |
|
| 173 | 45x |
strmat <- mf_strings(mpf) |
| 174 |
## spaces in column header will get underlines, but we dont' want them to |
|
| 175 | 45x |
nhl <- mf_nlheader(mpf) |
| 176 | 45x |
strmat[seq_len(nhl - 1), ] <- gsub( |
| 177 | 45x |
"^[[:space:]]+$", |
| 178 |
"", |
|
| 179 | 45x |
strmat[seq_len(nhl - 1), ] |
| 180 |
) |
|
| 181 | 45x |
strmat[nhl, ] <- ifelse(nzchar(strmat[nhl, ]), strmat[nhl, ], " ") |
| 182 | 45x |
strmat <- prep_strs_for_rtf(strmat, string_map, markup_df) |
| 183 | 45x |
nspancols <- nhl - 1L |
| 184 | 45x |
if (nspancols > 0) {
|
| 185 | 20x |
csph <- lapply(seq_len(nspancols), function(ii) {
|
| 186 | 28x |
spns <- mf_spans(mpf)[ii, ] |
| 187 | 28x |
vals <- strmat[ii, ] |
| 188 | 28x |
jj <- 1 ## could start at 2 cause 1 is row label/topleft space but why complicate things |
| 189 | 28x |
ind <- 1 |
| 190 | 28x |
myrle <- rle(vals) |
| 191 | 28x |
while (jj < length(vals)) {
|
| 192 | 62x |
if (myrle$lengths[ind] == 1) {
|
| 193 | 31x |
jj <- jj + 1 |
| 194 | 31x |
ind <- ind + 1 |
| 195 |
} else {
|
|
| 196 | 31x |
sq <- seq(jj, jj + myrle$lengths[ind] - 1) |
| 197 | 31x |
valspns <- spns[sq] |
| 198 | 31x |
if (valspns[1] != length(sq)) {
|
| 199 | ! |
vals[sq] <- split_val_spans(vals[jj], valspns) |
| 200 |
} |
|
| 201 | 31x |
jj <- tail(sq, 1) + 1 |
| 202 | 31x |
ind <- ind + 1 |
| 203 |
} |
|
| 204 |
} |
|
| 205 | 28x |
vals |
| 206 |
}) |
|
| 207 |
} else {
|
|
| 208 | 25x |
csph <- NULL |
| 209 |
} |
|
| 210 | ||
| 211 | 45x |
list(colspan = csph, colheader = strmat[nhl, ]) |
| 212 |
} |
|
| 213 | ||
| 214 |
split_val_spans <- function(val, spns) {
|
|
| 215 | ! |
if (val == "") {
|
| 216 | ! |
return(rep(val, length(spns))) |
| 217 |
} |
|
| 218 | ! |
padvec <- rep("", length(spns))
|
| 219 | ! |
j <- 1 |
| 220 | ! |
padnum <- 0 |
| 221 | ! |
while (j < length(spns)) {
|
| 222 | ! |
onespn <- spns[j] |
| 223 | ! |
if (padnum > 0) {
|
| 224 | ! |
padvec[j:(j + onespn - 1)] <- strrep(" ", padnum)
|
| 225 |
} |
|
| 226 | ! |
padnum <- padnum + 1 |
| 227 | ! |
j <- j + onespn |
| 228 |
} |
|
| 229 | ! |
paste0(padvec, val, padvec) |
| 230 |
} |
|
| 231 | ||
| 232 |
partmpf_to_colinds <- function(fullmpf, partmpf) {
|
|
| 233 | 37x |
stopifnot( |
| 234 | 37x |
mf_nlheader(fullmpf) == mf_nlheader(partmpf), |
| 235 | 37x |
mf_ncol(fullmpf) >= mf_ncol(partmpf) |
| 236 |
) |
|
| 237 | 37x |
nlh <- mf_nlheader(fullmpf) |
| 238 | 37x |
fullhdr <- mf_strings(fullmpf)[1:nlh, , drop = FALSE] |
| 239 | 37x |
parthdr <- mf_strings(partmpf)[1:nlh, , drop = FALSE] |
| 240 | 37x |
fullkeys <- apply(fullhdr, 2, paste, collapse = "", simplify = TRUE) |
| 241 |
## hack to force first key to match uniquely |
|
| 242 | 37x |
fullkeys[1] <- paste(c("xxx", fullkeys[-1]), collapse = "")
|
| 243 | 37x |
stopifnot(length(fullkeys) == length(unique(fullkeys))) |
| 244 | 37x |
partkeys <- apply(parthdr, 2, paste, collapse = "", simplify = TRUE) |
| 245 | 37x |
partkeys[1] <- fullkeys[1] |
| 246 | 37x |
match(partkeys, fullkeys) |
| 247 |
} |
|
| 248 | ||
| 249 |
subset_border_mat <- function(full_brdr, full_mpf, part_mpf) {
|
|
| 250 | 37x |
if (is.null(full_brdr)) {
|
| 251 | ! |
return(NULL) |
| 252 |
} |
|
| 253 | 37x |
inds <- partmpf_to_colinds(full_mpf, part_mpf) |
| 254 | 37x |
full_brdr[, inds, drop = FALSE] |
| 255 |
} |
|
| 256 | ||
| 257 | ||
| 258 |
get_ncol <- function(tt) {
|
|
| 259 | 6x |
if (is(tt, "listing_df") || is(tt, "VTableTree")) {
|
| 260 | 3x |
ncol(tt) |
| 261 | 3x |
} else if (is(tt, "MatrixPrintForm")) {
|
| 262 | 1x |
mf_ncol(tt) |
| 263 | 2x |
} else if (is.list(tt)) {
|
| 264 | 2x |
if (is(tt[[1]], "MatrixPrintForm")) {
|
| 265 | 1x |
mf_ncol(tt[[1]]) |
| 266 |
} else {
|
|
| 267 | 1x |
ncol(tt[[1]]) |
| 268 |
} |
|
| 269 |
} |
|
| 270 |
} |
|
| 271 | ||
| 272 |
# apply format and round_type to columns of a listing_df object and return as a dataframe |
|
| 273 |
# only for usage as input to gentlg |
|
| 274 |
listingdf_dataframe_formats <- function(df, round_type = obj_round_type(df)) {
|
|
| 275 | 4x |
if (!is(df, "listing_df")) {
|
| 276 | ! |
return(df) |
| 277 |
} else {
|
|
| 278 | 4x |
cols <- listing_dispcols(df) |
| 279 | 4x |
df[cols] <- lapply(names(df), function(col) {
|
| 280 | 28x |
fmt <- obj_format(df[[col]]) |
| 281 | 28x |
if (!is.null(fmt)) {
|
| 282 | 2x |
na_str <- obj_na_str(df[[col]]) |
| 283 | 2x |
lbl <- obj_label(df[[col]]) |
| 284 | ||
| 285 | 2x |
df[[col]] <- sapply(df[[col]], FUN = function(x) {
|
| 286 | 12x |
format_value(x, format = fmt, na_str = na_str, round_type = round_type) |
| 287 |
}) |
|
| 288 | ||
| 289 | 2x |
obj_label(df[[col]]) <- lbl |
| 290 |
} |
|
| 291 | 28x |
df[[col]] |
| 292 |
}) |
|
| 293 |
} |
|
| 294 | ||
| 295 | 4x |
class(df) <- "data.frame" |
| 296 | 4x |
df |
| 297 |
} |
|
| 298 | ||
| 299 |
#' @name tt_to_tlgrtf |
|
| 300 |
#' @title TableTree to .rtf Conversion |
|
| 301 |
#' @description |
|
| 302 |
#' A function to convert TableTree to .rtf |
|
| 303 |
#' @details |
|
| 304 |
#' This function aids in converting the rtables TableTree into the desired .rtf file. |
|
| 305 |
#' |
|
| 306 |
#' @param tt (`TableTree`)\cr TableTree object to convert to RTF |
|
| 307 |
#' @param file (`character(1)`)\cr File to create, including path, but excluding |
|
| 308 |
#' .rtf extension. |
|
| 309 |
#' @param orientation (`character`)\cr Orientation of the output ("portrait" or "landscape")
|
|
| 310 |
#' @param colwidths (`numeric` vector)\cr Column widths for the table |
|
| 311 |
#' @param label_width_ins (`numeric`)\cr Label width in inches |
|
| 312 |
#' @param pagenum (`logical`)\cr Whether to add page numbers to the output. |
|
| 313 |
#' Only applicable to listings (i.e. it is ignored for tables and figures). |
|
| 314 |
#' @param fontspec (`font_spec`)\cr Font specification object |
|
| 315 |
#' @param pg_width (`numeric`)\cr Page width in inches |
|
| 316 |
#' @param margins (`numeric` vector)\cr Margins in inches (top, right, bottom, left) |
|
| 317 |
#' @param paginate (`logical`)\cr Whether to paginate the output |
|
| 318 |
#' @param col_gap (`numeric`)\cr Column gap in spaces |
|
| 319 |
#' @param verbose (`logical`)\cr Whether to print verbose output |
|
| 320 |
#' @param tlgtype (`character`)\cr Type of the output (Table, Listing, or Figure) |
|
| 321 |
#' @param string_map (`data.frame`)\cr Unicode mapping for special characters |
|
| 322 |
#' @param markup_df (`data.frame`)\cr Data frame containing markup information |
|
| 323 |
#' @param ... Additional arguments passed to gentlg |
|
| 324 |
#' @inheritParams tidytlg::gentlg |
|
| 325 |
#' @param nosplitin (`list`)\cr list(row=, col=). Path elements whose children should not be paginated within |
|
| 326 |
#' if it can be avoided. e.g., list(col="TRT01A") means don't split within treatment arms unless |
|
| 327 |
#' all the associated columns don't fit on a single page. |
|
| 328 |
#' @param combined_rtf (`logical(1)`)\cr In the case where the result is broken up into multiple |
|
| 329 |
#' parts due to width, should a combined rtf file also be created. Defaults to `FALSE`. |
|
| 330 |
#' @param one_table (`logical(1)`)\cr If `tt` is a (non-`MatrixPrintForm`) list, |
|
| 331 |
#' should the parts be added to the rtf within a single table (`TRUE`, the |
|
| 332 |
#' default) or as separate tables. End users will not generally need to set this. |
|
| 333 |
#' @param border_mat (`matrix`)\cr A `m x k` matrix where m is the number of columns of `tt` |
|
| 334 |
#' and k is the number of lines the header takes up. See [tidytlg::add_bottom_borders] |
|
| 335 |
#' for what the matrix should contain. Users should only specify this when the |
|
| 336 |
#' default behavior does not meet their needs. |
|
| 337 |
#' @param alignments (`list`)\cr List of named lists. Vectorized. |
|
| 338 |
#' (Default = `list()`) Used to specify individual column or cell alignments. |
|
| 339 |
#' Each named list contains `row`, `col`, and `value`, which are passed to |
|
| 340 |
#' [huxtable::set_align()] to set the alignments. |
|
| 341 |
#' @param round_type (`character(1)`)\cr the type of rounding to perform. |
|
| 342 |
#' See [formatters::format_value()] for more details. |
|
| 343 |
#' @param validate (`logical(1)`)\cr Whether to validate the table structure using |
|
| 344 |
#' `rtables::validate_table_struct()`. Defaults to `TRUE`. If `FALSE`, a message |
|
| 345 |
#' will be displayed when validation fails. |
|
| 346 |
#' @param export_csv (`logical(1)`)\cr Whether to export the object as a csv representation. |
|
| 347 |
#' Default = FALSE. |
|
| 348 |
#' @param output_csv_directory (`character(1)`)\cr the directory to export the csv. |
|
| 349 |
#' Default = NULL. Ignored if export_csv = FALSE. |
|
| 350 |
#' If NULL or attempting to export in a non-existent directory, the csv will be exported |
|
| 351 |
#' in the same directory as the .rtf file. |
|
| 352 |
#' @import rlistings |
|
| 353 |
#' @rdname tt_to_tlgrtf |
|
| 354 |
#' @export |
|
| 355 |
#' @note `file` should always include path. Path will be extracted |
|
| 356 |
#' and passed separately to `gentlg`. |
|
| 357 |
#' @note When `one_table` is `FALSE`, only the width of the row label |
|
| 358 |
#' pseudocolumn can be directly controlled due to a limitation in |
|
| 359 |
#' `tidytlg::gentlg`. The proportion of the full page that the first value |
|
| 360 |
#' in colwidths would take up is preserved and all other columns equally |
|
| 361 |
#' split the remaining available width. This will cause, e.g., the |
|
| 362 |
#' elements within the allparts rtf generated when `combined_rtf` is `TRUE` |
|
| 363 |
#' to differ visually from the content of the individual part rtfs. |
|
| 364 |
#' @return If `file` is non-NULL, this is called for the side-effect of writing |
|
| 365 |
#' one or more RTF files. Otherwise, returns a list of `huxtable` objects. |
|
| 366 |
tt_to_tlgrtf <- function( |
|
| 367 |
tt, |
|
| 368 |
file = NULL, |
|
| 369 |
orientation = c("portrait", "landscape"),
|
|
| 370 |
colwidths = def_colwidths( |
|
| 371 |
tt, |
|
| 372 |
fontspec, |
|
| 373 |
col_gap = col_gap, |
|
| 374 |
label_width_ins = label_width_ins, |
|
| 375 |
type = tlgtype |
|
| 376 |
), |
|
| 377 |
label_width_ins = 2, |
|
| 378 |
watermark = NULL, |
|
| 379 |
pagenum = ifelse(tlgtype == "Listing", TRUE, FALSE), |
|
| 380 |
fontspec = font_spec("Times", 9L, 1.2),
|
|
| 381 |
pg_width = pg_width_by_orient(orientation == "landscape"), |
|
| 382 |
margins = c(0, 0, 0, 0), |
|
| 383 |
paginate = tlg_type(tt) == "Table", |
|
| 384 |
col_gap = ifelse(tlgtype == "Listing", .5, 3), |
|
| 385 |
nosplitin = list( |
|
| 386 |
row = character(), |
|
| 387 |
col = character() |
|
| 388 |
), |
|
| 389 |
verbose = FALSE, |
|
| 390 |
tlgtype = tlg_type(tt), |
|
| 391 |
string_map = default_str_map, |
|
| 392 |
markup_df = dps_markup_df, |
|
| 393 |
combined_rtf = FALSE, |
|
| 394 |
one_table = TRUE, |
|
| 395 |
border_mat = make_header_bordmat(obj = tt), |
|
| 396 |
round_type = obj_round_type(tt), |
|
| 397 |
alignments = list(), |
|
| 398 |
validate = TRUE, |
|
| 399 |
export_csv = FALSE, |
|
| 400 |
output_csv_directory = NULL, |
|
| 401 |
... |
|
| 402 |
) {
|
|
| 403 | ||
| 404 | 70x |
checkmate::assert_flag(export_csv) |
| 405 | 70x |
checkmate::assert_character(output_csv_directory, null.ok = TRUE, len = 1) |
| 406 | ||
| 407 | 70x |
if (validate && tlgtype == "Table" && methods::is(tt, "VTableTree")) {
|
| 408 | 25x |
if (!rtables::validate_table_struct(tt)) {
|
| 409 | 2x |
message( |
| 410 | 2x |
"Invalid table structure detected. This may cause issues in the output. ", |
| 411 | 2x |
"The validation process failed, proceed with caution." |
| 412 |
) |
|
| 413 |
} |
|
| 414 | 44x |
} else if (!validate && tlgtype == "Table" && methods::is(tt, "VTableTree")) {
|
| 415 | 2x |
if (rtables::validate_table_struct(tt)) {
|
| 416 | 1x |
message( |
| 417 | 1x |
"Table structure validation succeeded. You should not need to set validate=FALSE." |
| 418 |
) |
|
| 419 |
} |
|
| 420 |
} |
|
| 421 | ||
| 422 | 69x |
if (tlgtype != "Listing") {
|
| 423 | 65x |
pagenum <- FALSE |
| 424 |
} |
|
| 425 | 69x |
orientation <- match.arg(orientation) |
| 426 | 69x |
newdev <- open_font_dev(fontspec) |
| 427 | 69x |
if (newdev) {
|
| 428 | 31x |
on.exit(close_font_dev()) |
| 429 |
} |
|
| 430 | ||
| 431 | 69x |
if (tlgtype == "Listing" && nrow(tt) == 0) {
|
| 432 | 1x |
dat <- as.list(c("No data to report", rep("", ncol(tt) - 1)))
|
| 433 | 1x |
names(dat) <- names(tt) |
| 434 | 1x |
df <- as.data.frame(dat) |
| 435 | 1x |
var_labels(df) <- var_labels(tt) |
| 436 | ||
| 437 | 1x |
tt <- as_listing( |
| 438 | 1x |
df, |
| 439 | 1x |
key_cols = get_keycols(tt), |
| 440 | 1x |
disp_cols = listing_dispcols(tt), |
| 441 | 1x |
main_title = attr(tt, "main_title"), |
| 442 | 1x |
main_footer = attr(tt, "main_footer") |
| 443 |
) |
|
| 444 |
} |
|
| 445 | ||
| 446 | 69x |
if (tlgtype == "Table" && fontspec$size == 8) {
|
| 447 | 2x |
opts <- options( |
| 448 | 2x |
tidytlg.fontsize.table.footnote = 8, |
| 449 | 2x |
tidytlg.fontsize.table = 8 |
| 450 |
) |
|
| 451 | 2x |
on.exit(options(opts), add = TRUE) |
| 452 |
} |
|
| 453 | ||
| 454 | 69x |
if (length(colwidths) == 1) {
|
| 455 | 1x |
nc <- get_ncol(tt) |
| 456 | 1x |
tot_width <- page_lcpp( |
| 457 | 1x |
pg_width = pg_width, |
| 458 | 1x |
pg_height = 20, # don't care about this, |
| 459 | 1x |
font_family = fontspec$family, |
| 460 | 1x |
font_size = fontspec$size, |
| 461 | 1x |
lineheight = fontspec$lineheight, |
| 462 | 1x |
margins = c(0, 0, 0, 0), |
| 463 | 1x |
landscape = orientation == "landscape" |
| 464 | 1x |
)$cpp |
| 465 | 1x |
wdth_left <- tot_width - colwidths |
| 466 | 1x |
colwidths <- c(colwidths, rep(floor(wdth_left / nc), nc)) |
| 467 |
} |
|
| 468 | ||
| 469 | 69x |
max_lwidth <- inches_to_spaces(label_width_ins, fontspec) |
| 470 | 69x |
if (colwidths[1] > max_lwidth) {
|
| 471 | 2x |
colwidths[1] <- max_lwidth |
| 472 |
} |
|
| 473 | ||
| 474 | 69x |
if (!requireNamespace("tidytlg")) {
|
| 475 | ! |
stop("tidytlg not installed, cannot go out to rtf.")
|
| 476 |
} |
|
| 477 | ||
| 478 | 69x |
if (paginate) {
|
| 479 |
## implies type Table |
|
| 480 | 26x |
if (tlgtype != "Table") {
|
| 481 | ! |
stop( |
| 482 | ! |
"pagination is not currently supported for tlg types other than Table." |
| 483 |
) |
|
| 484 |
} |
|
| 485 | 26x |
if (methods::is(tt, "VTableTree")) {
|
| 486 | 26x |
hdrmpf <- matrix_form(tt[1, , keep_topleft = TRUE], round_type = round_type) |
| 487 | ! |
} else if (methods::is(tt, "list") && methods::is(tt[[1]], "MatrixPrintForm")) {
|
| 488 | ! |
hdrmpf <- tt[[1]] |
| 489 |
} else {
|
|
| 490 | ! |
hdrmpf <- tt |
| 491 |
} |
|
| 492 | ||
| 493 | 26x |
pags <- paginate_to_mpfs( |
| 494 | 26x |
tt, |
| 495 | 26x |
fontspec = fontspec, |
| 496 | 26x |
landscape = orientation == "landscape", |
| 497 | 26x |
colwidths = colwidths, |
| 498 | 26x |
col_gap = col_gap, |
| 499 | 26x |
pg_width = pg_width, |
| 500 | 26x |
pg_height = NULL, |
| 501 | 26x |
margins = margins, |
| 502 | 26x |
lpp = NULL, |
| 503 | 26x |
nosplitin = nosplitin, |
| 504 | 26x |
verbose = verbose, |
| 505 | 26x |
round_type = round_type |
| 506 |
) ## |
|
| 507 | 26x |
if (has_force_pag(tt)) {
|
| 508 | 1x |
nslices <- which( |
| 509 | 1x |
cumsum(vapply(pags, mf_ncol, 1L)) == ncol(tt) |
| 510 |
) |
|
| 511 | 1x |
oldpags <- pags |
| 512 | 1x |
pags <- lapply( |
| 513 | 1x |
seq_len(nslices), |
| 514 | 1x |
function(ii) {
|
| 515 | 6x |
inds <- seq(ii, by = nslices, length.out = length(oldpags) / nslices) |
| 516 | 6x |
oldpags[inds] |
| 517 |
} |
|
| 518 |
) |
|
| 519 |
} |
|
| 520 | 26x |
pag_bord_mats <- lapply( |
| 521 | 26x |
seq_along(pags), |
| 522 | 26x |
function(i) {
|
| 523 | 37x |
if (methods::is(pags[[i]], "MatrixPrintForm")) {
|
| 524 | 31x |
partmpf <- pags[[i]] |
| 525 |
} else {
|
|
| 526 | 6x |
partmpf <- pags[[i]][[1]] |
| 527 |
} |
|
| 528 | 37x |
subset_border_mat(border_mat, hdrmpf, partmpf) |
| 529 |
} |
|
| 530 |
) |
|
| 531 | 26x |
ret <- lapply( |
| 532 | 26x |
seq_along(pags), |
| 533 | 26x |
function(i) {
|
| 534 | 37x |
if (!is.null(file) && length(pags) > 1) {
|
| 535 | 12x |
fmti <- paste0("%0", ceiling(log(length(pags), base = 10)), "d")
|
| 536 | 12x |
fname <- paste0(file, "part", sprintf(fmti, i), "of", length(pags)) |
| 537 |
} else {
|
|
| 538 | 25x |
fname <- file |
| 539 |
} |
|
| 540 | 37x |
full_pag_i <- pags[[i]] |
| 541 | 26x |
if ( |
| 542 | 37x |
is.list(full_pag_i) && |
| 543 | 37x |
!methods::is(full_pag_i, "MatrixPrintForm") |
| 544 |
) {
|
|
| 545 | 6x |
pgi_for_cw <- full_pag_i[[1]] |
| 546 |
} else {
|
|
| 547 | 31x |
pgi_for_cw <- full_pag_i |
| 548 |
} |
|
| 549 | 37x |
tt_to_tlgrtf( |
| 550 | 37x |
full_pag_i, |
| 551 | 37x |
file = fname, |
| 552 | 37x |
orientation = orientation, |
| 553 | 37x |
colwidths = formatters::mf_col_widths(pgi_for_cw), |
| 554 | 37x |
fontspec = fontspec, |
| 555 | 37x |
watermark = watermark, |
| 556 | 37x |
col_gap = col_gap, |
| 557 | 37x |
paginate = FALSE, |
| 558 | 37x |
tlgtype = tlgtype, |
| 559 | 37x |
string_map = string_map, |
| 560 | 37x |
markup_df = markup_df, |
| 561 | 37x |
border_mat = pag_bord_mats[[i]], |
| 562 | 37x |
round_type = round_type, |
| 563 | 37x |
alignments = alignments, |
| 564 | 37x |
export_csv = export_csv, |
| 565 | 37x |
output_csv_directory = output_csv_directory, |
| 566 | 37x |
label_width_ins = label_width_ins, |
| 567 |
... |
|
| 568 |
) |
|
| 569 |
} |
|
| 570 |
) |
|
| 571 | 26x |
if (combined_rtf) {
|
| 572 | 1x |
if (length(pags) > 1) {
|
| 573 | 1x |
tt_to_tlgrtf( |
| 574 | 1x |
pags, |
| 575 | 1x |
file = paste0(file, "allparts"), |
| 576 | 1x |
orientation = orientation, |
| 577 | 1x |
fontspec = fontspec, |
| 578 | 1x |
watermark = watermark, |
| 579 | 1x |
col_gap = col_gap, |
| 580 | 1x |
paginate = FALSE, |
| 581 | 1x |
tlgtype = "Table", |
| 582 | 1x |
string_map = string_map, |
| 583 | 1x |
markup_df = markup_df, |
| 584 | 1x |
one_table = FALSE, |
| 585 |
## gentlg isn't vectorized on column widths so we're SOL here... |
|
| 586 | 1x |
colwidths = colwidths, ## this is largely ignored see note in docs |
| 587 |
# colwidths are already on the pags since they are mpfs |
|
| 588 | 1x |
border_mat = pag_bord_mats, |
| 589 | 1x |
round_type = round_type, |
| 590 | 1x |
alignments = alignments, |
| 591 | 1x |
export_csv = export_csv, |
| 592 | 1x |
output_csv_directory = output_csv_directory, |
| 593 | 1x |
label_width_ins = label_width_ins, |
| 594 |
... |
|
| 595 |
) |
|
| 596 | ! |
} else if (!is.null(file)) { # only one page after pagination
|
| 597 | ! |
message( |
| 598 | ! |
"Table ", |
| 599 | ! |
basename(file), |
| 600 | ! |
": No combined RTF created, output fit within one part." |
| 601 |
) |
|
| 602 |
} |
|
| 603 |
} |
|
| 604 | 26x |
if (is.null(file) && length(pags) > 1) {
|
| 605 | 1x |
ret <- unlist(ret, recursive = FALSE) |
| 606 |
} |
|
| 607 | 26x |
return(ret) |
| 608 |
} |
|
| 609 | ||
| 610 | 43x |
if (tlgtype == "Table") {
|
| 611 | 39x |
if (is.list(tt) && !methods::is(tt, "MatrixPrintForm")) {
|
| 612 | 7x |
df <- lapply( |
| 613 | 7x |
tt, |
| 614 | 7x |
tt_to_tbldf, |
| 615 | 7x |
fontspec = fontspec, |
| 616 | 7x |
string_map = string_map, |
| 617 | 7x |
markup_df = markup_df, |
| 618 | 7x |
round_type = round_type |
| 619 |
) |
|
| 620 | 7x |
if (one_table) {
|
| 621 | 6x |
df <- do.call( |
| 622 | 6x |
rbind, |
| 623 | 6x |
lapply( |
| 624 | 6x |
seq_along(df), |
| 625 | 6x |
function(ii) {
|
| 626 | 12x |
dfii <- df[[ii]] |
| 627 | 12x |
dfii$newpage <- 0 |
| 628 | 12x |
if (ii > 1) {
|
| 629 | 6x |
dfii$newpage[1] <- 1 |
| 630 |
} |
|
| 631 | 12x |
dfii$indentme <- ifelse(dfii$indentme <= 1, 0, dfii$indentme - 1) |
| 632 | 12x |
dfii |
| 633 |
} |
|
| 634 |
) |
|
| 635 |
) |
|
| 636 |
} |
|
| 637 |
} else {
|
|
| 638 | 32x |
df <- tt_to_tbldf( |
| 639 | 32x |
tt, |
| 640 | 32x |
fontspec = fontspec, |
| 641 | 32x |
string_map = string_map, |
| 642 | 32x |
markup_df = markup_df, |
| 643 | 32x |
round_type = round_type |
| 644 |
) |
|
| 645 |
} |
|
| 646 |
} else {
|
|
| 647 | 4x |
df <- tt[, listing_dispcols(tt)] |
| 648 |
# apply formats and round_type and return df as a dataframe to input in gentlg |
|
| 649 | 4x |
df <- listingdf_dataframe_formats(df, round_type = round_type) |
| 650 |
} |
|
| 651 | ||
| 652 |
## we only care about the col labels here... |
|
| 653 | 43x |
if (tlgtype == "Table" && is.list(tt) && !methods::is(tt, "MatrixPrintForm")) {
|
| 654 | 7x |
mpf <- tt[[1]] |
| 655 | 7x |
if (!one_table) {
|
| 656 | 1x |
colinfo <- lapply( |
| 657 | 1x |
tt, |
| 658 | 1x |
mpf_to_colspan, |
| 659 | 1x |
markup_df = markup_df, |
| 660 | 1x |
string_map = string_map |
| 661 |
) |
|
| 662 | 1x |
csph <- lapply(colinfo, function(x) x$colspan) |
| 663 | 1x |
colheader <- lapply(colinfo, function(x) x$colheader) |
| 664 |
} else {
|
|
| 665 | 6x |
colinfo <- mpf_to_colspan( |
| 666 | 6x |
mpf, |
| 667 | 6x |
markup_df = markup_df, |
| 668 | 6x |
string_map = string_map |
| 669 |
) |
|
| 670 | 6x |
csph <- colinfo$colspan |
| 671 | 6x |
colheader <- colinfo$colheader |
| 672 |
} |
|
| 673 | 36x |
} else if (methods::is(tt, "MatrixPrintForm")) {
|
| 674 | 31x |
mpf <- tt |
| 675 | 31x |
colinfo <- mpf_to_colspan( |
| 676 | 31x |
mpf, |
| 677 | 31x |
markup_df = markup_df, |
| 678 | 31x |
string_map = string_map |
| 679 |
) |
|
| 680 | 31x |
csph <- colinfo$colspan |
| 681 | 31x |
colheader <- colinfo$colheader |
| 682 |
} else {
|
|
| 683 | 5x |
mpf <- matrix_form( |
| 684 | 5x |
rtables::head(tt, 1), |
| 685 | 5x |
indent_rownames = FALSE, |
| 686 | 5x |
expand_newlines = FALSE, |
| 687 | 5x |
fontspec = fontspec, |
| 688 | 5x |
round_type = round_type |
| 689 |
) |
|
| 690 | 5x |
colinfo <- mpf_to_colspan( |
| 691 | 5x |
mpf, |
| 692 | 5x |
markup_df = markup_df, |
| 693 | 5x |
string_map = string_map |
| 694 |
) |
|
| 695 | 5x |
csph <- colinfo$colspan |
| 696 | 5x |
colheader <- colinfo$colheader |
| 697 |
} |
|
| 698 | ||
| 699 | 43x |
if (is.null(file)) {
|
| 700 | 12x |
fname <- NULL |
| 701 | 12x |
fpath <- tempdir() |
| 702 |
} else {
|
|
| 703 | 31x |
fname <- basename(file) |
| 704 |
## dirname on "table" returns "." so we're good using |
|
| 705 |
## this unconditionally as opath |
|
| 706 | 31x |
fpath <- dirname(file) |
| 707 |
} |
|
| 708 | ||
| 709 | 43x |
if (!one_table && # nolint start |
| 710 | 43x |
is.list(tt) && !is(tt, "MatrixPrintForm")) {
|
| 711 |
# this should be technically always 1 but just in case |
|
| 712 | 1x |
num_repeated_cols <- ncol(tt[[1]]$strings) - ncol(tt[[1]]) |
| 713 |
# the following lines will listify the vector colwidths, this is, convert it |
|
| 714 |
# to a list of vectors (one vector per page) |
|
| 715 | 1x |
l_colwidths <- list() |
| 716 | 1x |
j <- num_repeated_cols + 1 |
| 717 | 1x |
for (i in seq_along(tt)) {
|
| 718 | 3x |
subt_col_idxs <- j - 1 + seq(ncol(tt[[i]])) |
| 719 | 3x |
colwidths_subt <- colwidths[c(1:num_repeated_cols, subt_col_idxs)] |
| 720 |
## jump current col position to first column on next page |
|
| 721 | 3x |
j <- tail(subt_col_idxs, 1) + 1 |
| 722 | 3x |
l_colwidths[[i]] <- get_colwidths_as_proportions(colwidths_subt, |
| 723 | 3x |
tlgtype, |
| 724 | 3x |
label_width_ins, |
| 725 | 3x |
pg_width) |
| 726 |
} |
|
| 727 | 1x |
colwidths <- l_colwidths |
| 728 |
} else { # nolint end
|
|
| 729 | 42x |
colwidths <- get_colwidths_as_proportions(colwidths, |
| 730 | 42x |
tlgtype, |
| 731 | 42x |
label_width_ins, |
| 732 | 42x |
pg_width) |
| 733 |
} |
|
| 734 | ||
| 735 | ||
| 736 | 43x |
footer_val <- prep_strs_for_rtf( |
| 737 | 43x |
c( |
| 738 | 43x |
main_footer(mpf), |
| 739 | 43x |
prov_footer(mpf) |
| 740 |
), |
|
| 741 | 43x |
string_map, |
| 742 | 43x |
markup_df |
| 743 |
) |
|
| 744 | 43x |
if (length(footer_val) == 0) {
|
| 745 | 42x |
footer_val <- NULL |
| 746 |
} |
|
| 747 | ||
| 748 | 43x |
if (!is.null(fname) && tlgtype == "Table" && is.data.frame(df) && export_csv) {
|
| 749 | ||
| 750 | 6x |
output_csv_filename <- get_output_csv_filename(output_csv_directory, fpath, fname) |
| 751 | ||
| 752 | 6x |
utils::write.csv( |
| 753 | 6x |
df, |
| 754 | 6x |
file = output_csv_filename, |
| 755 | 6x |
row.names = FALSE |
| 756 |
) |
|
| 757 |
} |
|
| 758 | ||
| 759 | 43x |
tidytlg::gentlg( |
| 760 | 43x |
df, |
| 761 | 43x |
tlf = tlgtype, |
| 762 | 43x |
format = "rtf", |
| 763 | 43x |
idvars = if (tlgtype == "Listing") get_keycols(tt) else NULL, |
| 764 | 43x |
colspan = csph, |
| 765 | 43x |
file = fname, |
| 766 | 43x |
opath = fpath, |
| 767 | 43x |
colheader = colheader, |
| 768 | 43x |
title = prep_strs_for_rtf( |
| 769 | 43x |
main_title(mpf), |
| 770 | 43x |
string_map, |
| 771 | 43x |
markup_df |
| 772 |
), |
|
| 773 | 43x |
footers = footer_val, |
| 774 | 43x |
orientation = orientation, |
| 775 | 43x |
wcol = colwidths, |
| 776 | 43x |
watermark = watermark, |
| 777 | 43x |
pagenum = pagenum, |
| 778 | 43x |
bottom_borders = border_mat, |
| 779 | 43x |
print.hux = !is.null(fname), |
| 780 | 43x |
alignments = alignments, |
| 781 | 43x |
footers_one_row = TRUE, |
| 782 |
... |
|
| 783 |
) |
|
| 784 |
} |
|
| 785 | ||
| 786 |
## NB x/(x+sum(colwidths)) = labwidth_ins/total_width |
|
| 787 |
cwidths_final_adj <- function(labwidth_ins, total_width, colwidths) {
|
|
| 788 | 42x |
prop <- labwidth_ins / total_width |
| 789 | 42x |
lwidth <- floor(prop / (1 - prop) * sum(colwidths)) |
| 790 | 42x |
c(lwidth, colwidths) |
| 791 |
} |
|
| 792 | ||
| 793 |
make_bordmat_row <- function(rowspns) {
|
|
| 794 | 37x |
havespn <- rowspns > 1 |
| 795 | 37x |
if (!any(havespn)) {
|
| 796 | 30x |
return(rep(0, times = length(rowspns))) |
| 797 |
} |
|
| 798 | ||
| 799 | 7x |
pos <- 1 |
| 800 | 7x |
ngrp <- 1 |
| 801 | 7x |
ret <- numeric(length(rowspns)) |
| 802 | 7x |
while (pos < length(rowspns)) {
|
| 803 | 30x |
spnval <- rowspns[pos] |
| 804 | 30x |
if (spnval > 1) {
|
| 805 | 24x |
multipos <- seq(pos, pos + spnval - 1) |
| 806 | 24x |
val <- ngrp |
| 807 |
} else {
|
|
| 808 | 6x |
multipos <- pos |
| 809 | 6x |
val <- 0 |
| 810 |
} |
|
| 811 | 30x |
ret[multipos] <- val |
| 812 | 30x |
pos <- pos + spnval |
| 813 | 30x |
ngrp <- ngrp + 1 |
| 814 |
} |
|
| 815 | 7x |
ret |
| 816 |
} |
|
| 817 | ||
| 818 |
fixup_bord_mat <- function(brdmat, hstrs) {
|
|
| 819 |
## no lines between labels and their counts |
|
| 820 | 27x |
countcells <- matrix( |
| 821 | 27x |
grepl("N=", hstrs, fixed = TRUE),
|
| 822 | 27x |
nrow = nrow(hstrs), |
| 823 | 27x |
ncol = ncol(hstrs) |
| 824 |
) |
|
| 825 | ||
| 826 | 27x |
countcoords <- which(countcells, arr.ind = TRUE) |
| 827 | 27x |
for (i in seq_len(nrow(countcoords))) {
|
| 828 | 4x |
brdmat[countcoords[i, "row"] - 1, countcoords[i, "col"]] <- 0 |
| 829 |
} |
|
| 830 | ||
| 831 | 27x |
brdmat[!nzchar(hstrs) | hstrs == " "] <- 0 |
| 832 | 27x |
brdmat[nrow(brdmat), ] <- 1 |
| 833 | 27x |
brdmat[seq_len(nrow(brdmat) - 1), 1] <- 0 |
| 834 | 27x |
brdmat |
| 835 |
} |
|
| 836 | ||
| 837 |
.make_header_bordmat <- function( |
|
| 838 |
obj, |
|
| 839 |
mpf = matrix_form(utils::head(obj, 1), expand_newlines = FALSE) |
|
| 840 |
) {
|
|
| 841 | 27x |
spns <- mf_spans(mpf) |
| 842 | 27x |
nlh <- mf_nlheader(mpf) |
| 843 | 27x |
nrh <- mf_nrheader(mpf) |
| 844 | 27x |
stopifnot(nlh == nrh) |
| 845 | ||
| 846 | 27x |
hstrs <- mf_strings(mpf)[seq_len(nrh), , drop = FALSE] |
| 847 | 27x |
spns <- mf_spans(mpf)[seq_len(nrh), , drop = FALSE] |
| 848 | ||
| 849 | 27x |
brdmat <- do.call( |
| 850 | 27x |
rbind, |
| 851 | 27x |
lapply( |
| 852 | 27x |
seq_len(nrh), |
| 853 | 27x |
function(i) make_bordmat_row(spns[i, ]) |
| 854 |
) |
|
| 855 |
) |
|
| 856 | ||
| 857 | 27x |
brdmat <- fixup_bord_mat(brdmat, hstrs) |
| 858 | 27x |
brdmat |
| 859 |
} |
|
| 860 | ||
| 861 |
setGeneric( |
|
| 862 |
"make_header_bordmat", |
|
| 863 |
function(obj, |
|
| 864 |
mpf = matrix_form(utils::head(obj, 1), expand_newlines = FALSE)) {
|
|
| 865 | 58x |
standardGeneric("make_header_bordmat")
|
| 866 |
} |
|
| 867 |
) |
|
| 868 | ||
| 869 |
setMethod( |
|
| 870 |
"make_header_bordmat", |
|
| 871 |
c(obj = "ANY", mpf = "MatrixPrintForm"), |
|
| 872 |
.make_header_bordmat |
|
| 873 |
) |
|
| 874 | ||
| 875 |
setMethod( |
|
| 876 |
"make_header_bordmat", |
|
| 877 |
c(obj = "listing_df"), |
|
| 878 |
function(obj, mpf) matrix(1, nrow = 1, ncol = length(listing_dispcols(obj))) |
|
| 879 |
) |
|
| 880 | ||
| 881 |
setMethod( |
|
| 882 |
"make_header_bordmat", |
|
| 883 |
c(obj = "VTableTree", mpf = "missing"), |
|
| 884 |
function(obj, mpf) {
|
|
| 885 | 27x |
make_header_bordmat( |
| 886 | 27x |
mpf = matrix_form( |
| 887 | 27x |
utils::head(obj, 1), |
| 888 | 27x |
expand_newlines = FALSE |
| 889 |
) |
|
| 890 |
) |
|
| 891 |
} |
|
| 892 |
) |
| 1 |
# Proportion Difference ---- |
|
| 2 | ||
| 3 |
#' @name proportion_diff_mf |
|
| 4 |
#' |
|
| 5 |
#' @title Proportion Difference with Method Selection Based on the Mantel-Fleiss |
|
| 6 |
#' Criterion |
|
| 7 |
#' |
|
| 8 |
#' @description `r lifecycle::badge("stable")`
|
|
| 9 |
#' |
|
| 10 |
#' Performs an adaptive analysis of the difference in response proportions |
|
| 11 |
#' between a non-reference group and a reference group for stratified data, with |
|
| 12 |
#' the method selected based on the Mantel-Fleiss (MF) criterion. |
|
| 13 |
#' |
|
| 14 |
#' When the Mantel-Fleiss criterion is satisfied, a Cochran-Mantel-Haenszel |
|
| 15 |
#' (CMH) analysis is performed using the method specified by `mf_method`. |
|
| 16 |
#' When the criterion is not satisfied, an unconditional exact method is used |
|
| 17 |
#' instead. |
|
| 18 |
#' |
|
| 19 |
#' @details |
|
| 20 |
#' The data are prepared by [tern::h_prepare_rsp_table()], which constructs the |
|
| 21 |
#' response vector, group indicator, optional strata variable, and the |
|
| 22 |
#' corresponding 2 x 2 contingency table(s). When multiple strata variables are |
|
| 23 |
#' supplied, the cross-combinations of their levels define the analysis strata. |
|
| 24 |
#' |
|
| 25 |
#' The response variable is converted to a binary response according to `val`. |
|
| 26 |
#' The analysis compares the response proportion in the non-reference group with |
|
| 27 |
#' that in the reference group. The difference is calculated as the |
|
| 28 |
#' non-reference group minus the reference group. |
|
| 29 |
#' |
|
| 30 |
#' When strata are provided and the Mantel-Fleiss criterion is satisfied, the |
|
| 31 |
#' selected CMH method [tern::prop_diff_cmh()] is used to obtain the point |
|
| 32 |
#' estimate and confidence interval for the difference in response proportions. |
|
| 33 |
#' The available Cochran-Mantel-Haenszel-type methods for estimating the |
|
| 34 |
#' difference in response proportions and its confidence interval are: |
|
| 35 |
#' |
|
| 36 |
#' * `"cmh"`: CMH method. |
|
| 37 |
#' * `"cmh_sato"`: CMH method using the Sato variance estimator. |
|
| 38 |
#' * `"cmh_mn"`: CMH method using the Miettinen-Nurminen method. |
|
| 39 |
#' |
|
| 40 |
#' See [tern::prop_diff_cmh()] for more details and references. |
|
| 41 |
#' |
|
| 42 |
#' When strata are not specified, or when strata are specified but the |
|
| 43 |
#' Mantel-Fleiss criterion is not satisfied or cannot be evaluated (e.g., when |
|
| 44 |
#' all strata contain no observed values), the unconditional exact method |
|
| 45 |
#' [tern::prop_diff_uncond_exact()] is used to obtain the point estimate and |
|
| 46 |
#' confidence interval. |
|
| 47 |
#' |
|
| 48 |
#' The Mantel-Fleiss criterion is evaluated using [tern::mantel_fleiss_crit()]. |
|
| 49 |
#' |
|
| 50 |
#' The statistics function `s_proportion_diff_mf()` follows the usage and output |
|
| 51 |
#' of [tern::s_proportion_diff()] but removes the `method` argument and selects |
|
| 52 |
#' internally between a Cochran-Mantel-Haenszel-type method and an unconditional |
|
| 53 |
#' exact method based on the Mantel-Fleiss criterion. |
|
| 54 |
#' |
|
| 55 |
#' @inheritParams proposal_argument_convention |
|
| 56 |
#' @param df (`data.frame`) \cr |
|
| 57 |
#' The data containing all analysis variables for the non-reference group. |
|
| 58 |
#' @param .var (`character(1)`) \cr |
|
| 59 |
#' The column name in `df` (and, if supplied, `.ref_group`) specifying the |
|
| 60 |
#' response variable. The response is converted to a logical vector by |
|
| 61 |
#' comparing its values with `val`. `df[[var]]` (and `.ref_group[[var]]`, if |
|
| 62 |
#' supplied) must be an atomic vector as defined by |
|
| 63 |
#' `checkmate::check_atomic_vector()`, of one of the following types: |
|
| 64 |
#' `logical`, `integer`, `numeric`, or `character`. |
|
| 65 |
#' @param .in_ref_col (`logical(1)` or `NULL`) \cr |
|
| 66 |
#' Indicates whether the function is being evaluated for the reference |
|
| 67 |
#' column. Set to `TRUE` for the reference column and to `FALSE` or `NULL` |
|
| 68 |
#' otherwise. |
|
| 69 |
#' For the statistics function, `TRUE` or `NULL` results in empty statistics, |
|
| 70 |
#' while for the analysis function, the result is a `RowsVerticalSection` with |
|
| 71 |
#' a `NULL` value for all selected statistics. |
|
| 72 |
#' @param .ref_group (`data.frame` or `NULL`) \cr |
|
| 73 |
#' The data corresponding to the reference group. |
|
| 74 |
#' @param val (`logical(1)` or `integer(1)` or `numeric(1)` or `character(1)`) \cr |
|
| 75 |
#' The value used to identify the response of interest. An observation is |
|
| 76 |
#' considered a response of interest when `df[[.var]] == val` (and, if the |
|
| 77 |
#' the reference group is supplied, `.ref_group[[.var]] == val`). |
|
| 78 |
#' If `df[[.var]]` is a `factor`, `val` must be a `character` value matching |
|
| 79 |
#' one of its levels. Otherwise, `val` must have the same class as `df[[.var]]`. |
|
| 80 |
#' @param variables (`list` or `NULL`) \cr |
|
| 81 |
#' Optional strata variables specified as `list(strata = <strata_vars>)`, |
|
| 82 |
#' where `<strata_vars>` is a character vector containing column names in |
|
| 83 |
#' `df` (and, if supplied, `.ref_group`). All specified strata variables |
|
| 84 |
#' must be factors. |
|
| 85 |
#' @param na.rm (`logical(1)`) \cr |
|
| 86 |
#' Whether to remove incomplete rows from `df` (and, if supplied, `.ref_group`) |
|
| 87 |
#' before constructing the response, group, and strata vectors. |
|
| 88 |
#' Completeness is assessed only for `.var` and the strata columns (if supplied). |
|
| 89 |
#' If `na.rm = TRUE`, rows containing missing values in any of these columns |
|
| 90 |
#' are removed. If `na.rm = FALSE`, the function fails if any of these columns |
|
| 91 |
#' contain missing values, rather than returning results containing `NA` |
|
| 92 |
#' values. |
|
| 93 |
#' |
|
| 94 |
#' This argument is passed unchanged to the `complete_cases` argument of |
|
| 95 |
#' [tern::h_prepare_rsp_table()]. |
|
| 96 |
#' @param mf_method (`character(1)`) \cr |
|
| 97 |
#' The method used for estimating the difference in response proportions and |
|
| 98 |
#' its confidence interval when strata are provided and the Mantel-Fleiss |
|
| 99 |
#' criterion is satisfied. Available choices are: `"cmh"`, `"cmh_sato"`, and |
|
| 100 |
#' `"cmh_mn"`. See **Details** for more information. |
|
| 101 |
#' @param mf_threshold (`numeric(1)` or `NULL`) \cr |
|
| 102 |
#' Threshold for the Mantel-Fleiss criterion. |
|
| 103 |
#' If `NULL`, the default from [tern::mantel_fleiss_crit()] is used. |
|
| 104 |
#' @param .stats (`character`) \cr |
|
| 105 |
#' Statistics to select. Available choices are `"diff"`, `"diff_ci"`, and |
|
| 106 |
#' `"diff_est_ci"`. This parameter affects only the formatted analysis |
|
| 107 |
#' function and has no effect on the statistics function. |
|
| 108 |
#' @param .formats (named `character` or `list`) \cr |
|
| 109 |
#' Formats for the statistics. |
|
| 110 |
#' |
|
| 111 |
#' @seealso [cond_proportion_j()] |
|
| 112 |
#' @order 1 |
|
| 113 |
#' @author WW |
|
| 114 |
#' @examples |
|
| 115 |
#' n <- 100 # Number of observations. |
|
| 116 |
#' set.seed(123) |
|
| 117 |
#' dta <- data.frame( |
|
| 118 |
#' "rsp" = sample(c("Y", "N"), n, TRUE),
|
|
| 119 |
#' "grp" = c( |
|
| 120 |
#' sample(c("A", "Placebo"), round(n - 0.1 * n), TRUE),
|
|
| 121 |
#' rep("C", round(0.1 * n))
|
|
| 122 |
#' ), |
|
| 123 |
#' "f1" = sample(c("a1", "a2"), n, TRUE),
|
|
| 124 |
#' "f2" = sample(c("x", "y", "z"), n, TRUE),
|
|
| 125 |
#' stringsAsFactors = TRUE |
|
| 126 |
#' ) |
|
| 127 |
#' |
|
| 128 |
NULL |
|
| 129 | ||
| 130 |
#' @describeIn proportion_diff_mf Statistics function estimating the difference |
|
| 131 |
#' in response proportions and its confidence interval. |
|
| 132 |
#' When strata variables are provided and the Mantel-Fleiss criterion is |
|
| 133 |
#' satisfied, the CMH method [tern::prop_diff_cmh()] is used, with the |
|
| 134 |
#' `diff_se` argument determined by `mf_method`. Otherwise, i.e., when strata |
|
| 135 |
#' variables are not provided, or when strata variables are provided but the |
|
| 136 |
#' Mantel-Fleiss criterion is not satisfied or cannot be evaluated, |
|
| 137 |
#' the unconditional exact method [tern::prop_diff_uncond_exact()] is used. |
|
| 138 |
#' |
|
| 139 |
#' @return |
|
| 140 |
#' * `s_proportion_diff_mf()` returns a list containing: |
|
| 141 |
#' \describe{
|
|
| 142 |
#' \item{`diff` (`numeric(1)` or `numeric(0)`)}{Point estimate of the
|
|
| 143 |
#' difference in response proportions, or `numeric(0)` when the function is |
|
| 144 |
#' evaluated for the reference column (`.in_ref_col` is NULL or `.in_ref_col` |
|
| 145 |
#' is `TRUE`).} |
|
| 146 |
#' \item{`diff_ci` (`numeric(2)` or `numeric(0)`)}{Confidence interval for
|
|
| 147 |
#' the difference in response proportions, or `numeric(0)` when the function |
|
| 148 |
#' is evaluated for the reference column (`.in_ref_col` is NULL or |
|
| 149 |
#' `.in_ref_col` is `TRUE`).} |
|
| 150 |
#' \item{`diff_est_ci` (`numeric(3)` or `numeric(0)`)}{Point estimate and
|
|
| 151 |
#' confidence interval for the difference in response proportions, or |
|
| 152 |
#' `numeric(0)` when the function is evaluated for the reference column |
|
| 153 |
#' (`.in_ref_col` is NULL or `.in_ref_col` is `TRUE`).} |
|
| 154 |
#' \item{`executed_method` (`character(1)`)}{Name of the method actually used
|
|
| 155 |
#' for the analysis. Takes the value `NA_character_` when the function is |
|
| 156 |
#' evaluated for the reference column (`.in_ref_col` is `NULL` or |
|
| 157 |
#' `.in_ref_col` is `TRUE`). Otherwise, identifies the method used: |
|
| 158 |
#' `"cmh"`, `"cmh_sato"`, `"cmh_mn"`, or `"uncond_exact_diff"`. |
|
| 159 |
#' Formatted analysis functions can use this value to determine whether an |
|
| 160 |
#' appropriate footnote should be added.} |
|
| 161 |
#' } |
|
| 162 |
#' |
|
| 163 |
#' @order 3 |
|
| 164 |
#' @export |
|
| 165 |
#' @examples |
|
| 166 |
#' prop_d <- s_proportion_diff_mf( |
|
| 167 |
#' df = subset(dta, grp == "A"), |
|
| 168 |
#' .var = "rsp", |
|
| 169 |
#' .ref_group = subset(dta, grp == "Placebo"), |
|
| 170 |
#' .in_ref_col = FALSE, |
|
| 171 |
#' val = "Y", |
|
| 172 |
#' variables = list(strata = c("f1", "f2")),
|
|
| 173 |
#' conf_level = 0.90, |
|
| 174 |
#' mf_method = "cmh_mn" |
|
| 175 |
#' ) |
|
| 176 |
#' prop_d |
|
| 177 |
#' |
|
| 178 |
s_proportion_diff_mf <- function(df, |
|
| 179 |
.var, |
|
| 180 |
.in_ref_col = NULL, |
|
| 181 |
.ref_group = NULL, |
|
| 182 |
val = TRUE, |
|
| 183 |
variables = list(strata = NULL), |
|
| 184 |
na.rm = FALSE, |
|
| 185 |
conf_level = 0.95, |
|
| 186 |
mf_method = c("cmh", "cmh_sato", "cmh_mn"),
|
|
| 187 |
mf_threshold = NULL) {
|
|
| 188 | 39x |
assert_flag(.in_ref_col, null.ok = TRUE) |
| 189 | 39x |
assert_data_frame(.ref_group) |
| 190 | 38x |
assert_list(variables) |
| 191 | 38x |
assert_flag(na.rm) |
| 192 | ||
| 193 | 38x |
mf_method <- match.arg(mf_method) |
| 194 | ||
| 195 | 38x |
if (is.null(.in_ref_col) || .in_ref_col) {
|
| 196 | 10x |
y <- list( |
| 197 | 10x |
diff = numeric(), |
| 198 | 10x |
diff_ci = numeric(), |
| 199 | 10x |
diff_est_ci = numeric(), |
| 200 | 10x |
executed_method = NA_character_ |
| 201 |
) |
|
| 202 |
} else {
|
|
| 203 | 28x |
assert_false(is.null(.ref_group)) |
| 204 | ||
| 205 | 28x |
rsp_data <- h_prepare_rsp_table( |
| 206 | 28x |
df = df, |
| 207 | 28x |
df_ref = .ref_group, |
| 208 | 28x |
var = .var, |
| 209 | 28x |
val = val, |
| 210 | 28x |
strata_vars = variables$strata, |
| 211 | 28x |
complete_cases = na.rm |
| 212 |
) |
|
| 213 | ||
| 214 |
# Check the Mantel-Fleiss criterion for stratified data. |
|
| 215 | 25x |
is_mf_satisfied <- if (is.null(variables$strata)) {
|
| 216 | 1x |
warning( |
| 217 | 1x |
"No strata variables were supplied; the Mantel-Fleiss criterion ", |
| 218 | 1x |
"cannot be checked. Falling back to unconditional exact analysis." |
| 219 |
) |
|
| 220 | 1x |
FALSE |
| 221 | 25x |
} else if (is.null(mf_threshold)) {
|
| 222 |
# Note: mantel_fleiss_crit() can return NA. |
|
| 223 | 21x |
isTRUE(mantel_fleiss_crit(rsp_data$tbl)) |
| 224 |
} else {
|
|
| 225 | 3x |
isTRUE(mantel_fleiss_crit(rsp_data$tbl, threshold = mf_threshold)) |
| 226 |
} |
|
| 227 | ||
| 228 | 25x |
y <- if (is_mf_satisfied) {
|
| 229 | 8x |
executed_method <- mf_method |
| 230 | 8x |
diff_se <- switch(mf_method, |
| 231 | 8x |
cmh = "standard", |
| 232 | 8x |
cmh_sato = "sato", |
| 233 | 8x |
cmh_mn = "miettinen_nurminen" |
| 234 |
) |
|
| 235 | 8x |
prop_diff_cmh( |
| 236 | 8x |
rsp = rsp_data$rsp, |
| 237 | 8x |
grp = rsp_data$grp, |
| 238 | 8x |
strata = rsp_data$strata, |
| 239 | 8x |
conf_level = conf_level, |
| 240 | 8x |
diff_se = diff_se |
| 241 |
) |
|
| 242 |
} else {
|
|
| 243 | 17x |
executed_method <- "uncond_exact_diff" |
| 244 | 17x |
prop_diff_uncond_exact( |
| 245 | 17x |
rsp = rsp_data$rsp, grp = rsp_data$grp, conf_level = conf_level |
| 246 |
) |
|
| 247 |
} |
|
| 248 | ||
| 249 | 25x |
assert_subset(c("diff", "diff_ci"), names(y))
|
| 250 | 25x |
y <- y[c("diff", "diff_ci")]
|
| 251 | 25x |
y$diff <- setNames(y$diff * 100, paste0("diff_", executed_method))
|
| 252 | 25x |
y$diff_ci <- setNames(y$diff_ci * 100, paste0("diff_ci_", executed_method, c("_l", "_u")))
|
| 253 | 25x |
y$diff_est_ci <- c(y$diff, y$diff_ci) |
| 254 | 25x |
y$executed_method <- executed_method |
| 255 |
} |
|
| 256 | ||
| 257 | 35x |
h_set_labels_prop_diff_mf(y, mf_method = mf_method, conf_level = conf_level) |
| 258 |
} |
|
| 259 | ||
| 260 |
#' @describeIn proportion_diff_mf Formatted analysis function used as `afun` |
|
| 261 |
#' for the difference in response proportions with method selection based on |
|
| 262 |
#' the Mantel-Fleiss criterion. |
|
| 263 |
#' |
|
| 264 |
#' @param exact_footnote (`character(1)` or `RefFootnote`) \cr |
|
| 265 |
#' Footnote used to indicate that the unconditional exact method was actually |
|
| 266 |
#' used for the analysis. |
|
| 267 |
#' |
|
| 268 |
#' @return |
|
| 269 |
#' * `a_proportion_diff_mf()` returns the corresponding `RowsVerticalSection` |
|
| 270 |
#' object with formatted results. |
|
| 271 |
#' |
|
| 272 |
#' @order 2 |
|
| 273 |
#' @export |
|
| 274 |
#' @examples |
|
| 275 |
#' lyt <- basic_table() |> |
|
| 276 |
#' split_cols_by(var = "grp", ref_group = "Placebo") |> |
|
| 277 |
#' analyze( |
|
| 278 |
#' vars = "rsp", |
|
| 279 |
#' afun = a_proportion_diff_mf, |
|
| 280 |
#' extra_args = list( |
|
| 281 |
#' val = "Y", |
|
| 282 |
#' variables = list(strata = c("f1", "f2")),
|
|
| 283 |
#' conf_level = 0.90, |
|
| 284 |
#' mf_method = "cmh_mn", |
|
| 285 |
#' .stats = "diff_est_ci" |
|
| 286 |
#' ) |
|
| 287 |
#' ) |
|
| 288 |
#' |
|
| 289 |
#' build_table(lyt, df = dta) |
|
| 290 |
#' |
|
| 291 |
a_proportion_diff_mf <- function(df, |
|
| 292 |
.var, |
|
| 293 |
.in_ref_col = NULL, |
|
| 294 |
.ref_group = NULL, |
|
| 295 |
val = TRUE, |
|
| 296 |
variables = list(strata = NULL), |
|
| 297 |
na.rm = FALSE, |
|
| 298 |
conf_level = 0.95, |
|
| 299 |
mf_method = c("cmh", "cmh_sato", "cmh_mn"),
|
|
| 300 |
mf_threshold = NULL, |
|
| 301 |
..., |
|
| 302 |
.stats = NULL, |
|
| 303 |
.formats = NULL, |
|
| 304 |
.labels = NULL, |
|
| 305 |
.indent_mods = NULL, |
|
| 306 |
exact_footnote = rtables::RefFootnote( |
|
| 307 |
note = tern::d_proportion_diff( |
|
| 308 |
method = "uncond_exact_diff", method_only = TRUE |
|
| 309 |
), |
|
| 310 |
index = 1L, |
|
| 311 |
symbol = "+" |
|
| 312 |
)) {
|
|
| 313 | 22x |
assert_scalar(.var) |
| 314 | 22x |
assert_scalar(.in_ref_col, null.ok = TRUE) |
| 315 | 22x |
assert_scalar(val) |
| 316 | 22x |
assert_scalar(na.rm) |
| 317 | 22x |
assert_scalar(conf_level) |
| 318 | 22x |
assert_true( |
| 319 | 22x |
identical(class(exact_footnote), structure("RefFootnote", package = "rtables")) ||
|
| 320 | 22x |
test_string(exact_footnote) |
| 321 |
) |
|
| 322 | ||
| 323 | 22x |
dots_extra_args <- list(...) |
| 324 | ||
| 325 |
# It supports only default stats, not custom stats. |
|
| 326 | 22x |
.stats <- .split_std_from_custom_stats(.stats)$default_stats |
| 327 | ||
| 328 | 22x |
x_stats <- .apply_stat_functions( |
| 329 | 22x |
default_stat_fnc = s_proportion_diff_mf, |
| 330 | 22x |
custom_stat_fnc_list = NULL, |
| 331 | 22x |
args_list = c( |
| 332 | 22x |
df = list(df), |
| 333 | 22x |
.var = .var, |
| 334 | 22x |
.in_ref_col = .in_ref_col, |
| 335 | 22x |
.ref_group = list(.ref_group), |
| 336 | 22x |
val = val, |
| 337 | 22x |
variables = list(variables), |
| 338 | 22x |
na.rm = na.rm, |
| 339 | 22x |
conf_level = conf_level, |
| 340 | 22x |
mf_method = list(mf_method), |
| 341 | 22x |
mf_threshold = mf_threshold, |
| 342 | 22x |
dots_extra_args |
| 343 |
) |
|
| 344 |
) |
|
| 345 | ||
| 346 | 21x |
method <- x_stats$executed_method |
| 347 | 21x |
x_stats$executed_method <- NULL |
| 348 | ||
| 349 | 21x |
cell_footnotes <- if (identical(method, "uncond_exact_diff")) { # method can be NA.
|
| 350 | 7x |
fn <- list(list(exact_footnote)) |
| 351 | 7x |
setNames(rep(fn, length(x_stats)), names(x_stats)) |
| 352 |
} else {
|
|
| 353 | 14x |
list(NULL) |
| 354 |
} |
|
| 355 | ||
| 356 | 21x |
format_stats( |
| 357 | 21x |
x_stats, |
| 358 | 21x |
method_groups = "proportion_diff", |
| 359 | 21x |
stats_in = .stats, |
| 360 | 21x |
formats_in = .formats, |
| 361 | 21x |
labels_in = .labels, |
| 362 | 21x |
indents_in = .indent_mods, |
| 363 | 21x |
.cell_footnotes = cell_footnotes |
| 364 |
) |
|
| 365 |
} |
|
| 366 | ||
| 367 |
#' @title Helper function to set labels for `s_proportion_diff_mf()` statistics |
|
| 368 |
#' |
|
| 369 |
#' @description `r lifecycle::badge("stable")`
|
|
| 370 |
#' |
|
| 371 |
#' Sets descriptive labels on the statistics returned by `s_proportion_diff_mf()`. |
|
| 372 |
#' |
|
| 373 |
#' The label includes the detailed names of the two methods that may be selected |
|
| 374 |
#' according to the Mantel-Fleiss criterion: `mf_method` is selected when the |
|
| 375 |
#' criterion is satisfied, while `non_mf_method` is selected when the criterion |
|
| 376 |
#' is not satisfied or cannot be evaluated. |
|
| 377 |
#' |
|
| 378 |
#' Specifically, the method part of the label has the form |
|
| 379 |
#' `"(mf_method_label / non_mf_method_label)"`, where `mf_method_label` and |
|
| 380 |
#' `non_mf_method_label` are the corresponding detailed method names. |
|
| 381 |
#' |
|
| 382 |
#' @inheritParams proportion_diff_mf conf_level |
|
| 383 |
#' @param y (`list`) \cr |
|
| 384 |
#' The list of statistics returned by `s_proportion_diff_mf()` for which |
|
| 385 |
#' labels should be set. Must contain the named elements `"diff"`, `"diff_ci"`, |
|
| 386 |
#' and `"diff_est_ci"`. |
|
| 387 |
#' @param mf_method (`character(1)`) \cr |
|
| 388 |
#' The method specified for the Mantel-Fleiss-based analysis. |
|
| 389 |
#' Available choices are those accepted by the `method` argument of |
|
| 390 |
#' [tern::d_proportion_diff()]. |
|
| 391 |
#' @param non_mf_method (`character(1)`) \cr |
|
| 392 |
#' The method used when the Mantel-Fleiss criterion is not satisfied or cannot |
|
| 393 |
#' be evaluated. |
|
| 394 |
#' Available choices are those accepted by the method argument of |
|
| 395 |
#' [tern::d_proportion_diff()]. |
|
| 396 |
#' |
|
| 397 |
#' @return |
|
| 398 |
#' The input `y` with descriptive label attributes added to the `"diff"`, |
|
| 399 |
#' `"diff_ci"`, and `"diff_est_ci"` elements. |
|
| 400 |
#' |
|
| 401 |
#' @keywords internal |
|
| 402 |
h_set_labels_prop_diff_mf <- function(y, |
|
| 403 |
mf_method, |
|
| 404 |
non_mf_method = "uncond_exact_diff", |
|
| 405 |
conf_level) {
|
|
| 406 | 48x |
assert_list(y) |
| 407 | 48x |
assert_subset(c("diff", "diff_ci", "diff_est_ci"), choices = names(y))
|
| 408 | ||
| 409 | 48x |
label_prefix <- "Difference in Response rate (%)" |
| 410 | 48x |
mf_method_label <- d_proportion_diff(method = mf_method, method_only = TRUE) |
| 411 | 48x |
non_mf_method_label <- d_proportion_diff(method = non_mf_method, method_only = TRUE) |
| 412 | 48x |
method_label <- paste0("(", mf_method_label, " / ", non_mf_method_label, ")")
|
| 413 | ||
| 414 |
# Set labels. |
|
| 415 | 48x |
attr(y$diff, "label") <- paste(label_prefix, method_label) |
| 416 | 48x |
attr(y$diff_ci, "label") <- paste( |
| 417 | 48x |
label_prefix, tern::f_conf_level(conf_level), method_label |
| 418 |
) |
|
| 419 | 48x |
attr(y$diff_est_ci, "label") <- paste( |
| 420 | 48x |
label_prefix, "and", tern::f_conf_level(conf_level), method_label |
| 421 |
) |
|
| 422 | 48x |
y |
| 423 |
} |
|
| 424 | ||
| 425 |
# Test Proportion Difference ---- |
|
| 426 | ||
| 427 |
#' @name test_proportion_diff_mf |
|
| 428 |
#' |
|
| 429 |
#' @title Test of Proportion Difference with Method Selection Based on the |
|
| 430 |
#' Mantel-Fleiss Criterion |
|
| 431 |
#' |
|
| 432 |
#' @description `r lifecycle::badge("stable")`
|
|
| 433 |
#' |
|
| 434 |
#' Tests the difference in response proportions between a non-reference group |
|
| 435 |
#' and a reference group for stratified data. The test method is selected based |
|
| 436 |
#' on the Mantel-Fleiss (MF) criterion. |
|
| 437 |
#' |
|
| 438 |
#' When the Mantel-Fleiss criterion is satisfied, a Cochran-Mantel-Haenszel |
|
| 439 |
#' (CMH)-type test is performed using the method specified by `mf_method`. |
|
| 440 |
#' When the criterion is not satisfied, Fisher's exact test is used instead. |
|
| 441 |
#' |
|
| 442 |
#' @details |
|
| 443 |
#' The data are prepared by [tern::h_prepare_rsp_table()], which constructs the |
|
| 444 |
#' response vector, group indicator, optional strata variable, and the |
|
| 445 |
#' corresponding 2 x 2 contingency table(s). When multiple strata variables are |
|
| 446 |
#' supplied, the cross-combinations of their levels define the analysis strata. |
|
| 447 |
#' |
|
| 448 |
#' The response variable is converted to a binary response according to `val`. |
|
| 449 |
#' The analysis compares the response proportion in the non-reference group with |
|
| 450 |
#' that in the reference group. The difference is calculated as the |
|
| 451 |
#' non-reference group minus the reference group. |
|
| 452 |
#' |
|
| 453 |
#' When strata are provided and the Mantel-Fleiss criterion is satisfied, the |
|
| 454 |
#' selected CMH test [tern::prop_cmh()] is used to obtain the p-value for the |
|
| 455 |
#' test of the difference in response proportions. The available |
|
| 456 |
#' Cochran-Mantel-Haenszel tests are: |
|
| 457 |
#' |
|
| 458 |
#' * `"cmh"`: Stratified Cochran-Mantel-Haenszel test. |
|
| 459 |
#' * `"cmh_sato"`: Stratified CMH test using the Sato variance estimator. |
|
| 460 |
#' * `"cmh_wh"`: Stratified CMH test using the Wilson-Hilferty transformation. |
|
| 461 |
#' |
|
| 462 |
#' See [tern::prop_cmh()] for more details and references. |
|
| 463 |
#' |
|
| 464 |
#' When strata are not specified, or when strata are specified but the |
|
| 465 |
#' Mantel-Fleiss criterion is not satisfied or cannot be evaluated (e.g., when |
|
| 466 |
#' none of the strata contain any observed values), Fisher's exact test |
|
| 467 |
#' [tern::prop_fisher()] is used instead. |
|
| 468 |
#' |
|
| 469 |
#' The Mantel-Fleiss criterion is evaluated using [tern::mantel_fleiss_crit()]. |
|
| 470 |
#' |
|
| 471 |
#' The statistics function `s_test_proportion_diff_mf()` follows the usage and |
|
| 472 |
#' output of [tern::s_test_proportion_diff()] but removes the `method` argument |
|
| 473 |
#' and selects internally between a Cochran-Mantel-Haenszel-type test and |
|
| 474 |
#' Fisher's exact test based on the Mantel-Fleiss criterion. |
|
| 475 |
#' |
|
| 476 |
#' @inheritParams proportion_diff_mf |
|
| 477 |
#' @inheritParams proposal_argument_convention alternative |
|
| 478 |
#' @param mf_method (`character(1)`) \cr |
|
| 479 |
#' The method used for testing the difference in response proportions when |
|
| 480 |
#' strata are provided and the Mantel-Fleiss criterion is satisfied. |
|
| 481 |
#' Available choices are `"cmh"`, `"cmh_sato"`, and `"cmh_wh"`. |
|
| 482 |
#' See **Details** for more information. |
|
| 483 |
#' @param .stats (character) \cr |
|
| 484 |
#' Statistics to select. The only available choice is `"pval"`. |
|
| 485 |
#' This parameter affects only the formatted analysis function and has no |
|
| 486 |
#' effect on the statistics function. |
|
| 487 |
#' |
|
| 488 |
#' @seealso [proportion_diff_mf], [cond_proportion_j()] |
|
| 489 |
#' @order 1 |
|
| 490 |
#' @author WW |
|
| 491 |
#' @examples |
|
| 492 |
#' n <- 100 # Number of observations. |
|
| 493 |
#' set.seed(123) |
|
| 494 |
#' dta <- data.frame( |
|
| 495 |
#' "rsp" = sample(c("Y", "N"), n, TRUE),
|
|
| 496 |
#' "grp" = c( |
|
| 497 |
#' sample(c("A", "Placebo"), round(n - 0.1 * n), TRUE),
|
|
| 498 |
#' rep("C", round(0.1 * n))
|
|
| 499 |
#' ), |
|
| 500 |
#' "f1" = sample(c("a1", "a2"), n, TRUE),
|
|
| 501 |
#' "f2" = sample(c("x", "y", "z"), n, TRUE),
|
|
| 502 |
#' stringsAsFactors = TRUE |
|
| 503 |
#' ) |
|
| 504 |
#' |
|
| 505 |
NULL |
|
| 506 | ||
| 507 |
#' @describeIn test_proportion_diff_mf Statistics function testing the |
|
| 508 |
#' difference in response proportions. |
|
| 509 |
#' When strata variables are provided and the Mantel-Fleiss criterion is |
|
| 510 |
#' satisfied, the CMH test [tern::prop_cmh()] is used, with the values of the |
|
| 511 |
#' `diff_se` and `transform` arguments determined by `mf_method`. |
|
| 512 |
#' Otherwise, i.e., when strata variables are not provided, or when strata |
|
| 513 |
#' variables are provided but the Mantel-Fleiss criterion is not satisfied |
|
| 514 |
#' or cannot be evaluated, Fisher's exact test [tern::prop_fisher()] is used. |
|
| 515 |
#' |
|
| 516 |
#' @return |
|
| 517 |
#' * `s_test_proportion_diff_mf()` returns a list containing: |
|
| 518 |
#' \describe{
|
|
| 519 |
#' \item{`pval` (`numeric(1)` or `numeric(0)`)}{P-value from the test, or
|
|
| 520 |
#' `numeric(0)` when the function is evaluated for the reference column |
|
| 521 |
#' (`.in_ref_col` is NULL or `.in_ref_col` is `TRUE`).} |
|
| 522 |
#' \item{`executed_method` (`character(1)`)}{Name of the test actually
|
|
| 523 |
#' used for the analysis. |
|
| 524 |
#' Takes the value `NA_character_` when the function is evaluated for the |
|
| 525 |
#' reference column (`.in_ref_col` is `NULL` or `.in_ref_col` is `TRUE`). |
|
| 526 |
#' Otherwise, identifies the method used: `"cmh"`, `"cmh_sato"`, `"cmh_wh"`, |
|
| 527 |
#' or `"fisher"`. |
|
| 528 |
#' Formatted analysis functions can use this value to determine whether an |
|
| 529 |
#' appropriate footnote should be added.} |
|
| 530 |
#' } |
|
| 531 |
#' |
|
| 532 |
#' @order 3 |
|
| 533 |
#' @export |
|
| 534 |
#' @examples |
|
| 535 |
#' test_prop_d <- s_test_proportion_diff_mf( |
|
| 536 |
#' df = subset(dta, grp == "A"), |
|
| 537 |
#' .var = "rsp", |
|
| 538 |
#' .ref_group = subset(dta, grp == "Placebo"), |
|
| 539 |
#' .in_ref_col = FALSE, |
|
| 540 |
#' val = "Y", |
|
| 541 |
#' variables = list(strata = c("f1", "f2"))
|
|
| 542 |
#' ) |
|
| 543 |
#' test_prop_d |
|
| 544 |
#' |
|
| 545 |
s_test_proportion_diff_mf <- function(df, |
|
| 546 |
.var, |
|
| 547 |
.in_ref_col = NULL, |
|
| 548 |
.ref_group = NULL, |
|
| 549 |
val = TRUE, |
|
| 550 |
variables = list(strata = NULL), |
|
| 551 |
na.rm = FALSE, |
|
| 552 |
alternative = c("two.sided", "less", "greater"),
|
|
| 553 |
mf_method = c("cmh", "cmh_sato", "cmh_wh"),
|
|
| 554 |
mf_threshold = NULL) {
|
|
| 555 | 39x |
assert_flag(.in_ref_col, null.ok = TRUE) |
| 556 | 39x |
assert_data_frame(.ref_group, null.ok = TRUE) |
| 557 | 39x |
assert_list(variables, null.ok = TRUE) |
| 558 | 39x |
assert_flag(na.rm) |
| 559 | ||
| 560 | 39x |
mf_method <- match.arg(mf_method) |
| 561 | ||
| 562 | 39x |
if (is.null(.in_ref_col) || .in_ref_col) {
|
| 563 | 10x |
executed_method <- NA_character_ |
| 564 | 10x |
pval <- numeric() |
| 565 |
} else {
|
|
| 566 | 29x |
assert_false(is.null(.ref_group)) |
| 567 | ||
| 568 | 28x |
rsp_data <- h_prepare_rsp_table( |
| 569 | 28x |
df = df, |
| 570 | 28x |
df_ref = .ref_group, |
| 571 | 28x |
var = .var, |
| 572 | 28x |
val = val, |
| 573 | 28x |
strata_vars = variables$strata, |
| 574 | 28x |
complete_cases = na.rm |
| 575 |
) |
|
| 576 | 25x |
tbl <- rsp_data$tbl |
| 577 | ||
| 578 |
# Check the Mantel-Fleiss criterion for stratified data. |
|
| 579 | 25x |
is_mf_satisfied <- if (is.null(variables$strata)) {
|
| 580 | 1x |
warning( |
| 581 | 1x |
"No strata variables were supplied; the Mantel-Fleiss criterion ", |
| 582 | 1x |
"cannot be checked. Falling back to Fisher's exact test." |
| 583 |
) |
|
| 584 | 1x |
FALSE |
| 585 | 25x |
} else if (is.null(mf_threshold)) {
|
| 586 |
# Note: mantel_fleiss_crit() can return NA. |
|
| 587 | 21x |
isTRUE(mantel_fleiss_crit(rsp_data$tbl)) |
| 588 |
} else {
|
|
| 589 | 3x |
isTRUE(mantel_fleiss_crit(rsp_data$tbl, threshold = mf_threshold)) |
| 590 |
} |
|
| 591 | ||
| 592 | 25x |
pval <- if (is_mf_satisfied) {
|
| 593 | 7x |
executed_method <- mf_method |
| 594 | 7x |
switch(mf_method, |
| 595 | 2x |
cmh = prop_cmh(tbl, alternative = alternative), |
| 596 | 4x |
cmh_sato = prop_cmh(tbl, alternative = alternative, diff_se = "sato"), |
| 597 | 1x |
cmh_wh = prop_cmh(tbl, alternative = alternative, transform = "wilson_hilferty") |
| 598 |
) |
|
| 599 |
} else {
|
|
| 600 | 18x |
executed_method <- "fisher" |
| 601 | 18x |
tbl_unstrat <- margin.table(tbl, 1:2) |
| 602 | 18x |
prop_fisher(tbl_unstrat, alternative = alternative) |
| 603 |
} |
|
| 604 |
} |
|
| 605 | ||
| 606 |
# Set labels. |
|
| 607 | 35x |
method_label <- paste0( |
| 608 |
"(",
|
|
| 609 | 35x |
d_test_proportion_diff("fisher", method_only = TRUE),
|
| 610 |
" / ", |
|
| 611 | 35x |
d_test_proportion_diff(mf_method, alternative, method_only = TRUE), |
| 612 |
")" |
|
| 613 |
) |
|
| 614 | ||
| 615 | 35x |
list( |
| 616 | 35x |
pval = formatters::with_label(pval, method_label), |
| 617 | 35x |
executed_method = executed_method |
| 618 |
) |
|
| 619 |
} |
|
| 620 | ||
| 621 |
#' @describeIn test_proportion_diff_mf Formatted analysis function used as `afun` |
|
| 622 |
#' to test the difference in response proportions, with method selection based |
|
| 623 |
#' on the Mantel-Fleiss criterion. |
|
| 624 |
#' |
|
| 625 |
#' @return |
|
| 626 |
#' * `a_test_proportion_diff_mf()` returns the corresponding `RowsVerticalSection` |
|
| 627 |
#' object with formatted results. |
|
| 628 |
#' |
|
| 629 |
#' @order 2 |
|
| 630 |
#' @export |
|
| 631 |
#' @examples |
|
| 632 |
#' lyt <- basic_table() |> |
|
| 633 |
#' split_cols_by(var = "grp", ref_group = "Placebo") |> |
|
| 634 |
#' analyze( |
|
| 635 |
#' vars = "rsp", |
|
| 636 |
#' afun = a_test_proportion_diff_mf, |
|
| 637 |
#' extra_args = list( |
|
| 638 |
#' val = "Y", |
|
| 639 |
#' variables = list(strata = c("f1", "f2"))
|
|
| 640 |
#' ) |
|
| 641 |
#' ) |
|
| 642 |
#' |
|
| 643 |
#' build_table(lyt, df = dta) |
|
| 644 |
#' |
|
| 645 |
a_test_proportion_diff_mf <- function(df, |
|
| 646 |
.var, |
|
| 647 |
.in_ref_col = NULL, |
|
| 648 |
.ref_group = NULL, |
|
| 649 |
val = TRUE, |
|
| 650 |
variables = list(strata = NULL), |
|
| 651 |
na.rm = FALSE, |
|
| 652 |
alternative = c("two.sided", "less", "greater"),
|
|
| 653 |
mf_method = c("cmh", "cmh_sato", "cmh_wh"),
|
|
| 654 |
mf_threshold = NULL, |
|
| 655 |
..., |
|
| 656 |
.stats = NULL, |
|
| 657 |
.formats = NULL, |
|
| 658 |
.labels = NULL, |
|
| 659 |
.indent_mods = NULL, |
|
| 660 |
exact_footnote = rtables::RefFootnote( |
|
| 661 |
note = tern::d_test_proportion_diff( |
|
| 662 |
method = "fisher", method_only = TRUE |
|
| 663 |
), |
|
| 664 |
index = 1L, |
|
| 665 |
symbol = "+" |
|
| 666 |
)) {
|
|
| 667 | 22x |
assert_scalar(.var) |
| 668 | 22x |
assert_scalar(.in_ref_col, null.ok = TRUE) |
| 669 | 22x |
assert_scalar(val) |
| 670 | 22x |
assert_scalar(na.rm) |
| 671 | 22x |
assert_true( |
| 672 | 22x |
identical(class(exact_footnote), structure("RefFootnote", package = "rtables")) ||
|
| 673 | 22x |
test_string(exact_footnote) |
| 674 |
) |
|
| 675 | ||
| 676 | 22x |
dots_extra_args <- list(...) |
| 677 | ||
| 678 |
# Only support default stats, not custom stats |
|
| 679 | 22x |
.stats <- .split_std_from_custom_stats(.stats)$default_stats |
| 680 | ||
| 681 | 22x |
x_stats <- .apply_stat_functions( |
| 682 | 22x |
default_stat_fnc = s_test_proportion_diff_mf, |
| 683 | 22x |
custom_stat_fnc_list = NULL, |
| 684 | 22x |
args_list = c( |
| 685 | 22x |
df = list(df), |
| 686 | 22x |
.var = .var, |
| 687 | 22x |
.in_ref_col = .in_ref_col, |
| 688 | 22x |
.ref_group = list(.ref_group), |
| 689 | 22x |
val = val, |
| 690 | 22x |
variables = list(variables), |
| 691 | 22x |
na.rm = na.rm, |
| 692 | 22x |
alternative = list(alternative), |
| 693 | 22x |
mf_method = list(mf_method), |
| 694 | 22x |
mf_threshold = mf_threshold, |
| 695 | 22x |
dots_extra_args |
| 696 |
) |
|
| 697 |
) |
|
| 698 | ||
| 699 | 21x |
method <- x_stats$executed_method |
| 700 | 21x |
x_stats$executed_method <- NULL |
| 701 | ||
| 702 | 21x |
cell_footnotes <- if (identical(method, "fisher")) { # method can be NA.
|
| 703 | 8x |
fn <- list(list(exact_footnote)) |
| 704 | 8x |
setNames(rep(fn, length(x_stats)), names(x_stats)) |
| 705 |
} else {
|
|
| 706 | 13x |
list(NULL) |
| 707 |
} |
|
| 708 | ||
| 709 | 21x |
format_stats( |
| 710 | 21x |
x_stats, |
| 711 | 21x |
method_groups = "test_proportion_diff", |
| 712 | 21x |
stats_in = .stats, |
| 713 | 21x |
formats_in = .formats, |
| 714 | 21x |
labels_in = .labels, |
| 715 | 21x |
indents_in = .indent_mods, |
| 716 | 21x |
.cell_footnotes = cell_footnotes |
| 717 |
) |
|
| 718 |
} |
| 1 |
#' Helpers for Processing Least Square Means |
|
| 2 |
#' |
|
| 3 |
#' @inheritParams fit_mmrm_j |
|
| 4 |
#' @param fit result of model fitting function, e.g. [mmrm::mmrm()] or [stats::lm()]. |
|
| 5 |
#' @param averages (`list`)\cr optional named list of visit levels which should be averaged |
|
| 6 |
#' and reported along side the single visits. |
|
| 7 |
#' @param weights (`string`)\cr argument from [emmeans::emmeans()], 'counterfactual' by default. |
|
| 8 |
#' @param specs (`list`)\cr list of least square means specifications, with |
|
| 9 |
#' elements `coefs` (coefficient list) and `grid` (corresponding `data.frame`). |
|
| 10 |
#' @param emmeans_res (`list`)\cr initial `emmeans` result from [h_get_emmeans_res()]. |
|
| 11 |
#' |
|
| 12 |
#' @name lsmeans_helpers |
|
| 13 |
#' @keywords internal |
|
| 14 |
NULL |
|
| 15 | ||
| 16 |
#' @describeIn lsmeans_helpers partial matching of a set of `options` to a |
|
| 17 |
#' character vector `x`. Here partial matching means that for each element of `x` we look for the |
|
| 18 |
#' (first) element of `options` which is contained in the element of `x`. Then we return |
|
| 19 |
#' the matched `options` in the same order as `x`. |
|
| 20 |
#' @param options (`character`)\cr vector of options to match against `x`. |
|
| 21 |
#' @param x (`character`)\cr vector of strings to be partially matched with `options` |
|
| 22 |
h_partial_match <- function(options, x) {
|
|
| 23 | 7x |
checkmate::assert_character(options) |
| 24 | 7x |
checkmate::assert_character(x) |
| 25 | ||
| 26 | 7x |
result <- sapply(x, function(xi) {
|
| 27 | 55x |
matches <- vapply( |
| 28 | 55x |
options, |
| 29 | 55x |
function(opt) grepl(opt, xi, fixed = TRUE), |
| 30 | 55x |
logical(1) |
| 31 |
) |
|
| 32 | 55x |
options[which(matches)[1L]] |
| 33 | 7x |
}, USE.NAMES = FALSE) |
| 34 | 7x |
if (anyNA(result)) {
|
| 35 | 1x |
warning( |
| 36 | 1x |
"Some elements of 'x' did not match any of the 'options': ", |
| 37 | 1x |
paste(x[is.na(result)], collapse = ", ") |
| 38 |
) |
|
| 39 |
} |
|
| 40 | 7x |
result |
| 41 |
} |
|
| 42 | ||
| 43 |
#' @describeIn lsmeans_helpers returns a list with |
|
| 44 |
#' `object` (`emmGrid` object containing `emmeans` results) and `grid` |
|
| 45 |
#' (`data.frame` containing the (optional) subgroup, arm and the visit variables |
|
| 46 |
#' together with the sample size `n` for each combination). |
|
| 47 |
h_get_emmeans_res <- function(fit, vars, weights) {
|
|
| 48 | 60x |
data_complete <- stats::model.frame(fit) |
| 49 | 60x |
checkmate::assert_data_frame(data_complete) |
| 50 | 60x |
checkmate::assert_list(vars) |
| 51 | ||
| 52 | 60x |
if (!is.null(vars$subgroup)) {
|
| 53 | 10x |
checkmate::assert_factor(data_complete[[vars$subgroup]]) |
| 54 |
} |
|
| 55 | 60x |
emmeans_object <- emmeans::emmeans( |
| 56 | 60x |
fit, |
| 57 | 60x |
data = data_complete, |
| 58 | 60x |
specs = c(vars$subgroup, vars$visit, vars$arm), |
| 59 | 60x |
weights = weights |
| 60 |
) |
|
| 61 | ||
| 62 |
# Save grid with renamed number of subjects column. |
|
| 63 | 60x |
visit_arm_grid <- emmeans_object@grid |
| 64 | 60x |
wgt_index <- match(".wgt.", names(visit_arm_grid))
|
| 65 | 60x |
names(visit_arm_grid)[wgt_index] <- "n" |
| 66 | 60x |
visit_arm_grid$n <- as.integer(visit_arm_grid$n) |
| 67 | ||
| 68 | 60x |
list(object = emmeans_object, grid = visit_arm_grid) |
| 69 |
} |
|
| 70 | ||
| 71 |
#' @describeIn lsmeans_helpers constructs average of visits specifications. |
|
| 72 |
h_get_average_visit_specs <- function(emmeans_res, vars, averages, fit) {
|
|
| 73 | 12x |
visit_grid <- emmeans_res$grid[[vars$visit]] |
| 74 | 12x |
model_frame <- stats::model.frame(fit) |
| 75 | 12x |
averages_list <- list() |
| 76 | 12x |
visit_vec <- n_vec <- c() |
| 77 | 12x |
if (!is.null(vars$subgroup)) {
|
| 78 | 3x |
subgroup_grid <- emmeans_res$grid[[vars$subgroup]] |
| 79 | 3x |
subgroup_vec <- c() |
| 80 |
} |
|
| 81 | 12x |
if (!is.null(vars$arm)) {
|
| 82 | 9x |
arm_grid <- emmeans_res$grid[[vars$arm]] |
| 83 | 9x |
arm_vec <- c() |
| 84 |
} |
|
| 85 | 12x |
for (i in seq_along(averages)) {
|
| 86 | 20x |
average_label <- names(averages)[i] |
| 87 | 20x |
visits_average <- averages[[i]] |
| 88 | 20x |
checkmate::assert_subset(visits_average, choices = levels(visit_grid)) |
| 89 | 19x |
which_visits_in_average <- visit_grid %in% visits_average |
| 90 | 19x |
average_coefs <- as.integer(which_visits_in_average) / length(visits_average) |
| 91 | 19x |
zero_coefs <- numeric(length = length(average_coefs)) |
| 92 | ||
| 93 | 19x |
if (is.null(vars$arm) && is.null(vars$subgroup)) {
|
| 94 | 4x |
averages_list[[average_label]] <- average_coefs |
| 95 | 4x |
visit_vec <- c(visit_vec, average_label) |
| 96 | 4x |
is_in_subset <- (model_frame[[vars$visit]] %in% visits_average) |
| 97 | 4x |
this_n <- length(unique(model_frame[is_in_subset, vars$id])) |
| 98 | 4x |
n_vec <- c(n_vec, this_n) |
| 99 | 15x |
} else if (is.null(vars$arm) && !is.null(vars$subgroup)) {
|
| 100 | 2x |
for (this_subgroup in levels(subgroup_grid)) {
|
| 101 | 4x |
this_coefs <- zero_coefs |
| 102 | 4x |
subgroup_average_label <- paste(this_subgroup, average_label, sep = ".") |
| 103 | 4x |
which_subgroup <- subgroup_grid == this_subgroup |
| 104 | 4x |
which_visits_and_subgroup <- which_visits_in_average & which_subgroup |
| 105 | 4x |
this_coefs[which_visits_and_subgroup] <- 1 / sum(which_visits_and_subgroup) |
| 106 | 4x |
averages_list[[subgroup_average_label]] <- this_coefs |
| 107 | 4x |
subgroup_vec <- c(subgroup_vec, this_subgroup) |
| 108 | 4x |
visit_vec <- c(visit_vec, average_label) |
| 109 | 4x |
is_in_subset <- (model_frame[[vars$subgroup]] == this_subgroup) & |
| 110 | 4x |
(model_frame[[vars$visit]] %in% visits_average) |
| 111 | 4x |
this_n <- length(unique(model_frame[is_in_subset, vars$id])) |
| 112 | 4x |
n_vec <- c(n_vec, this_n) |
| 113 |
} |
|
| 114 | 13x |
} else if (!is.null(vars$arm) && is.null(vars$subgroup)) {
|
| 115 | 9x |
for (this_arm in levels(arm_grid)) {
|
| 116 | 18x |
this_coefs <- zero_coefs |
| 117 | 18x |
arm_average_label <- paste(this_arm, average_label, sep = ".") |
| 118 | 18x |
which_arm <- arm_grid == this_arm |
| 119 | 18x |
this_coefs[which_arm] <- average_coefs[which_arm] |
| 120 | 18x |
averages_list[[arm_average_label]] <- this_coefs |
| 121 | 18x |
arm_vec <- c(arm_vec, this_arm) |
| 122 | 18x |
visit_vec <- c(visit_vec, average_label) |
| 123 | 18x |
is_in_subset <- (model_frame[[vars$arm]] == this_arm) & (model_frame[[vars$visit]] %in% visits_average) |
| 124 | 18x |
this_n <- length(unique(model_frame[is_in_subset, vars$id])) |
| 125 | 18x |
n_vec <- c(n_vec, this_n) |
| 126 |
} |
|
| 127 |
} else {
|
|
| 128 | 4x |
for (this_arm in levels(arm_grid)) {
|
| 129 | 8x |
for (this_subgroup in levels(subgroup_grid)) {
|
| 130 | 16x |
this_coefs <- zero_coefs |
| 131 | 16x |
arm_subgroup_average_label <- paste(this_subgroup, this_arm, average_label, sep = ".") |
| 132 | 16x |
which_arm <- arm_grid == this_arm |
| 133 | 16x |
which_subgroup <- subgroup_grid == this_subgroup |
| 134 | 16x |
which_visits_and_arm_and_subgroup <- which_visits_in_average & which_arm & which_subgroup |
| 135 | 16x |
this_coefs[which_visits_and_arm_and_subgroup] <- 1 / sum(which_visits_and_arm_and_subgroup) |
| 136 | 16x |
averages_list[[arm_subgroup_average_label]] <- this_coefs |
| 137 | 16x |
arm_vec <- c(arm_vec, this_arm) |
| 138 | 16x |
subgroup_vec <- c(subgroup_vec, this_subgroup) |
| 139 | 16x |
visit_vec <- c(visit_vec, average_label) |
| 140 | 16x |
is_in_subset <- (model_frame[[vars$arm]] == this_arm) & |
| 141 | 16x |
(model_frame[[vars$subgroup]] == this_subgroup) & |
| 142 | 16x |
(model_frame[[vars$visit]] %in% visits_average) |
| 143 | 16x |
this_n <- length(unique(model_frame[is_in_subset, vars$id])) |
| 144 | 16x |
n_vec <- c(n_vec, this_n) |
| 145 |
} |
|
| 146 |
} |
|
| 147 |
} |
|
| 148 |
} |
|
| 149 | 11x |
if (is.null(vars$arm) && is.null(vars$subgroup)) {
|
| 150 | 2x |
averages_grid <- data.frame(visit = visit_vec, n = n_vec) |
| 151 | 2x |
names(averages_grid) <- c(vars$visit, "n") |
| 152 | 9x |
} else if (!is.null(vars$arm) && is.null(vars$subgroup)) {
|
| 153 | 6x |
averages_grid <- data.frame(arm = arm_vec, visit = visit_vec, n = n_vec) |
| 154 | 6x |
names(averages_grid) <- c(vars$arm, vars$visit, "n") |
| 155 | 3x |
} else if (is.null(vars$arm) && !is.null(vars$subgroup)) {
|
| 156 | 1x |
averages_grid <- data.frame(subgroup = subgroup_vec, visit = visit_vec, n = n_vec) |
| 157 | 1x |
names(averages_grid) <- c(vars$subgroup, vars$visit, "n") |
| 158 |
} else {
|
|
| 159 | 2x |
averages_grid <- data.frame(subgroup = subgroup_vec, arm = arm_vec, visit = visit_vec, n = n_vec) |
| 160 | 2x |
names(averages_grid) <- c(vars$subgroup, vars$arm, vars$visit, "n") |
| 161 |
} |
|
| 162 | 11x |
list(coefs = averages_list, grid = averages_grid) |
| 163 |
} |
|
| 164 | ||
| 165 |
#' @describeIn lsmeans_helpers estimates least square means as a `data.frame` |
|
| 166 |
#' given specifications. |
|
| 167 |
#' |
|
| 168 |
#' @note The difference here compared to the original tern.mmrm::h_get_spec_visit_estimates() |
|
| 169 |
#' function is that additional arguments for [emmeans::contrast()] can be passed via the |
|
| 170 |
#' dots (`...`) argument. |
|
| 171 |
#' Once this has been added to the `tern.mmrm` package then its functions can be used instead. |
|
| 172 |
#' |
|
| 173 |
#' @param tests (`flag`)\cr whether to add test results to the estimates. |
|
| 174 |
#' @param ... additional arguments for [emmeans::contrast()]. |
|
| 175 |
h_get_spec_visit_estimates <- function(emmeans_res, specs, conf_level, tests = FALSE, ...) {
|
|
| 176 | 58x |
checkmate::assert_list(emmeans_res) |
| 177 | 58x |
checkmate::assert_list(specs) |
| 178 | 58x |
checkmate::assert_number(conf_level) |
| 179 | 58x |
checkmate::assert_flag(tests) |
| 180 | ||
| 181 | 58x |
conts <- emmeans::contrast(emmeans_res$object, specs$coefs, ...) |
| 182 | 58x |
cis <- stats::confint(conts, level = conf_level) |
| 183 | 58x |
res <- cbind( |
| 184 | 58x |
specs$grid, |
| 185 | 58x |
data.frame(estimate = cis$estimate, se = cis$SE, df = cis$df, lower_cl = cis$lower.CL, upper_cl = cis$upper.CL) |
| 186 |
) |
|
| 187 | 58x |
if (tests) {
|
| 188 | 49x |
conts_df <- as.data.frame(conts) |
| 189 | 49x |
res$t_stat <- conts_df$t.ratio |
| 190 | 49x |
res$p_value <- conts_df$p.value |
| 191 | 49x |
res$p_value_less <- stats::pt(conts_df$t.ratio, df = conts_df$df, lower.tail = TRUE) |
| 192 | 49x |
res$p_value_greater <- stats::pt(conts_df$t.ratio, df = conts_df$df, lower.tail = FALSE) |
| 193 |
} |
|
| 194 | 58x |
res |
| 195 |
} |
|
| 196 | ||
| 197 |
#' @describeIn lsmeans_helpers estimates least square means for single visits. |
|
| 198 |
h_get_single_visit_estimates <- function(emmeans_res, conf_level) {
|
|
| 199 | 45x |
checkmate::assert_list(emmeans_res) |
| 200 | 45x |
checkmate::assert_number(conf_level) |
| 201 | ||
| 202 | 45x |
cis <- stats::confint(emmeans_res$object, level = conf_level) |
| 203 | 45x |
cbind( |
| 204 | 45x |
emmeans_res$grid[, setdiff(names(emmeans_res$grid), "n"), drop = FALSE], |
| 205 | 45x |
data.frame(estimate = cis$emmean, se = cis$SE, df = cis$df, lower_cl = cis$lower.CL, upper_cl = cis$upper.CL), |
| 206 | 45x |
emmeans_res$grid[, "n", drop = FALSE] |
| 207 |
) |
|
| 208 |
} |
|
| 209 | ||
| 210 |
#' @describeIn lsmeans_helpers constructs `data.frame` with |
|
| 211 |
#' relative reduction vs. reference arm based on single visit estimates. |
|
| 212 |
#' @param estimates (`data.frame`)\cr single visit least square mean estimates. |
|
| 213 |
h_get_relative_reduc_df <- function(estimates, vars) {
|
|
| 214 | 42x |
checkmate::assert_data_frame(estimates) |
| 215 | 42x |
checkmate::assert_list(vars) |
| 216 | ||
| 217 | 42x |
ref_arm_level <- estimates[[vars$arm]][1L] |
| 218 | 42x |
ref_estimates <- estimates[estimates[[vars$arm]] == ref_arm_level, c(vars$subgroup, vars$visit, "estimate")] |
| 219 | 42x |
names(ref_estimates)[names(ref_estimates) == "estimate"] <- "ref" |
| 220 | 42x |
result <- merge( |
| 221 | 42x |
estimates[estimates[[vars$arm]] != ref_arm_level, ], |
| 222 | 42x |
ref_estimates, |
| 223 | 42x |
by = c(vars$subgroup, vars$visit), |
| 224 | 42x |
sort = FALSE |
| 225 |
) |
|
| 226 | 42x |
result$relative_reduc <- (result$ref - result$estimate) / result$ref |
| 227 | 42x |
result[, c(vars$subgroup, vars$visit, vars$arm, "relative_reduc")] |
| 228 |
} |
|
| 229 | ||
| 230 |
#' @describeIn lsmeans_helpers constructs single visit contrast specifications. |
|
| 231 |
h_single_visit_contrast_specs <- function(emmeans_res, vars) {
|
|
| 232 | 25x |
checkmate::assert_list(emmeans_res) |
| 233 | 25x |
checkmate::assert_list(vars) |
| 234 | ||
| 235 | 25x |
emmeans_res$grid$index <- seq_len(nrow(emmeans_res$grid)) |
| 236 | ||
| 237 | 25x |
grid_by_subgroup_visit <- split(emmeans_res$grid, emmeans_res$grid[c(vars$subgroup, vars$visit)]) |
| 238 | ||
| 239 | 25x |
arm_levels <- emmeans_res$object@levels[[vars$arm]] |
| 240 | 25x |
visit_levels <- emmeans_res$object@levels[[vars$visit]] |
| 241 | 25x |
if (!is.null(vars$subgroup)) {
|
| 242 | 7x |
subgroup_levels <- emmeans_res$object@levels[[vars$subgroup]] |
| 243 |
} |
|
| 244 | 25x |
ref_arm_level <- arm_levels[1L] |
| 245 | 25x |
zeros_coefs <- numeric(nrow(emmeans_res$grid)) |
| 246 | 25x |
overall_list <- list() |
| 247 | 25x |
arm_vec <- visit_vec <- subgroup_vec <- c() |
| 248 | 25x |
for (j in seq_along(grid_by_subgroup_visit)) {
|
| 249 | 128x |
this_grid <- grid_by_subgroup_visit[[j]] |
| 250 | 128x |
ref_index <- which(this_grid[[vars$arm]] == ref_arm_level) |
| 251 | 128x |
this_visit <- as.character(unique(this_grid[[vars$visit]])) |
| 252 | 128x |
checkmate::assert_true(length(this_visit) == 1L) |
| 253 | 128x |
if (!is.null(vars$subgroup)) {
|
| 254 | 56x |
this_subgroup <- as.character(unique(this_grid[[vars$subgroup]])) |
| 255 | 56x |
checkmate::assert_true(length(this_subgroup) == 1L) |
| 256 |
} |
|
| 257 | 128x |
this_ref_coefs <- zeros_coefs |
| 258 | 128x |
this_ref_coefs[this_grid$index[ref_index]] <- -1 |
| 259 | 128x |
this_list <- list() |
| 260 | 128x |
for (i in seq_len(nrow(this_grid))[-ref_index]) {
|
| 261 | 172x |
this_coefs <- this_ref_coefs |
| 262 | 172x |
this_coefs[this_grid$index[i]] <- 1 |
| 263 | 172x |
this_arm <- as.character(this_grid[[vars$arm]][i]) |
| 264 | 172x |
arm_vec <- c(arm_vec, this_arm) |
| 265 | 172x |
visit_vec <- c(visit_vec, this_visit) |
| 266 | 172x |
if (!is.null(vars$subgroup)) {
|
| 267 | 64x |
subgroup_vec <- c(subgroup_vec, this_subgroup) |
| 268 |
} |
|
| 269 | 172x |
this_label <- paste(this_arm, this_visit, sep = ".") |
| 270 | 172x |
if (!is.null(vars$subgroup)) {
|
| 271 | 64x |
this_label <- paste(this_subgroup, this_label, sep = ".") |
| 272 |
} |
|
| 273 | 172x |
this_list[[this_label]] <- this_coefs |
| 274 |
} |
|
| 275 | 128x |
overall_list <- c(overall_list, this_list) |
| 276 |
} |
|
| 277 | ||
| 278 | 25x |
grid <- data.frame(arm = arm_vec, visit = visit_vec) |
| 279 | 25x |
if (!is.null(vars$subgroup)) {
|
| 280 | 7x |
grid[[vars$subgroup]] <- subgroup_vec |
| 281 |
} |
|
| 282 | 25x |
names(grid) <- c(vars$arm, vars$visit, vars$subgroup) |
| 283 | 25x |
list(coefs = overall_list, grid = grid) |
| 284 |
} |
|
| 285 | ||
| 286 |
#' @describeIn lsmeans_helpers constructs average visits contrast specifications, |
|
| 287 |
#' given the `specs` for single visit contrasts and the averages required. |
|
| 288 |
h_average_visit_contrast_specs <- function(specs, averages, vars) {
|
|
| 289 | 9x |
subgroup_arm_visit_grid <- specs$grid |
| 290 | 9x |
arm_levels <- unique(subgroup_arm_visit_grid[[vars$arm]]) |
| 291 | 9x |
if (!is.null(vars$subgroup)) {
|
| 292 | 2x |
subgroup_levels <- unique(subgroup_arm_visit_grid[[vars$subgroup]]) |
| 293 |
} |
|
| 294 | 9x |
subgroup_arm_visit_grid$index <- seq_len(nrow(subgroup_arm_visit_grid)) |
| 295 | 9x |
grid_by_arm_subgroup <- split(subgroup_arm_visit_grid, subgroup_arm_visit_grid[c(vars$arm, vars$subgroup)]) |
| 296 | 9x |
overall_list <- list() |
| 297 | 9x |
subgroup_vec <- arm_vec <- visit_vec <- c() |
| 298 | 9x |
for (j in seq_along(grid_by_arm_subgroup)) {
|
| 299 | 12x |
this_grid <- grid_by_arm_subgroup[[j]] |
| 300 | 12x |
this_arm <- as.character(unique(this_grid[[vars$arm]])) |
| 301 | 12x |
checkmate::assert_true(length(this_arm) == 1L) |
| 302 | 12x |
if (!is.null(vars$subgroup)) {
|
| 303 | 4x |
this_subgroup <- as.character(unique(this_grid[[vars$subgroup]])) |
| 304 | 4x |
checkmate::assert_true(length(this_subgroup) == 1L) |
| 305 |
} |
|
| 306 | 12x |
for (i in seq_along(averages)) {
|
| 307 | 19x |
average_label <- names(averages)[i] |
| 308 | 19x |
visits_average <- averages[[i]] |
| 309 | 19x |
which_visits_in_average <- this_grid[, vars$visit] %in% visits_average |
| 310 | 19x |
averaged_indices <- this_grid$index[which_visits_in_average] |
| 311 | 19x |
this_comb <- paste(this_arm, average_label, sep = ".") |
| 312 | 19x |
if (!is.null(vars$subgroup)) {
|
| 313 | 8x |
this_comb <- paste(this_subgroup, this_comb, sep = ".") |
| 314 |
} |
|
| 315 | 19x |
averaged_coefs <- colMeans(do.call(rbind, specs$coefs[averaged_indices])) |
| 316 | 19x |
overall_list[[this_comb]] <- averaged_coefs |
| 317 | 19x |
arm_vec <- c(arm_vec, this_arm) |
| 318 | 19x |
if (!is.null(vars$subgroup)) {
|
| 319 | 8x |
subgroup_vec <- c(subgroup_vec, this_subgroup) |
| 320 |
} |
|
| 321 | 19x |
visit_vec <- c(visit_vec, average_label) |
| 322 |
} |
|
| 323 |
} |
|
| 324 | ||
| 325 | 9x |
grid <- data.frame(arm = arm_vec, visit = visit_vec) |
| 326 | 9x |
if (!is.null(vars$subgroup)) {
|
| 327 | 2x |
grid[[vars$subgroup]] <- subgroup_vec |
| 328 |
} |
|
| 329 | 9x |
names(grid) <- c(vars$arm, vars$visit, vars$subgroup) |
| 330 | ||
| 331 | 9x |
list(coefs = overall_list, grid = grid) |
| 332 |
} |
|
| 333 | ||
| 334 |
#' @describeIn lsmeans_helpers Computes multiplicity-adjusted p-values and |
|
| 335 |
#' confidence intervals for treatment-vs-reference contrasts using `multcomp`. |
|
| 336 |
#' Returns a `data.frame` with the arm and visit grid columns plus |
|
| 337 |
#' `p_value`, `p_value_less`, `p_value_greater`, `lower_cl`, and `upper_cl`. |
|
| 338 |
#' |
|
| 339 |
#' @param contrast_df (`numeric`) |
|
| 340 |
#' Degrees of freedom from the unadjusted contrast estimates, used to derive |
|
| 341 |
#' a single df for [emmeans::as.glht()]. |
|
| 342 |
h_get_mult_adj_estimates <- function(emmeans_res, vars, mult_adj, conf_level, contrast_df) {
|
|
| 343 | 5x |
checkmate::assert_list(emmeans_res) |
| 344 | 5x |
checkmate::assert_list(vars) |
| 345 | 5x |
checkmate::assert_string(mult_adj) |
| 346 | 5x |
checkmate::assert_number(conf_level) |
| 347 | 5x |
checkmate::assert_numeric(contrast_df) |
| 348 | ||
| 349 | 5x |
emmeans_conts <- emmeans::contrast( |
| 350 | 5x |
emmeans_res$object, |
| 351 | 5x |
method = "trt.vs.ctrl", |
| 352 | 5x |
by = c(vars$subgroup, vars$visit), |
| 353 | 5x |
adjust = "none" # To avoid a note below. Does not matter. |
| 354 |
) |
|
| 355 | 5x |
if (!requireNamespace("multcomp", quietly = TRUE)) {
|
| 356 | ! |
stop("please install multcomp for multiplicity adjustment")
|
| 357 |
} |
|
| 358 | 5x |
glht_type <- switch(mult_adj, |
| 359 | 5x |
dunnett = "single-step", |
| 360 | 5x |
`step-down-dunnett` = "free" |
| 361 |
) |
|
| 362 | 5x |
glht_test <- multcomp::adjusted(type = glht_type) |
| 363 |
# We need a single df for the as.glht below. We give it explicitly to avoid a message. |
|
| 364 | 5x |
glht_df <- floor(mean(contrast_df, na.rm = TRUE)) |
| 365 | ||
| 366 | 5x |
glht_conts_greater <- emmeans::as.glht(emmeans_conts, df = glht_df, alternative = "greater") |
| 367 | 5x |
glht_summary_greater <- summary(glht_conts_greater, test = glht_test) |
| 368 | 5x |
pvals_greater <- do.call(c, lapply(glht_summary_greater, function(x) x$test$pvalues)) |
| 369 | ||
| 370 | 5x |
glht_conts_lower <- emmeans::as.glht(emmeans_conts, df = glht_df, alternative = "less") |
| 371 | 5x |
glht_summary_lower <- summary(glht_conts_lower, test = glht_test) |
| 372 | 5x |
pvals_lower <- do.call(c, lapply(glht_summary_lower, function(x) x$test$pvalues)) |
| 373 | ||
| 374 | 5x |
glht_conts <- emmeans::as.glht(emmeans_conts, df = glht_df, alternative = "two.sided") |
| 375 | 5x |
glht_summary <- summary(glht_conts, test = glht_test) |
| 376 | 5x |
pvals <- do.call(c, lapply(glht_summary, function(x) x$test$pvalues)) |
| 377 | ||
| 378 | 5x |
glht_ci <- stats::confint(glht_conts, level = conf_level) |
| 379 | 5x |
glht_ci_matrix <- do.call(rbind, lapply(glht_ci, function(x) x$confint[, c("lwr", "upr")]))
|
| 380 | ||
| 381 | 5x |
arm_levels <- emmeans_res$object@levels[[vars$arm]] |
| 382 | 5x |
non_ref_arms <- arm_levels[-1L] |
| 383 | ||
| 384 | 5x |
grid <- emmeans_conts@grid |
| 385 | 5x |
arm_vec <- h_partial_match(options = non_ref_arms, x = as.character(grid$contrast)) |
| 386 | 5x |
grid <- cbind( |
| 387 | 5x |
arm = arm_vec, |
| 388 | 5x |
grid[c(vars$subgroup, vars$visit)] |
| 389 |
) |
|
| 390 | 5x |
names(grid) <- c(vars$arm, vars$subgroup, vars$visit) |
| 391 | ||
| 392 | 5x |
data.frame( |
| 393 | 5x |
grid, |
| 394 | 5x |
p_value = pvals, |
| 395 | 5x |
p_value_less = pvals_lower, |
| 396 | 5x |
p_value_greater = pvals_greater, |
| 397 | 5x |
lower_cl = glht_ci_matrix[, "lwr"], |
| 398 | 5x |
upper_cl = glht_ci_matrix[, "upr"], |
| 399 | 5x |
row.names = NULL |
| 400 |
) |
|
| 401 |
} |
| 1 |
#' Odds ratio estimation |
|
| 2 |
#' |
|
| 3 |
#' @description `r lifecycle::badge('stable')`
|
|
| 4 |
#' A set of functions for Odds-Ratio (OR) calculation. |
|
| 5 |
#' |
|
| 6 |
#' @param method (`string`)\cr whether to use the correct (`'exact'`) calculation in the conditional likelihood or one |
|
| 7 |
#' of the approximations, or the CMH method. See [survival::clogit()] for details. |
|
| 8 |
#' |
|
| 9 |
#' @param df (`data.frame`)\cr input data frame. |
|
| 10 |
#' @param .var (`string`)\cr name of the response variable. |
|
| 11 |
#' @param .df_row (`data.frame`)\cr data frame containing all rows. |
|
| 12 |
#' @param ref_path (`character`)\cr path to the reference group. |
|
| 13 |
#' @param .spl_context (`environment`)\cr split context environment. |
|
| 14 |
#' @param ... Additional arguments passed to the statistics function. |
|
| 15 |
#' @param .stats (`character`)\cr statistics to calculate. |
|
| 16 |
#' @param .formats (`list`)\cr formats for the statistics. |
|
| 17 |
#' @param .labels (`list`)\cr labels for the statistics. |
|
| 18 |
#' @param .indent_mods (`list`)\cr indentation modifications for the statistics. |
|
| 19 |
#' @param .ref_group (`data.frame`)\cr reference group data frame. |
|
| 20 |
#' @param .in_ref_col (`logical`)\cr whether the current column is the reference column. |
|
| 21 |
#' @param variables (`list`)\cr list with arm and strata variable names. |
|
| 22 |
#' @param conf_level (`numeric`)\cr confidence level for the confidence interval. |
|
| 23 |
#' @param groups_list (`list`)\cr list of groups for combination. |
|
| 24 |
#' |
|
| 25 |
#' @note |
|
| 26 |
#' The `a_odds_ratio_j()` and `s_odds_ratio_j()` functions have the `_j` suffix to distinguish them |
|
| 27 |
#' from [tern::a_odds_ratio()] and [tern::s_odds_ratio()], respectively. |
|
| 28 |
#' These functions differ as follows: |
|
| 29 |
#' |
|
| 30 |
#' * Additional `method = 'cmh'` option is provided to calculate the Cochran-Mantel-Haenszel estimate. |
|
| 31 |
#' * The p-value is returned as an additional statistic. |
|
| 32 |
#' |
|
| 33 |
#' Once these updates are contributed back to `tern`, they can later be replaced by the `tern` versions. |
|
| 34 |
#' |
|
| 35 |
#' @name odds_ratio |
|
| 36 |
#' @order 1 |
|
| 37 |
NULL |
|
| 38 | ||
| 39 |
#' @describeIn odds_ratio Statistics function which estimates the odds ratio |
|
| 40 |
#' between a treatment and a control. A `variables` list with `arm` and `strata` |
|
| 41 |
#' variable names must be passed if a stratified analysis is required. |
|
| 42 |
#' |
|
| 43 |
#' @param na_if_no_events (`flag`)\cr whether the point estimate should be `NA` if there |
|
| 44 |
#' are no events in one arm. The p-value and confidence interval will still be computed. |
|
| 45 |
#' |
|
| 46 |
#' @return |
|
| 47 |
#' * `s_odds_ratio_j()` returns a named list with the statistics `or_ci` |
|
| 48 |
#' (containing `est`, `lcl`, and `ucl`), `pval` and `n_tot`. |
|
| 49 |
#' |
|
| 50 |
#' @examples |
|
| 51 |
#' s_odds_ratio_j( |
|
| 52 |
#' df = subset(dta, grp == "A"), |
|
| 53 |
#' .var = "rsp", |
|
| 54 |
#' .ref_group = subset(dta, grp == "B"), |
|
| 55 |
#' .in_ref_col = FALSE, |
|
| 56 |
#' .df_row = dta |
|
| 57 |
#' ) |
|
| 58 |
#' |
|
| 59 |
#' s_odds_ratio_j( |
|
| 60 |
#' df = subset(dta, grp == "A"), |
|
| 61 |
#' .var = "rsp", |
|
| 62 |
#' .ref_group = subset(dta, grp == "B"), |
|
| 63 |
#' .in_ref_col = FALSE, |
|
| 64 |
#' .df_row = dta, |
|
| 65 |
#' variables = list(arm = "grp", strata = "strata") |
|
| 66 |
#' ) |
|
| 67 |
#' |
|
| 68 |
#' s_odds_ratio_j( |
|
| 69 |
#' df = subset(dta, grp == "A"), |
|
| 70 |
#' method = "cmh", |
|
| 71 |
#' .var = "rsp", |
|
| 72 |
#' .ref_group = subset(dta, grp == "B"), |
|
| 73 |
#' .in_ref_col = FALSE, |
|
| 74 |
#' .df_row = dta, |
|
| 75 |
#' variables = list(arm = "grp", strata = c("strata"))
|
|
| 76 |
#' ) |
|
| 77 |
#' @export |
|
| 78 |
s_odds_ratio_j <- function( |
|
| 79 |
df, |
|
| 80 |
.var, |
|
| 81 |
.ref_group, |
|
| 82 |
.in_ref_col, |
|
| 83 |
.df_row, |
|
| 84 |
variables = list(arm = NULL, strata = NULL), |
|
| 85 |
conf_level = 0.95, |
|
| 86 |
groups_list = NULL, |
|
| 87 |
na_if_no_events = TRUE, |
|
| 88 |
method = c("exact", "approximate", "efron", "breslow", "cmh")) {
|
|
| 89 | 17x |
checkmate::assert_flag(na_if_no_events) |
| 90 |
# New: pval here |
|
| 91 | 17x |
y <- list(or_ci = list(), n_tot = list(), pval = list()) |
| 92 | 17x |
method <- match.arg(method) |
| 93 | 17x |
one_group_empty <- nrow(df) == 0 || nrow(.ref_group) == 0 |
| 94 | ||
| 95 | 17x |
if (!.in_ref_col) {
|
| 96 | 15x |
(assert_proportion_value)(conf_level) |
| 97 | 15x |
assert_df_with_variables(df, list(rsp = .var)) |
| 98 | 15x |
assert_df_with_variables(.ref_group, list(rsp = .var)) |
| 99 | ||
| 100 | 15x |
if (one_group_empty) {
|
| 101 | 1x |
y <- list(or_ci = c(est = NA_real_, lcl = 0, ucl = Inf), n_tot = c(n_tot = nrow(df) + nrow(.ref_group)), pval = 1) |
| 102 | 14x |
} else if (is.null(variables$strata)) {
|
| 103 | 6x |
data <- data.frame( |
| 104 | 6x |
rsp = c(.ref_group[[.var]], df[[.var]]), |
| 105 | 6x |
grp = factor(rep(c("ref", "Not-ref"), c(nrow(.ref_group), nrow(df))), levels = c("ref", "Not-ref"))
|
| 106 |
) |
|
| 107 | 6x |
y <- or_glm_j(data, conf_level = conf_level) |
| 108 |
} else {
|
|
| 109 | 8x |
assert_df_with_variables(.df_row, c(list(rsp = .var), variables)) |
| 110 | 8x |
checkmate::assert_subset(method, c("exact", "approximate", "efron", "breslow", "cmh"), empty.ok = FALSE)
|
| 111 |
# The group variable prepared for stratified analysis must be synchronized with the combination groups |
|
| 112 |
# definition. |
|
| 113 | 8x |
if (is.null(groups_list)) {
|
| 114 | 8x |
ref_grp <- as.character(unique(.ref_group[[variables$arm]])) |
| 115 | 8x |
trt_grp <- as.character(unique(df[[variables$arm]])) |
| 116 | 8x |
grp <- stats::relevel(factor(.df_row[[variables$arm]]), ref = ref_grp) |
| 117 |
} else {
|
|
| 118 |
# If more than one level in reference col. |
|
| 119 | ! |
reference <- as.character(unique(.ref_group[[variables$arm]])) |
| 120 | ! |
grp_ref_flag <- vapply(X = groups_list, FUN.VALUE = TRUE, FUN = function(x) all(reference %in% x)) |
| 121 | ! |
ref_grp <- names(groups_list)[grp_ref_flag] |
| 122 | ||
| 123 |
# If more than one level in treatment col. |
|
| 124 | ! |
treatment <- as.character(unique(df[[variables$arm]])) |
| 125 | ! |
grp_trt_flag <- vapply(X = groups_list, FUN.VALUE = TRUE, FUN = function(x) all(treatment %in% x)) |
| 126 | ! |
trt_grp <- names(groups_list)[grp_trt_flag] |
| 127 | ||
| 128 | ! |
grp <- combine_levels(.df_row[[variables$arm]], levels = reference, new_level = ref_grp) |
| 129 | ! |
grp <- combine_levels(grp, levels = treatment, new_level = trt_grp) |
| 130 |
} |
|
| 131 | ||
| 132 |
# New: CMH method |
|
| 133 | 8x |
if (method == "cmh") {
|
| 134 | 7x |
data <- data.frame( |
| 135 | 7x |
rsp = c(.ref_group[[.var]], df[[.var]]), |
| 136 | 7x |
grp = factor(rep(c("ref", "Not-ref"), c(nrow(.ref_group), nrow(df))), levels = c("ref", "Not-ref")),
|
| 137 | 7x |
strata = interaction(rbind(.ref_group[variables$strata], df[variables$strata])) |
| 138 |
) |
|
| 139 | ||
| 140 | 7x |
y <- or_cmh(data, conf_level = conf_level) |
| 141 |
} else {
|
|
| 142 |
# The reference level in `grp` must be the same as in the `rtables` column split. |
|
| 143 | 1x |
data <- data.frame(rsp = .df_row[[.var]], grp = grp, strata = interaction(.df_row[variables$strata])) |
| 144 | ||
| 145 | 1x |
y_all <- or_clogit_j(data, conf_level = conf_level, method = method) |
| 146 | 1x |
checkmate::assert_string(trt_grp) |
| 147 |
# New: pval here |
|
| 148 | 1x |
checkmate::assert_subset(trt_grp, names(y_all$or_ci_pvals)) |
| 149 | 1x |
y_or_ci_pval <- y_all$or_ci_pvals[[trt_grp]] |
| 150 | 1x |
y$or_ci <- y_or_ci_pval[c("est", "lcl", "ucl")]
|
| 151 | 1x |
y$n_tot <- y_all$n_tot |
| 152 | 1x |
y$pval <- y_or_ci_pval["pval"] |
| 153 |
} |
|
| 154 |
} |
|
| 155 | ||
| 156 | 15x |
one_group_no_events <- (sum(.ref_group[[.var]]) == 0) || (sum(df[[.var]]) == 0) |
| 157 | 15x |
if (na_if_no_events && one_group_no_events) {
|
| 158 | 4x |
y$or_ci[["est"]] <- NA_real_ |
| 159 |
} |
|
| 160 | ||
| 161 | 15x |
na_because_sparse <- one_group_empty || (na_if_no_events && one_group_no_events) |
| 162 | 15x |
if ("est" %in% names(y$or_ci) && is.na(y$or_ci[["est"]]) && !na_because_sparse && method != "approximate") {
|
| 163 | ! |
warning( |
| 164 | ! |
"Unable to compute the odds ratio estimate. Please try re-running the function with ", |
| 165 | ! |
"parameter `method` set to \"approximate\"." |
| 166 |
) |
|
| 167 |
} |
|
| 168 |
} |
|
| 169 | ||
| 170 | 17x |
y$or_ci <- with_label(x = y$or_ci, label = paste0("Odds Ratio (", 100 * conf_level, "% CI)"))
|
| 171 | ||
| 172 |
# New: pval here |
|
| 173 | 17x |
y$pval <- unname(y$pval) |
| 174 | ||
| 175 | 17x |
y |
| 176 |
} |
|
| 177 | ||
| 178 |
#' @describeIn odds_ratio Formatted analysis function which is used as `afun`. Note that the |
|
| 179 |
#' junco specific `ref_path` and `.spl_context` arguments are used for reference column information. |
|
| 180 |
#' |
|
| 181 |
#' @return |
|
| 182 |
#' * `a_odds_ratio_j()` returns the corresponding list with formatted [rtables::CellValue()]. |
|
| 183 |
#' |
|
| 184 |
#' @examples |
|
| 185 |
#' set.seed(12) |
|
| 186 |
#' dta <- data.frame( |
|
| 187 |
#' rsp = sample(c(TRUE, FALSE), 100, TRUE), |
|
| 188 |
#' grp = factor(rep(c("A", "B"), each = 50), levels = c("A", "B")),
|
|
| 189 |
#' strata = factor(sample(c("C", "D"), 100, TRUE))
|
|
| 190 |
#' ) |
|
| 191 |
#' |
|
| 192 |
#' a_odds_ratio_j( |
|
| 193 |
#' df = subset(dta, grp == "A"), |
|
| 194 |
#' .var = "rsp", |
|
| 195 |
#' ref_path = c("grp", "B"),
|
|
| 196 |
#' .spl_context = data.frame( |
|
| 197 |
#' cur_col_split = I(list("grp")),
|
|
| 198 |
#' cur_col_split_val = I(list(c(grp = "A"))), |
|
| 199 |
#' full_parent_df = I(list(dta)) |
|
| 200 |
#' ), |
|
| 201 |
#' .df_row = dta |
|
| 202 |
#' ) |
|
| 203 |
#' |
|
| 204 |
#' |
|
| 205 |
#' l <- basic_table() |> |
|
| 206 |
#' split_cols_by(var = "grp") |> |
|
| 207 |
#' analyze( |
|
| 208 |
#' "rsp", |
|
| 209 |
#' afun = a_odds_ratio_j, |
|
| 210 |
#' show_labels = "hidden", |
|
| 211 |
#' extra_args = list( |
|
| 212 |
#' ref_path = c("grp", "B"),
|
|
| 213 |
#' .stats = c("or_ci", "pval")
|
|
| 214 |
#' ) |
|
| 215 |
#' ) |
|
| 216 |
#' |
|
| 217 |
#' build_table(l, df = dta) |
|
| 218 |
#' |
|
| 219 |
#' l2 <- basic_table() |> |
|
| 220 |
#' split_cols_by(var = "grp") |> |
|
| 221 |
#' analyze( |
|
| 222 |
#' "rsp", |
|
| 223 |
#' afun = a_odds_ratio_j, |
|
| 224 |
#' show_labels = "hidden", |
|
| 225 |
#' extra_args = list( |
|
| 226 |
#' variables = list(arm = "grp", strata = "strata"), |
|
| 227 |
#' method = "cmh", |
|
| 228 |
#' ref_path = c("grp", "A"),
|
|
| 229 |
#' .stats = c("or_ci", "pval")
|
|
| 230 |
#' ) |
|
| 231 |
#' ) |
|
| 232 |
#' |
|
| 233 |
#' build_table(l2, df = dta) |
|
| 234 |
#' @export |
|
| 235 |
#' @order 2 |
|
| 236 |
a_odds_ratio_j <- function( |
|
| 237 |
df, |
|
| 238 |
.var, |
|
| 239 |
.df_row, |
|
| 240 |
ref_path, |
|
| 241 |
.spl_context, |
|
| 242 |
..., |
|
| 243 |
.stats = NULL, |
|
| 244 |
.formats = NULL, |
|
| 245 |
.labels = NULL, |
|
| 246 |
.indent_mods = NULL) {
|
|
| 247 |
# Check for additional parameters to the statistics function |
|
| 248 | 4x |
dots_extra_args <- list(...) |
| 249 | ||
| 250 |
# Only support default stats, not custom stats |
|
| 251 | 4x |
.stats <- .split_std_from_custom_stats(.stats)$default_stats |
| 252 | ||
| 253 |
# Obtain reference column information |
|
| 254 | 4x |
ref <- get_ref_info(ref_path, .spl_context) |
| 255 | ||
| 256 |
# Apply statistics function |
|
| 257 | 4x |
x_stats <- .apply_stat_functions( |
| 258 | 4x |
default_stat_fnc = s_odds_ratio_j, |
| 259 | 4x |
custom_stat_fnc_list = NULL, |
| 260 | 4x |
args_list = c( |
| 261 | 4x |
df = list(df), |
| 262 | 4x |
.var = .var, |
| 263 | 4x |
.df_row = list(.df_row), |
| 264 | 4x |
.ref_group = list(ref$ref_group), |
| 265 | 4x |
.in_ref_col = ref$in_ref_col, |
| 266 | 4x |
dots_extra_args |
| 267 |
) |
|
| 268 |
) |
|
| 269 | ||
| 270 |
# Format according to specifications |
|
| 271 | 4x |
format_stats( |
| 272 | 4x |
x_stats, |
| 273 | 4x |
method_groups = "odds_ratio", |
| 274 | 4x |
stats_in = .stats, |
| 275 | 4x |
formats_in = .formats, |
| 276 | 4x |
labels_in = .labels, |
| 277 | 4x |
indents_in = .indent_mods |
| 278 |
) |
|
| 279 |
} |
|
| 280 | ||
| 281 |
#' Helper functions for odds ratio estimation |
|
| 282 |
#' |
|
| 283 |
#' @description `r lifecycle::badge('stable')`
|
|
| 284 |
#' |
|
| 285 |
#' Functions to calculate odds ratios in [s_odds_ratio_j()]. |
|
| 286 |
#' |
|
| 287 |
#' @inheritParams odds_ratio |
|
| 288 |
#' @inheritParams proposal_argument_convention |
|
| 289 |
#' @param data (`data.frame`)\cr data frame containing at least the variables `rsp` and `grp`, and optionally |
|
| 290 |
#' `strata` for [or_clogit_j()]. |
|
| 291 |
#' @return A named `list` of elements `or_ci`, `n_tot` and `pval`. |
|
| 292 |
#' |
|
| 293 |
#' @seealso [odds_ratio] |
|
| 294 |
#' |
|
| 295 |
#' @name h_odds_ratio |
|
| 296 |
NULL |
|
| 297 | ||
| 298 |
#' @describeIn h_odds_ratio Estimates the odds ratio based on [stats::glm()]. Note that there must be |
|
| 299 |
#' exactly 2 groups in `data` as specified by the `grp` variable. |
|
| 300 |
#' |
|
| 301 |
#' @examples |
|
| 302 |
#' data <- data.frame( |
|
| 303 |
#' rsp = as.logical(c(1, 1, 0, 1, 0, 0, 1, 1)), |
|
| 304 |
#' grp = letters[c(1, 1, 1, 2, 2, 2, 1, 2)], |
|
| 305 |
#' strata = letters[c(1, 2, 1, 2, 2, 2, 1, 2)], |
|
| 306 |
#' stringsAsFactors = TRUE |
|
| 307 |
#' ) |
|
| 308 |
#' |
|
| 309 |
#' or_glm_j(data, conf_level = 0.95) |
|
| 310 |
#' |
|
| 311 |
#' @export |
|
| 312 |
or_glm_j <- function(data, conf_level) {
|
|
| 313 | 7x |
checkmate::assert_logical(data$rsp) |
| 314 | 7x |
(assert_proportion_value)(conf_level) |
| 315 | 7x |
assert_df_with_variables(data, list(rsp = "rsp", grp = "grp")) |
| 316 | 7x |
checkmate::assert_multi_class(data$grp, classes = c("factor", "character"))
|
| 317 | ||
| 318 | 7x |
data$grp <- as_factor_keep_attributes(data$grp) |
| 319 | 7x |
assert_df_with_factors(data, list(val = "grp"), min.levels = 2, max.levels = 2) |
| 320 | 7x |
formula <- stats::as.formula("rsp ~ grp")
|
| 321 | 7x |
model_fit <- stats::glm(formula = formula, data = data, family = stats::binomial(link = "logit")) |
| 322 | ||
| 323 |
# Note that here we need to discard the intercept. |
|
| 324 | 7x |
or <- exp(stats::coef(model_fit)[-1]) |
| 325 | 7x |
or_ci <- exp(stats::confint.default(model_fit, level = conf_level)[-1, , drop = FALSE]) |
| 326 | ||
| 327 | 7x |
values <- stats::setNames(c(or, or_ci), c("est", "lcl", "ucl"))
|
| 328 | 7x |
n_tot <- stats::setNames(nrow(model_fit$model), "n_tot") |
| 329 | ||
| 330 |
# New: pval here |
|
| 331 | 7x |
pval <- summary(model_fit)$coef[-1, "Pr(>|z|)"] |
| 332 | ||
| 333 | 7x |
list(or_ci = values, n_tot = n_tot, pval = pval) |
| 334 |
} |
|
| 335 | ||
| 336 |
#' @describeIn h_odds_ratio Estimates the odds ratio based on [survival::clogit()]. This is done for |
|
| 337 |
#' the whole data set including all groups, since the results are not the same as when doing |
|
| 338 |
#' pairwise comparisons between the groups. |
|
| 339 |
#' |
|
| 340 |
#' @examples |
|
| 341 |
#' data <- data.frame( |
|
| 342 |
#' rsp = as.logical(c(1, 1, 0, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 0)), |
|
| 343 |
#' grp = letters[c(1, 1, 1, 2, 2, 2, 3, 3, 3, 3, 1, 1, 1, 2, 2, 2, 3, 3, 3, 3)], |
|
| 344 |
#' strata = LETTERS[c(1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2)], |
|
| 345 |
#' stringsAsFactors = TRUE |
|
| 346 |
#' ) |
|
| 347 |
#' |
|
| 348 |
#' or_clogit_j(data, conf_level = 0.95) |
|
| 349 |
#' |
|
| 350 |
#' @export |
|
| 351 |
#' @importFrom survival Surv |
|
| 352 |
or_clogit_j <- function(data, conf_level, method = "exact") {
|
|
| 353 | 1x |
checkmate::assert_logical(data$rsp) |
| 354 | 1x |
(assert_proportion_value)(conf_level) |
| 355 | 1x |
assert_df_with_variables(data, list(rsp = "rsp", grp = "grp", strata = "strata")) |
| 356 | 1x |
checkmate::assert_multi_class(data$grp, classes = c("factor", "character"))
|
| 357 | 1x |
checkmate::assert_multi_class(data$strata, classes = c("factor", "character"))
|
| 358 | 1x |
checkmate::assert_subset(method, c("exact", "approximate", "efron", "breslow"), empty.ok = FALSE)
|
| 359 | ||
| 360 | 1x |
data$grp <- as_factor_keep_attributes(data$grp) |
| 361 | 1x |
data$strata <- as_factor_keep_attributes(data$strata) |
| 362 | ||
| 363 |
# Deviation from convention: `survival::strata` must be simply `strata`. |
|
| 364 | 1x |
strata <- survival::strata |
| 365 | 1x |
formula <- stats::as.formula("rsp ~ grp + strata(strata)")
|
| 366 | 1x |
model_fit <- clogit_with_tryCatch( |
| 367 | 1x |
formula = formula, |
| 368 | 1x |
data = data, |
| 369 | 1x |
method = method |
| 370 |
) |
|
| 371 | ||
| 372 |
# New: pval here |
|
| 373 | ||
| 374 |
# Create a list with one set of OR estimates, CI and p-value per coefficient, i.e. comparison of one group vs. the |
|
| 375 |
# reference group. |
|
| 376 | 1x |
coef_est <- stats::coef(model_fit) |
| 377 | 1x |
ci_est <- stats::confint(model_fit, level = conf_level) |
| 378 | 1x |
pvals <- summary(model_fit)$coef[, "Pr(>|z|)", drop = FALSE] |
| 379 | 1x |
or_ci_pvals <- list() |
| 380 | 1x |
for (coef_name in names(coef_est)) {
|
| 381 | 1x |
grp_name <- gsub("^grp", "", x = coef_name)
|
| 382 | 1x |
or_ci_pvals[[grp_name]] <- stats::setNames( |
| 383 | 1x |
object = c(exp(c(coef_est[coef_name], ci_est[coef_name, , drop = TRUE])), pvals[coef_name, 1]), |
| 384 | 1x |
nm = c("est", "lcl", "ucl", "pval")
|
| 385 |
) |
|
| 386 |
} |
|
| 387 | 1x |
list(or_ci_pvals = or_ci_pvals, n_tot = c(n_tot = model_fit$n)) |
| 388 |
} |
|
| 389 | ||
| 390 |
#' @describeIn h_odds_ratio Estimates the odds ratio based on CMH. Note that there must be |
|
| 391 |
#' exactly 2 groups in `data` as specified by the `grp` variable. |
|
| 392 |
#' |
|
| 393 |
#' @examples |
|
| 394 |
#' set.seed(123) |
|
| 395 |
#' data <- data.frame( |
|
| 396 |
#' rsp = as.logical(rbinom(n = 40, size = 1, prob = 0.5)), |
|
| 397 |
#' grp = letters[sample(1:2, size = 40, replace = TRUE)], |
|
| 398 |
#' strata = LETTERS[sample(1:2, size = 40, replace = TRUE)], |
|
| 399 |
#' stringsAsFactors = TRUE |
|
| 400 |
#' ) |
|
| 401 |
#' |
|
| 402 |
#' or_cmh(data, conf_level = 0.95) |
|
| 403 |
#' |
|
| 404 |
#' @export |
|
| 405 |
or_cmh <- function(data, conf_level) {
|
|
| 406 | 7x |
checkmate::assert_logical(data$rsp) |
| 407 | 7x |
assert_proportion_value(conf_level) |
| 408 | 7x |
assert_df_with_variables(data, list(rsp = "rsp", grp = "grp")) |
| 409 | 7x |
checkmate::assert_multi_class(data$grp, classes = c("factor", "character"))
|
| 410 | ||
| 411 | 7x |
data$grp <- as_factor_keep_attributes(data$grp) |
| 412 | 7x |
assert_df_with_factors(data, list(val = "grp"), min.levels = 2, max.levels = 2) |
| 413 | ||
| 414 |
# first dimension: treatment, control - therefore we reverse the levels order 2nd dimension: TRUE, FALSE 3rd |
|
| 415 |
# dimension: levels of strata rsp as factor rsp to handle edge case of no FALSE (or TRUE) rsp records |
|
| 416 | 7x |
t_tbl <- table( |
| 417 | 7x |
factor(data$grp, levels = rev(levels(data$grp))), |
| 418 | 7x |
factor(data$rsp, levels = c("TRUE", "FALSE")),
|
| 419 | 7x |
data$strata |
| 420 |
) |
|
| 421 | ||
| 422 | 7x |
trt_ind <- 1 |
| 423 | 7x |
ctrl_ind <- 2 |
| 424 | ||
| 425 | 7x |
resp_ind <- 1 |
| 426 | 7x |
nonresp_ind <- 2 |
| 427 | ||
| 428 | 7x |
n1resp <- t_tbl[trt_ind, resp_ind, ] |
| 429 | 7x |
n2resp <- t_tbl[ctrl_ind, resp_ind, ] |
| 430 | 7x |
n1non <- t_tbl[trt_ind, nonresp_ind, ] |
| 431 | 7x |
n2non <- t_tbl[ctrl_ind, nonresp_ind, ] |
| 432 |
# Example: n2non are the control patient non-responders per stratum. |
|
| 433 | ||
| 434 |
# CMH statistic for odds ratio, Treatment over Control |
|
| 435 | 7x |
use_stratum <- (n1resp + n2resp + n1non + n2non) > 0 |
| 436 | 7x |
n1resp <- n1resp[use_stratum] |
| 437 | 7x |
n2resp <- n2resp[use_stratum] |
| 438 | 7x |
n1non <- n1non[use_stratum] |
| 439 | 7x |
n2non <- n2non[use_stratum] |
| 440 | 7x |
n <- n1resp + n2resp + n1non + n2non |
| 441 | ||
| 442 | 7x |
or_num <- sum(n1resp * n2non / n) |
| 443 | 7x |
or_denom <- sum(n1non * n2resp / n) |
| 444 | 7x |
log_or <- log(or_num) - log(or_denom) |
| 445 | 7x |
or <- exp(log_or) |
| 446 | ||
| 447 | 7x |
term1_num <- sum((n1resp + n2non) * n1resp * n2non / n^2) |
| 448 | 7x |
term1_denom <- 2 * (sum(n1resp * n2non / n))^2 |
| 449 | ||
| 450 | 7x |
term2_num <- sum(((n1resp + n2non) * n1non * n2resp + (n1non + n2resp) * n1resp * n2non) / n^2) |
| 451 | 7x |
term2_denom <- 2 * (sum(n1resp * n2non / n)) * (sum(n1non * n2resp / n)) |
| 452 | ||
| 453 | 7x |
term3_num <- sum((n1non + n2resp) * n1non * n2resp / n^2) |
| 454 | 7x |
term3_denom <- 2 * (sum(n1non * n2resp / n))^2 |
| 455 | ||
| 456 | 7x |
var_log_or <- term1_num / term1_denom + term2_num / term2_denom + term3_num / term3_denom |
| 457 | 7x |
se_log_or <- sqrt(var_log_or) |
| 458 | ||
| 459 | 7x |
z <- stats::qnorm((1 + conf_level) / 2) |
| 460 | 7x |
log_rel_or <- log_or + c(-1, +1) * z * se_log_or |
| 461 | 7x |
ci <- exp(log_rel_or) |
| 462 | 7x |
or_ci <- stats::setNames(c(or, ci), c("est", "lcl", "ucl"))
|
| 463 | ||
| 464 | 7x |
pval <- stats::mantelhaen.test(t_tbl, correct = FALSE)$p.value |
| 465 | ||
| 466 | 7x |
n_tot <- stats::setNames(sum(n), "n_tot") |
| 467 | ||
| 468 | 7x |
list(or_ci = or_ci, n_tot = n_tot, pval = pval) |
| 469 |
} |
| 1 |
#' @name a_freq_j |
|
| 2 |
#' |
|
| 3 |
#' @title Analysis/statistical function for count and percentage in core columns |
|
| 4 |
#' and (optional) relative risk columns |
|
| 5 |
#' |
|
| 6 |
#' @inheritParams proposal_argument_convention |
|
| 7 |
#' @param .var (`character(1)`)\cr Name of a categorical analysis variable in `df`. |
|
| 8 |
#' The variable must be a `factor`. |
|
| 9 |
#' @param val (`character` or NULL)\cr |
|
| 10 |
#' When NULL, all levels of the incoming variable (variable used in the `analyze` call) |
|
| 11 |
#' will be considered.\cr |
|
| 12 |
#' When a single `string`, only that current level/value of the incoming variable |
|
| 13 |
#' will be considered.\cr |
|
| 14 |
#' When multiple levels, only those levels/values of the incoming variable |
|
| 15 |
#' will be considered.\cr |
|
| 16 |
#' When no values are observed (eg zero row input df), |
|
| 17 |
#' a row with row-label `No data to report` will be included in the table. |
|
| 18 |
#' @param drop_levels (`logical`)\cr If `TRUE` non-observed levels |
|
| 19 |
#' (based upon .df_row) will not be included.\cr |
|
| 20 |
#' Cannot be used together with `val`. |
|
| 21 |
#' @param excl_levels (`character` or NULL)\cr |
|
| 22 |
#' When NULL, no levels of the incoming variable (variable used in the `analyze` call) |
|
| 23 |
#' will be excluded.\cr |
|
| 24 |
#' When multiple levels, those levels/values of the incoming variable |
|
| 25 |
#' will be excluded.\cr |
|
| 26 |
#' Cannot be used together with `val`. |
|
| 27 |
#' @param new_levels (list(2) or NULL)\cr List of length 2.\cr |
|
| 28 |
#' First element : names of the new levels\cr |
|
| 29 |
#' Second element: list with values of the new levels.\cr |
|
| 30 |
#' @param new_levels_after (`logical`)\cr If `TRUE` new levels will be added after last level. |
|
| 31 |
#' @param denom (`string`)\cr See Details. |
|
| 32 |
#' @param alt_df (`dataframe`)\cr Will be derived based upon alt_df_full and denom_by within a_freq_j. |
|
| 33 |
#' @param parent_df (`dataframe`)\cr Will be derived within a_freq_j based |
|
| 34 |
#' upon the input dataframe that goes into build_table (df) and denom_by.\cr |
|
| 35 |
#' It is a data frame in the higher row-space than the current input df |
|
| 36 |
#' (which underwent row-splitting by the rtables splitting machinery). |
|
| 37 |
#' |
|
| 38 |
#' @param countsource Either `df`, `altdf`, or `altdf_subset`.\cr |
|
| 39 |
#' When `altdf` the counts will be based upon the alternative dataframe `alt_df`.\cr |
|
| 40 |
#' When `altdf_subset` the counts will be based upon `alt_df` but first restricted\cr |
|
| 41 |
#' to the levels/values of the current row split for `.var` (or to `val` when provided).\cr |
|
| 42 |
#' This is useful for subgroup processing, |
|
| 43 |
#' to present counts of subjects in a subgroup from the alternative dataframe. |
|
| 44 |
#' |
|
| 45 |
#' @details |
|
| 46 |
#' |
|
| 47 |
#' `denom` controls the denominator used to calculate proportions/percents. |
|
| 48 |
#' It must be one of \cr |
|
| 49 |
#' \itemize{
|
|
| 50 |
#' \item \strong{N_col} Column count, \cr
|
|
| 51 |
#' \item \strong{n_df} Number of patients (based upon the main input dataframe `df`),\cr
|
|
| 52 |
#' \item \strong{n_altdf} Number of patients from the secondary dataframe (`.alt_df_full`),\cr
|
|
| 53 |
#' Note that argument `denom_by` will perform a row-split on the `.alt_df_full` dataframe.\cr |
|
| 54 |
#' It is a requirement that variables specified in `denom_by` are part of the row split specifications. \cr |
|
| 55 |
#' \item \strong{N_colgroup} Number of patients from the column group variable
|
|
| 56 |
#' (note that this is based upon the input .alt_df_full dataframe).\cr |
|
| 57 |
#' Note that the argument `colgroup` (column variable) needs to be provided, |
|
| 58 |
#' as it cannot be retrieved directly from the column layout definition. |
|
| 59 |
#' \item \strong{n_rowdf} Number of patients from the current row-level dataframe
|
|
| 60 |
#' (`.row_df` from the rtables splitting machinery).\cr |
|
| 61 |
#' \item \strong{n_parentdf} Number of patients from a higher row-level split than the current split.\cr
|
|
| 62 |
#' This higher row-level split is specified in the argument `denom_by`.\cr |
|
| 63 |
#' } |
|
| 64 |
#' |
|
| 65 |
#' @return |
|
| 66 |
#' * `s_freq_j`: returns a list of following statistics\cr |
|
| 67 |
#' \itemize{
|
|
| 68 |
#' \item n_df |
|
| 69 |
#' \item n_rowdf |
|
| 70 |
#' \item n_parentdf |
|
| 71 |
#' \item n_altdf |
|
| 72 |
#' \item denom |
|
| 73 |
#' \item count |
|
| 74 |
#' \item count_unique |
|
| 75 |
#' \item count_unique_fraction |
|
| 76 |
#' \item count_unique_denom_fraction |
|
| 77 |
#' \item unique_fraction |
|
| 78 |
#' } |
|
| 79 |
#' |
|
| 80 |
#' @export |
|
| 81 |
#' @importFrom stats setNames |
|
| 82 |
s_freq_j <- function( |
|
| 83 |
df, |
|
| 84 |
.var, |
|
| 85 |
.df_row, |
|
| 86 |
val = NULL, |
|
| 87 |
drop_levels = FALSE, |
|
| 88 |
excl_levels = NULL, |
|
| 89 |
alt_df, |
|
| 90 |
parent_df, |
|
| 91 |
id = "USUBJID", |
|
| 92 |
denom = c("n_df", "n_altdf", "N_col", "n_rowdf", "n_parentdf"),
|
|
| 93 |
.N_col, |
|
| 94 |
countsource = c("df", "altdf", "altdf_subset")
|
|
| 95 |
) {
|
|
| 96 | 571x |
if (is.na(.var) || is.null(.var)) {
|
| 97 | ! |
stop("Argument .var cannot be NA or NULL.")
|
| 98 |
} |
|
| 99 | ||
| 100 | 571x |
checkmate::assert_string(.var) |
| 101 | 571x |
checkmate::assert_string(id) |
| 102 | 571x |
checkmate::assert_subset(id, colnames(df), empty.ok = FALSE) |
| 103 | ||
| 104 | 571x |
countsource <- match.arg(countsource) |
| 105 | ||
| 106 | 571x |
if (countsource %in% c("altdf", "altdf_subset")) {
|
| 107 | 4x |
df <- alt_df |
| 108 |
} |
|
| 109 | ||
| 110 | 571x |
checkmate::assert_names(names(df), must.include = .var) |
| 111 | 571x |
checkmate::assert_class(df[[.var]], classes = "factor") |
| 112 | ||
| 113 | 571x |
.alt_df <- alt_df |
| 114 | ||
| 115 | 571x |
n1 <- length(unique(.alt_df[[id]])) |
| 116 | 571x |
n2 <- length(unique(df[[id]])) |
| 117 | ||
| 118 | 571x |
n3 <- length(unique(.df_row[[id]])) |
| 119 | ||
| 120 | 571x |
if (is.null(parent_df)) {
|
| 121 | 70x |
parent_df <- df |
| 122 |
} |
|
| 123 | 571x |
n4 <- length(unique(parent_df[[id]])) |
| 124 | ||
| 125 | ||
| 126 | 571x |
denom <- match.arg(denom) |> switch( |
| 127 | 571x |
"n_altdf" = n1, |
| 128 | 571x |
"n_df" = n2, |
| 129 | 571x |
"n_rowdf" = n3, |
| 130 | 571x |
"N_col" = .N_col, |
| 131 | 571x |
"n_parentdf" = n4 |
| 132 |
) |
|
| 133 | ||
| 134 | 571x |
y <- list() |
| 135 | ||
| 136 | 571x |
y$n_altdf <- c("n_altdf" = n1)
|
| 137 | 571x |
y$n_df <- c("n_df" = n2)
|
| 138 | 571x |
y$n_rowdf <- c("n_rowdf" = n3)
|
| 139 | 571x |
y$n_parentdf <- c("n_parentdf" = n4)
|
| 140 | 571x |
y$denom <- c("denom" = denom)
|
| 141 | ||
| 142 | 571x |
if (drop_levels) {
|
| 143 | ! |
obs_levs <- unique(.df_row[[.var]]) |
| 144 | ! |
obs_levs <- intersect(levels(.df_row[[.var]]), obs_levs) |
| 145 | ||
| 146 | ! |
if (!is.null(excl_levels)) obs_levs <- setdiff(obs_levs, excl_levels) |
| 147 | ||
| 148 | ! |
if (!is.null(val)) {
|
| 149 | ! |
stop("argument val cannot be used together with drop_levels = TRUE.")
|
| 150 |
} |
|
| 151 | ! |
val <- obs_levs |
| 152 |
} |
|
| 153 | ||
| 154 | 571x |
if (!is.null(val)) {
|
| 155 | 178x |
df <- df[df[[.var]] %in% val, ] |
| 156 | 178x |
.df_row <- .df_row[.df_row[[.var]] %in% val, ] |
| 157 | ||
| 158 | 178x |
df <- h_update_factor(df, .var, val) |
| 159 | 178x |
.df_row <- h_update_factor(.df_row, .var, val) |
| 160 |
} |
|
| 161 | ||
| 162 | 571x |
if (!is.null(excl_levels) && drop_levels == FALSE) {
|
| 163 |
# restrict the levels to the ones specified in val argument |
|
| 164 | ! |
df <- df[!(df[[.var]] %in% excl_levels), ] |
| 165 | ! |
.df_row <- .df_row[!(.df_row[[.var]] %in% excl_levels), ] |
| 166 | ||
| 167 | ! |
df <- h_update_factor(df, .var, excl_levels = excl_levels) |
| 168 | ! |
.df_row <- h_update_factor(.df_row, .var, excl_levels = excl_levels) |
| 169 |
} |
|
| 170 | ||
| 171 | 571x |
x <- df[[.var]] |
| 172 | 571x |
x_unique <- unique(df[, c(.var, id)])[[.var]] |
| 173 | ||
| 174 | 571x |
if (identical(levels(df[[.var]]), no_data_to_report_str)) {
|
| 175 | 2x |
xy <- list() |
| 176 | 2x |
nms <- c( |
| 177 | 2x |
"count", |
| 178 | 2x |
"count_unique", |
| 179 | 2x |
"count_unique_fraction", |
| 180 | 2x |
"count_unique_denom_fraction", |
| 181 | 2x |
"unique_fraction" |
| 182 |
) |
|
| 183 | 2x |
xy <- replicate(length(nms), list(setNames(list(NULL), no_data_to_report_str))) |
| 184 | 2x |
names(xy) <- nms |
| 185 | 2x |
y <- append(y, xy) |
| 186 |
} else {
|
|
| 187 | 569x |
y$count <- lapply( |
| 188 | 569x |
as.list(table(x, useNA = "ifany")), |
| 189 | 569x |
stats::setNames, |
| 190 | 569x |
nm = "count" |
| 191 |
) |
|
| 192 | ||
| 193 | 569x |
y$count_unique <- lapply( |
| 194 | 569x |
as.list(table(x_unique, useNA = "ifany")), |
| 195 | 569x |
stats::setNames, |
| 196 | 569x |
nm = "count_unique" |
| 197 |
) |
|
| 198 | ||
| 199 | 569x |
y$count_unique_fraction <- lapply( |
| 200 | 569x |
y$count_unique, |
| 201 | 569x |
function(x) {
|
| 202 |
# Return NA when the denom = 0. |
|
| 203 |
# The formatting function converts NA values to "-" for display. |
|
| 204 | 2137x |
c(x, "p" = ifelse(denom > 0, x / denom, NA)) |
| 205 |
} |
|
| 206 |
) |
|
| 207 | ||
| 208 | 569x |
y$count_unique_denom_fraction <- lapply( |
| 209 | 569x |
y$count_unique, |
| 210 | 569x |
function(x) {
|
| 211 |
# Return NA when the denom = 0. |
|
| 212 |
# The formatting function converts NA values to "-" for display. |
|
| 213 | 2137x |
c(x, "d" = denom, "p" = ifelse(denom > 0, x / denom, NA)) |
| 214 |
} |
|
| 215 |
) |
|
| 216 | ||
| 217 | 569x |
y$unique_fraction <- lapply( |
| 218 | 569x |
y$count_unique, |
| 219 | 569x |
function(x) {
|
| 220 |
# Return NA when the denom = 0. |
|
| 221 |
# The formatting function converts NA values to "-" for display. |
|
| 222 | 2137x |
c("p" = ifelse(denom > 0, x / denom, NA_real_))
|
| 223 |
} |
|
| 224 |
) |
|
| 225 |
} |
|
| 226 | ||
| 227 | 571x |
return(y) |
| 228 |
} |
|
| 229 | ||
| 230 |
s_risk_diff_levii_j <- function( |
|
| 231 |
levii, |
|
| 232 |
df, |
|
| 233 |
.var, |
|
| 234 |
ref_df, |
|
| 235 |
ref_denom_df, |
|
| 236 |
.in_ref_col, |
|
| 237 |
curgrp_denom_df, |
|
| 238 |
id, |
|
| 239 |
variables, |
|
| 240 |
conf_level, |
|
| 241 |
method, |
|
| 242 |
weights_method |
|
| 243 |
) {
|
|
| 244 | 393x |
dfii <- df[df[[.var]] == levii & !is.na(df[[.var]]), ] |
| 245 | 393x |
ref_dfii <- ref_df[ref_df[[.var]] == levii & !is.na(ref_df[[.var]]), ] |
| 246 | ||
| 247 |
# construction of df_val, based upon curgrp_denom_df, dfii |
|
| 248 | 393x |
df_val <- curgrp_denom_df |
| 249 | 393x |
df_val$rsp <- FALSE |
| 250 |
# subjects with value levii observed in df TRUE |
|
| 251 | 393x |
df_val$rsp[df_val[[id]] %in% unique(dfii[[id]])] <- TRUE |
| 252 | ||
| 253 |
# repeat for ref group, based upon ref_denom_df, ref_dfii |
|
| 254 | 393x |
ref_df_val <- ref_denom_df |
| 255 | 393x |
ref_df_val$rsp <- FALSE |
| 256 |
# subjects with value levii observed in ref_df TRUE |
|
| 257 | 393x |
ref_df_val$rsp[ref_df_val[[id]] %in% unique(ref_dfii[[id]])] <- TRUE |
| 258 | ||
| 259 | 393x |
res_ci_3d <- tern::s_proportion_diff( |
| 260 | 393x |
df = df_val, |
| 261 | 393x |
.var = "rsp", |
| 262 | 393x |
.ref_group = ref_df_val, |
| 263 | 393x |
.in_ref_col = .in_ref_col, |
| 264 | 393x |
variables = variables, |
| 265 | 393x |
conf_level = conf_level, |
| 266 | 393x |
method = method, |
| 267 | 393x |
weights_method = weights_method |
| 268 | 393x |
)$diff_est_ci |
| 269 |
} |
|
| 270 | ||
| 271 | ||
| 272 |
s_risk_diff_val_j <- function( |
|
| 273 |
df, |
|
| 274 |
.var, |
|
| 275 |
.df_row, |
|
| 276 |
ctrl_grp, |
|
| 277 |
cur_trt_grp, |
|
| 278 |
trt_var, |
|
| 279 |
val = NULL, |
|
| 280 |
drop_levels = FALSE, |
|
| 281 |
excl_levels = NULL, |
|
| 282 |
denom_df, |
|
| 283 |
id = "USUBJID", |
|
| 284 |
riskdiff = TRUE, |
|
| 285 |
variables = list(strata = NULL), |
|
| 286 |
conf_level = 0.95, |
|
| 287 |
method = c( |
|
| 288 |
"waldcc", |
|
| 289 |
"wald", |
|
| 290 |
"cmh", |
|
| 291 |
"ha", |
|
| 292 |
"newcombe", |
|
| 293 |
"newcombecc", |
|
| 294 |
"strat_newcombe", |
|
| 295 |
"strat_newcombecc", |
|
| 296 |
"cmh_sato", |
|
| 297 |
"cmh_mn", |
|
| 298 |
"uncond_exact_diff" |
|
| 299 |
), |
|
| 300 |
weights_method = "cmh" |
|
| 301 |
) {
|
|
| 302 | 77x |
if (drop_levels) {
|
| 303 | ! |
obs_levs <- unique(.df_row[[.var]]) |
| 304 | ! |
obs_levs <- intersect(levels(.df_row[[.var]]), obs_levs) |
| 305 | ||
| 306 | ! |
if (!is.null(excl_levels)) obs_levs <- setdiff(obs_levs, excl_levels) |
| 307 | ||
| 308 | ! |
if (!is.null(val)) {
|
| 309 | ! |
stop("argument val cannot be used together with drop_levels = TRUE, please specify one or the other.")
|
| 310 |
} |
|
| 311 | ! |
val <- obs_levs |
| 312 |
} |
|
| 313 | ||
| 314 | 77x |
if (!is.null(val)) {
|
| 315 |
# restrict the levels to the ones specified in val argument |
|
| 316 | 14x |
df <- df[df[[.var]] %in% val, ] |
| 317 | 14x |
.df_row <- .df_row[.df_row[[.var]] %in% val, ] |
| 318 | ||
| 319 | 14x |
df <- h_update_factor(df, .var, val) |
| 320 | 14x |
.df_row <- h_update_factor(.df_row, .var, val) |
| 321 |
} |
|
| 322 | ||
| 323 | 77x |
if (!is.null(excl_levels) && drop_levels == FALSE) {
|
| 324 |
# restrict the levels to the ones specified in val argument |
|
| 325 | ! |
df <- df[!(df[[.var]] %in% excl_levels), ] |
| 326 | ! |
.df_row <- .df_row[!(.df_row[[.var]] %in% excl_levels), ] |
| 327 | ||
| 328 | ! |
df <- h_update_factor(df, .var, excl_levels = excl_levels) |
| 329 | ! |
.df_row <- h_update_factor(.df_row, .var, excl_levels = excl_levels) |
| 330 |
} |
|
| 331 | ||
| 332 | 77x |
levs <- levels(df[[.var]]) |
| 333 | ||
| 334 | 77x |
if (identical(levs, no_data_to_report_str)) {
|
| 335 | ! |
riskdiff <- FALSE |
| 336 |
} |
|
| 337 | 77x |
if (!riskdiff) {
|
| 338 | 8x |
return(list(rr_ci_3d = setNames(replicate(length(levs), list(NULL)), levs))) |
| 339 |
} |
|
| 340 |
### check on denom_df |
|
| 341 | 69x |
if (NROW(denom_df[[id]]) > length(unique(denom_df[[id]]))) {
|
| 342 | ! |
stop( |
| 343 | ! |
"\nProblem: a_freq_j \n |
| 344 | ! |
Denominator has multiple records per id. \n |
| 345 | ! |
Please specify colgroup and/or denom_by to refine your denominator for proper risk difference derivation." |
| 346 |
) |
|
| 347 |
} |
|
| 348 | ||
| 349 |
### are we in reference column? |
|
| 350 | 69x |
.in_ref_col <- (cur_trt_grp == ctrl_grp) |
| 351 | ||
| 352 |
### data from reference group - df based |
|
| 353 | 69x |
ref_df <- get_ctrl_subset(.df_row, trt_var = trt_var, ctrl_grp = ctrl_grp) |
| 354 | ||
| 355 |
### denominator data from reference group - denom_df based |
|
| 356 | 69x |
ref_denom_df <- get_ctrl_subset( |
| 357 | 69x |
denom_df, |
| 358 | 69x |
trt_var = trt_var, |
| 359 | 69x |
ctrl_grp = ctrl_grp |
| 360 |
) |
|
| 361 | ||
| 362 |
# ensure this is unique record per subject |
|
| 363 | 69x |
ref_denom_df <- unique(ref_denom_df[, c(id, variables$strata), drop = FALSE]) |
| 364 | ||
| 365 |
### denominator data from current group - denom_df based --- |
|
| 366 | 69x |
curgrp_denom_df <- get_ctrl_subset( |
| 367 | 69x |
denom_df, |
| 368 | 69x |
trt_var = trt_var, |
| 369 | 69x |
ctrl_grp = cur_trt_grp |
| 370 |
) |
|
| 371 | ||
| 372 |
# ensure this is unique record per subject |
|
| 373 | 69x |
curgrp_denom_df <- unique(curgrp_denom_df[, c(id, variables$strata), drop = FALSE]) |
| 374 | ||
| 375 |
# calculate the stats for each of the levels in levs |
|
| 376 | 69x |
rr_ci_3d <- sapply( |
| 377 | 69x |
levs, |
| 378 | 69x |
s_risk_diff_levii_j, |
| 379 | 69x |
df = df, |
| 380 | 69x |
.var = .var, |
| 381 | 69x |
ref_df = ref_df, |
| 382 | 69x |
ref_denom_df = ref_denom_df, |
| 383 | 69x |
.in_ref_col = .in_ref_col, |
| 384 | 69x |
curgrp_denom_df = curgrp_denom_df, |
| 385 | 69x |
id = id, |
| 386 | 69x |
variables = variables, |
| 387 | 69x |
conf_level = conf_level, |
| 388 | 69x |
method = method, |
| 389 | 69x |
weights_method = weights_method, |
| 390 | 69x |
USE.NAMES = TRUE, |
| 391 | 69x |
simplify = FALSE |
| 392 |
) |
|
| 393 | 69x |
list(rr_ci_3d = rr_ci_3d) |
| 394 |
} |
|
| 395 | ||
| 396 | ||
| 397 |
#' @name a_freq_j |
|
| 398 |
#' |
|
| 399 |
#' |
|
| 400 |
#' @inheritParams proposal_argument_convention |
|
| 401 |
#' @param .stats (`character`)\cr Statistics to include in the table. May contain one or more of: |
|
| 402 |
#' `"count"`, `"count_unique"`, `"count_unique_fraction"`, |
|
| 403 |
#' `"count_unique_denom_fraction"`, `"unique_fraction"`, |
|
| 404 |
#' `"n_df"`, `"n_altdf"`, `"n_rowdf"`, `"n_parentdf"`, `"denom"`. |
|
| 405 |
#' See Value for the full list of available statistics. |
|
| 406 |
#' @param riskdiff (`logical`)\cr |
|
| 407 |
#' When `TRUE`, risk difference calculations will be performed and |
|
| 408 |
#' presented (if required risk difference column splits are included).\cr |
|
| 409 |
#' When `FALSE`, risk difference columns will remain blank |
|
| 410 |
#' (if required risk difference column splits are included). |
|
| 411 |
#' @param ref_path (`string`)\cr Column path specifications for |
|
| 412 |
#' the control group for the relative risk derivation. |
|
| 413 |
#' @param variables Will be passed onto the risk difference function |
|
| 414 |
#' (internal function s_risk_diff_val_j), which is based upon [tern::s_proportion_diff()].\cr |
|
| 415 |
#' See `?tern::s_proportion_diff` for details. |
|
| 416 |
#' @param method Will be passed onto the risk difference function (internal function s_risk_diff_val_j).\cr |
|
| 417 |
#' @param weights_method Will be passed onto the risk difference function (internal function s_risk_diff_val_j).\cr |
|
| 418 |
#' @param label (`string`)\cr |
|
| 419 |
#' When `val` has length 1, |
|
| 420 |
#' the row label to be shown on the output can be specified using this argument.\cr |
|
| 421 |
#' When `val` is a `character vector`, the `label_map` argument can be specified |
|
| 422 |
#' to control the row-labels. |
|
| 423 |
#' @param labelstr An argument to ensure this function can be used |
|
| 424 |
#' as a `cfun` in a `summarize_row_groups` call.\cr |
|
| 425 |
#' It is recommended not to utilize this argument for other purposes.\cr |
|
| 426 |
#' The label argument could be used instead (if `val` is a single string)\cr |
|
| 427 |
#' An another approach could be to utilize the `label_map` argument |
|
| 428 |
#' to control the row labels of the incoming analysis variable. |
|
| 429 |
#' @param label_fstr (`string`)\cr |
|
| 430 |
#' a sprintf style format string. |
|
| 431 |
#' It can contain up to one `"%s"`, which takes the current split value and |
|
| 432 |
#' generates the row/column label.\cr |
|
| 433 |
#' It will be combined with the `labelstr` argument, |
|
| 434 |
#' when utilizing this function as |
|
| 435 |
#' a `cfun` in a `summarize_row_groups` call.\cr |
|
| 436 |
#' It is recommended not to utilize this argument for other purposes. |
|
| 437 |
#' The label argument could be used instead (if `val` is a single string)\cr |
|
| 438 |
#' @param label_map (`data.frame`)\cr |
|
| 439 |
#' A mapping data frame used to translate levels of the analysis variable(s) into |
|
| 440 |
#' the row labels displayed in the table, optionally conditioned on the nearest row-split.\cr |
|
| 441 |
#' |
|
| 442 |
#' Four types of mappings are supported: |
|
| 443 |
#' \enumerate{
|
|
| 444 |
#' \item Single-variable mapping. Column names: `level`, `label`. |
|
| 445 |
#' \item Multi-variable mapping. Column names: `var`, `level`, `label`. |
|
| 446 |
#' \item Conditional-on-row-split mapping. Column names: `<split_var>`, `level`, `label`. |
|
| 447 |
#' \item Conditional-on-row-split multi-variable mapping. Column names: `<split_var>`, `var`, `level`, `label`. |
|
| 448 |
#' } |
|
| 449 |
#' Here, `<split_var>` is a placeholder for the name of the variable used for the nearest row-split |
|
| 450 |
#' (e.g. `SEX`, `PARAMCD`).\cr |
|
| 451 |
#' |
|
| 452 |
#' **Recommendation**: ensure input variable(s) are of type factor, to avoid the error |
|
| 453 |
#' "got a label map that doesn't provide labels for all values".\cr |
|
| 454 |
#' |
|
| 455 |
#' See examples for more details. |
|
| 456 |
#' @param .alt_df_full (`dataframe`)\cr Denominator dataset |
|
| 457 |
#' for fraction and relative risk calculations.\cr |
|
| 458 |
#' this argument gets populated by the rtables |
|
| 459 |
#' split machinery (see [rtables::additional_fun_params]). |
|
| 460 |
#' @param denom_by (`character`)\cr Variables from row-split |
|
| 461 |
#' to be used in the denominator derivation.\cr |
|
| 462 |
#' This controls both `denom = "n_parentdf"` and `denom = "n_altdf"`.\cr |
|
| 463 |
#' When `denom = "n_altdf"`, the denominator is derived from `.alt_df_full` |
|
| 464 |
#' in combination with `denom_by` argument |
|
| 465 |
#' @param .labels_n (named `character`)\cr |
|
| 466 |
#' String to control row labels for the 'n'-statistics.\cr |
|
| 467 |
#' Only useful when more than one 'n'-statistic is requested (rare situations only). |
|
| 468 |
#' @param .formats (named 'character' or 'list')\cr |
|
| 469 |
#' formats for the statistics. |
|
| 470 |
#' @param extrablankline (`logical`)\cr |
|
| 471 |
#' When `TRUE`, an extra blank line will be added after the last value.\cr |
|
| 472 |
#' Avoid using this in template scripts, use section_div = " " instead (once PR for rtables is available)\cr |
|
| 473 |
#' @param extrablanklineafter (`string`)\cr |
|
| 474 |
#' When the row-label matches the string, an extra blank line will be added after |
|
| 475 |
#' that value. |
|
| 476 |
#' @param restr_columns `character`\cr |
|
| 477 |
#' If not NULL, columns not defined in `restr_columns` will be blanked out. |
|
| 478 |
#' @param colgroup The name of the column group variable that is used as source |
|
| 479 |
#' for denominator calculation.\cr |
|
| 480 |
#' Required to be specified when `denom = "N_colgroup"`. |
|
| 481 |
#' @param addstr2levs string, if not NULL will be appended to the rowlabel for that level, |
|
| 482 |
#' eg to add ",n (percent)" at the end of the rowlabels |
|
| 483 |
#' |
|
| 484 |
#' @examples |
|
| 485 |
#' library(dplyr) |
|
| 486 |
#' |
|
| 487 |
#' adsl <- ex_adsl |> select("USUBJID", "SEX", "ARM")
|
|
| 488 |
#' adae <- ex_adae |> select("USUBJID", "AEBODSYS", "AEDECOD")
|
|
| 489 |
#' adae[["TRTEMFL"]] <- "Y" |
|
| 490 |
#' |
|
| 491 |
#' trtvar <- "ARM" |
|
| 492 |
#' ctrl_grp <- "B: Placebo" |
|
| 493 |
#' adsl$colspan_trt <- factor(ifelse(adsl[[trtvar]] == ctrl_grp, " ", "Active Study Agent"), |
|
| 494 |
#' levels = c("Active Study Agent", " ")
|
|
| 495 |
#' ) |
|
| 496 |
#' |
|
| 497 |
#' adsl$rrisk_header <- "Risk Difference (%) (95% CI)" |
|
| 498 |
#' adsl$rrisk_label <- paste(adsl[[trtvar]], paste("vs", ctrl_grp))
|
|
| 499 |
#' |
|
| 500 |
#' adae <- adae |> left_join(adsl) |
|
| 501 |
#' |
|
| 502 |
#' colspan_trt_map <- create_colspan_map(adsl, |
|
| 503 |
#' non_active_grp = "B: Placebo", |
|
| 504 |
#' non_active_grp_span_lbl = " ", |
|
| 505 |
#' active_grp_span_lbl = "Active Study Agent", |
|
| 506 |
#' colspan_var = "colspan_trt", |
|
| 507 |
#' trt_var = trtvar |
|
| 508 |
#' ) |
|
| 509 |
#' |
|
| 510 |
#' ref_path <- c("colspan_trt", " ", trtvar, ctrl_grp)
|
|
| 511 |
#' |
|
| 512 |
#' lyt <- basic_table(show_colcounts = TRUE) |> |
|
| 513 |
#' split_cols_by("colspan_trt", split_fun = trim_levels_to_map(map = colspan_trt_map)) |>
|
|
| 514 |
#' split_cols_by(trtvar) |> |
|
| 515 |
#' split_cols_by("rrisk_header", nested = FALSE) |>
|
|
| 516 |
#' split_cols_by(trtvar, labels_var = "rrisk_label", split_fun = remove_split_levels(ctrl_grp)) |
|
| 517 |
#' |
|
| 518 |
#' lyt1 <- lyt |> |
|
| 519 |
#' analyze("TRTEMFL",
|
|
| 520 |
#' show_labels = "hidden", |
|
| 521 |
#' afun = a_freq_j, |
|
| 522 |
#' extra_args = list( |
|
| 523 |
#' method = "wald", |
|
| 524 |
#' .stats = c("count_unique_denom_fraction"),
|
|
| 525 |
#' ref_path = ref_path |
|
| 526 |
#' ) |
|
| 527 |
#' ) |
|
| 528 |
#' |
|
| 529 |
#' result1 <- build_table(lyt1, adae, alt_counts_df = adsl) |
|
| 530 |
#' |
|
| 531 |
#' result1 |
|
| 532 |
#' |
|
| 533 |
#' x_drug_x <- list(length(unique(subset(adae, adae[[trtvar]] == "A: Drug X")[["USUBJID"]]))) |
|
| 534 |
#' N_x_drug_x <- length(unique(subset(adsl, adsl[[trtvar]] == "A: Drug X")[["USUBJID"]])) |
|
| 535 |
#' y_placebo <- list(length(unique(subset(adae, adae[[trtvar]] == ctrl_grp)[["USUBJID"]]))) |
|
| 536 |
#' N_y_placebo <- length(unique(subset(adsl, adsl[[trtvar]] == ctrl_grp)[["USUBJID"]])) |
|
| 537 |
#' |
|
| 538 |
#' tern::stat_propdiff_ci( |
|
| 539 |
#' x = x_drug_x, |
|
| 540 |
#' N_x = N_x_drug_x, |
|
| 541 |
#' y = y_placebo, |
|
| 542 |
#' N_y = N_y_placebo |
|
| 543 |
#' ) |
|
| 544 |
#' |
|
| 545 |
#' x_combo <- list(length(unique(subset(adae, adae[[trtvar]] == "C: Combination")[["USUBJID"]]))) |
|
| 546 |
#' N_x_combo <- length(unique(subset(adsl, adsl[[trtvar]] == "C: Combination")[["USUBJID"]])) |
|
| 547 |
#' |
|
| 548 |
#' tern::stat_propdiff_ci( |
|
| 549 |
#' x = x_combo, |
|
| 550 |
#' N_x = N_x_combo, |
|
| 551 |
#' y = y_placebo, |
|
| 552 |
#' N_y = N_y_placebo |
|
| 553 |
#' ) |
|
| 554 |
#' |
|
| 555 |
#' |
|
| 556 |
#' extra_args_rr <- list( |
|
| 557 |
#' denom = "n_altdf", |
|
| 558 |
#' denom_by = "SEX", |
|
| 559 |
#' riskdiff = FALSE, |
|
| 560 |
#' .stats = c("count_unique")
|
|
| 561 |
#' ) |
|
| 562 |
#' |
|
| 563 |
#' extra_args_rr2 <- list( |
|
| 564 |
#' denom = "n_altdf", |
|
| 565 |
#' denom_by = "SEX", |
|
| 566 |
#' riskdiff = TRUE, |
|
| 567 |
#' ref_path = ref_path, |
|
| 568 |
#' method = "wald", |
|
| 569 |
#' .stats = c("count_unique_denom_fraction"),
|
|
| 570 |
#' na_str = rep("NA", 3)
|
|
| 571 |
#' ) |
|
| 572 |
#' |
|
| 573 |
#' lyt2 <- basic_table( |
|
| 574 |
#' top_level_section_div = " ", |
|
| 575 |
#' colcount_format = "N=xx" |
|
| 576 |
#' ) |> |
|
| 577 |
#' split_cols_by("colspan_trt", split_fun = trim_levels_to_map(map = colspan_trt_map)) |>
|
|
| 578 |
#' split_cols_by(trtvar, show_colcounts = TRUE) |> |
|
| 579 |
#' split_cols_by("rrisk_header", nested = FALSE) |>
|
|
| 580 |
#' split_cols_by(trtvar, |
|
| 581 |
#' labels_var = "rrisk_label", split_fun = remove_split_levels("B: Placebo"),
|
|
| 582 |
#' show_colcounts = FALSE |
|
| 583 |
#' ) |> |
|
| 584 |
#' split_rows_by("SEX", split_fun = drop_split_levels) |>
|
|
| 585 |
#' summarize_row_groups("SEX",
|
|
| 586 |
#' cfun = a_freq_j, |
|
| 587 |
#' extra_args = append(extra_args_rr, list(label_fstr = "Gender: %s")) |
|
| 588 |
#' ) |> |
|
| 589 |
#' split_rows_by("TRTEMFL",
|
|
| 590 |
#' split_fun = keep_split_levels("Y"),
|
|
| 591 |
#' indent_mod = -1L, |
|
| 592 |
#' section_div = c(" ")
|
|
| 593 |
#' ) |> |
|
| 594 |
#' summarize_row_groups("TRTEMFL",
|
|
| 595 |
#' cfun = a_freq_j, |
|
| 596 |
#' extra_args = append(extra_args_rr2, list( |
|
| 597 |
#' label = |
|
| 598 |
#' "Subjects with >=1 AE" |
|
| 599 |
#' )) |
|
| 600 |
#' ) |> |
|
| 601 |
#' split_rows_by("AEBODSYS",
|
|
| 602 |
#' split_label = "System Organ Class", |
|
| 603 |
#' split_fun = trim_levels_in_group("AEDECOD"),
|
|
| 604 |
#' label_pos = "topleft", |
|
| 605 |
#' section_div = c(" "),
|
|
| 606 |
#' nested = TRUE |
|
| 607 |
#' ) |> |
|
| 608 |
#' summarize_row_groups("AEBODSYS",
|
|
| 609 |
#' cfun = a_freq_j, |
|
| 610 |
#' extra_args = extra_args_rr2 |
|
| 611 |
#' ) |> |
|
| 612 |
#' analyze("AEDECOD",
|
|
| 613 |
#' afun = a_freq_j, |
|
| 614 |
#' extra_args = extra_args_rr2 |
|
| 615 |
#' ) |
|
| 616 |
#' |
|
| 617 |
#' result2 <- build_table(lyt2, adae, alt_counts_df = adsl) |
|
| 618 |
#' |
|
| 619 |
#' # ------------------------------------------------------------------------- |
|
| 620 |
#' # Examples using the label_map argument |
|
| 621 |
#' # ------------------------------------------------------------------------- |
|
| 622 |
#' |
|
| 623 |
#' adsl <- ex_adsl |> select("USUBJID", "SEX", "ARM")
|
|
| 624 |
#' adae <- ex_adae |> select("USUBJID", "AEBODSYS", "AEDECOD")
|
|
| 625 |
#' adae[["TRTEMFL"]] <- "Y" |
|
| 626 |
#' |
|
| 627 |
#' trtvar <- "ARM" |
|
| 628 |
#' adae <- adae |> left_join(adsl) |
|
| 629 |
#' |
|
| 630 |
#' # ------------------------------------------------------------------------- |
|
| 631 |
#' # 1. Simple mapping. |
|
| 632 |
#' # Remaps factor levels (value) to custom new labels (label). |
|
| 633 |
#' # ------------------------------------------------------------------------- |
|
| 634 |
#' |
|
| 635 |
#' label_map_simple <- data.frame( |
|
| 636 |
#' value = c("M", "F", "UNDIFFERENTIATED", "U"),
|
|
| 637 |
#' label = c("Male", "Female", "Undifferentiated", "Unknown")
|
|
| 638 |
#' ) |
|
| 639 |
#' |
|
| 640 |
#' lyt_lmap1 <- basic_table(show_colcounts = TRUE) |> |
|
| 641 |
#' split_cols_by(trtvar) |> |
|
| 642 |
#' analyze("SEX",
|
|
| 643 |
#' afun = a_freq_j, |
|
| 644 |
#' extra_args = list( |
|
| 645 |
#' .stats = "count_unique_fraction", |
|
| 646 |
#' label_map = label_map_simple |
|
| 647 |
#' ) |
|
| 648 |
#' ) |
|
| 649 |
#' |
|
| 650 |
#' result_lmap1 <- build_table(lyt_lmap1, adsl, alt_counts_df = adsl) |
|
| 651 |
#' result_lmap1 |
|
| 652 |
#' |
|
| 653 |
#' # ------------------------------------------------------------------------- |
|
| 654 |
#' # 2. Multi-variable mapping. |
|
| 655 |
#' # Useful in situations where multiple Y/N variables can be analyzed in one |
|
| 656 |
#' # [rtables::analyze()] call. |
|
| 657 |
#' # ------------------------------------------------------------------------- |
|
| 658 |
#' |
|
| 659 |
#' multi_vars <- c("DTH30FL", "DTHA30FL", "DTHB30FL")
|
|
| 660 |
#' adslx <- pharmaverseadamjnj::adsl |
|
| 661 |
#' # ensure the variables are factor with levels Y/N - and variable label is kept |
|
| 662 |
#' adslx <- adslx |> |
|
| 663 |
#' mutate(across(all_of(multi_vars), \(x) structure( |
|
| 664 |
#' factor(x, levels = c("Y", "N")),
|
|
| 665 |
#' label = attr(x, "label") |
|
| 666 |
#' ))) |
|
| 667 |
#' |
|
| 668 |
#' map_multi <- data.frame( |
|
| 669 |
#' var = multi_vars, |
|
| 670 |
#' value = "Y", |
|
| 671 |
#' label = c( |
|
| 672 |
#' "Death <=30D of Last Trt Flag", |
|
| 673 |
#' "Death >30D from Last Trt Flag", |
|
| 674 |
#' "Death <=30D of First Trt Flag" |
|
| 675 |
#' ) |
|
| 676 |
#' ) |
|
| 677 |
#' |
|
| 678 |
#' lyt_lmap1 <- basic_table(show_colcounts = TRUE) |> |
|
| 679 |
#' split_cols_by("ARM") |>
|
|
| 680 |
#' analyze(multi_vars, |
|
| 681 |
#' afun = a_freq_j, |
|
| 682 |
#' extra_args = list( |
|
| 683 |
#' .stats = "count_unique_denom_fraction", |
|
| 684 |
#' label_map = map_multi, |
|
| 685 |
#' val = "Y" |
|
| 686 |
#' ), |
|
| 687 |
#' show_labels = "hidden" |
|
| 688 |
#' ) |
|
| 689 |
#' |
|
| 690 |
#' result_lmap2 <- build_table(lyt_lmap1, adslx) |
|
| 691 |
#' result_lmap2 |
|
| 692 |
#' |
|
| 693 |
#' # ------------------------------------------------------------------------- |
|
| 694 |
#' # 3. Conditional-on-row-split mapping. |
|
| 695 |
#' # ------------------------------------------------------------------------- |
|
| 696 |
#' |
|
| 697 |
#' map_rowsplit <- data.frame( |
|
| 698 |
#' SEX = c("M", "F"),
|
|
| 699 |
#' value = c("Y", "Y"),
|
|
| 700 |
#' label = c("Male subjects with >=1 TE AE", "Female subjects with >=1 TE AE")
|
|
| 701 |
#' ) |
|
| 702 |
#' |
|
| 703 |
#' adsl <- ex_adsl |> select("USUBJID", "ARM", "SEX")
|
|
| 704 |
#' adae <- ex_adae |> select("USUBJID", "AEBODSYS", "AEDECOD")
|
|
| 705 |
#' adae[["TRTEMFL"]] <- "Y" |
|
| 706 |
#' adae <- adae |> left_join(adsl) |
|
| 707 |
#' |
|
| 708 |
#' lyt_lmap3 <- basic_table(show_colcounts = TRUE) |> |
|
| 709 |
#' split_cols_by("ARM") |>
|
|
| 710 |
#' split_rows_by("SEX", split_fun = keep_split_levels(c("F", "M")), child_labels = "hidden") |>
|
|
| 711 |
#' analyze("TRTEMFL",
|
|
| 712 |
#' afun = a_freq_j, |
|
| 713 |
#' extra_args = list( |
|
| 714 |
#' .stats = "count_unique_denom_fraction", |
|
| 715 |
#' label_map = map_rowsplit, |
|
| 716 |
#' val = "Y" |
|
| 717 |
#' ), |
|
| 718 |
#' show_labels = "hidden" |
|
| 719 |
#' ) |
|
| 720 |
#' |
|
| 721 |
#' result_lmap3 <- build_table(lyt_lmap3, adae, alt_counts_df = adsl) |
|
| 722 |
#' result_lmap3 |
|
| 723 |
#' |
|
| 724 |
#' # ----------------------------------------------------------------------------------- |
|
| 725 |
#' # 4. Conditional-on-row-split multi-variable mapping. |
|
| 726 |
#' # ----------------------------------------------------------------------------------- |
|
| 727 |
#' adsl_jnj <- pharmaverseadamjnj::adsl |
|
| 728 |
#' advs_jnj <- pharmaverseadamjnj::advs |
|
| 729 |
#' |
|
| 730 |
#' multi_vars <- c("CRIT1FL", "CRIT2FL", "CRIT3FL")
|
|
| 731 |
#' |
|
| 732 |
#' map_multi_rowsplit <- data.frame( |
|
| 733 |
#' PARAMCD = c(rep("DIABP", 3), rep("SYSBP", 3)),
|
|
| 734 |
#' var = rep(multi_vars, 2), |
|
| 735 |
#' value = rep("Y", 6),
|
|
| 736 |
#' label = c( |
|
| 737 |
#' "<50 mmHg and with >20 mmHg decrease from baseline", |
|
| 738 |
#' ">105 mmHg and with >30 mmHg increase from baseline", |
|
| 739 |
#' "Diastolic blood pressure<60", |
|
| 740 |
#' "<90 mmHg and with >30 mmHg decrease from baseline", |
|
| 741 |
#' ">180 mmHg and with >40 mmHg increase from baseline", |
|
| 742 |
#' "Systolic blood pressure<90" |
|
| 743 |
#' ) |
|
| 744 |
#' ) |
|
| 745 |
#' |
|
| 746 |
#' lyt_lmap4 <- basic_table(show_colcounts = TRUE) |> |
|
| 747 |
#' split_cols_by("TRT01A") |>
|
|
| 748 |
#' split_rows_by("PARAMCD", split_fun = keep_split_levels(c("DIABP", "SYSBP"))) |>
|
|
| 749 |
#' analyze(multi_vars, |
|
| 750 |
#' afun = a_freq_j, |
|
| 751 |
#' extra_args = list( |
|
| 752 |
#' .stats = "count_unique_denom_fraction", |
|
| 753 |
#' label_map = map_multi_rowsplit, |
|
| 754 |
#' val = "Y" |
|
| 755 |
#' ), |
|
| 756 |
#' show_labels = "hidden" |
|
| 757 |
#' ) |
|
| 758 |
#' |
|
| 759 |
#' result_lmap4 <- build_table(lyt_lmap4, advs_jnj, alt_counts_df = adsl_jnj) |
|
| 760 |
#' result_lmap4 |
|
| 761 |
#' |
|
| 762 |
#' @return |
|
| 763 |
#' * `a_freq_j`: returns a list of requested statistics with formatted `rtables::CellValue()`.\cr |
|
| 764 |
#' Within the relative risk difference columns, the following stats are blanked out: |
|
| 765 |
#' \itemize{
|
|
| 766 |
#' \item any of the n-statistics (n_df, n_altdf, n_parentdf, n_rowdf, denom) |
|
| 767 |
#' \item count |
|
| 768 |
#' \item count_unique |
|
| 769 |
#' } |
|
| 770 |
#' For the others (count_unique_fraction, count_unique_denom_fraction, unique_fraction), |
|
| 771 |
#' the statistic is replaced by the relative risk difference + confidence interval. |
|
| 772 |
#' @export |
|
| 773 |
a_freq_j <- function( |
|
| 774 |
df, |
|
| 775 |
labelstr = NULL, |
|
| 776 |
.var = NA, |
|
| 777 |
val = NULL, |
|
| 778 |
drop_levels = FALSE, |
|
| 779 |
excl_levels = NULL, |
|
| 780 |
new_levels = NULL, |
|
| 781 |
new_levels_after = FALSE, |
|
| 782 |
addstr2levs = NULL, |
|
| 783 |
.df_row, |
|
| 784 |
.spl_context, |
|
| 785 |
.N_col, |
|
| 786 |
id = "USUBJID", |
|
| 787 |
denom = c("N_col", "n_df", "n_altdf", "N_colgroup", "n_rowdf", "n_parentdf"),
|
|
| 788 |
riskdiff = TRUE, |
|
| 789 |
ref_path = NULL, |
|
| 790 |
variables = list(strata = NULL), |
|
| 791 |
conf_level = 0.95, |
|
| 792 |
method = c( |
|
| 793 |
"wald", |
|
| 794 |
"waldcc", |
|
| 795 |
"cmh", |
|
| 796 |
"ha", |
|
| 797 |
"newcombe", |
|
| 798 |
"newcombecc", |
|
| 799 |
"strat_newcombe", |
|
| 800 |
"strat_newcombecc", |
|
| 801 |
"cmh_sato", |
|
| 802 |
"cmh_mn", |
|
| 803 |
"uncond_exact_diff" |
|
| 804 |
), |
|
| 805 |
weights_method = "cmh", |
|
| 806 |
label = NULL, |
|
| 807 |
label_fstr = NULL, |
|
| 808 |
label_map = NULL, |
|
| 809 |
.alt_df_full = NULL, |
|
| 810 |
denom_by = NULL, |
|
| 811 |
.stats = "count_unique_denom_fraction", |
|
| 812 |
.formats = NULL, |
|
| 813 |
.indent_mods = NULL, |
|
| 814 |
na_str = rep("NA", 3),
|
|
| 815 |
.labels_n = NULL, |
|
| 816 |
extrablankline = FALSE, |
|
| 817 |
extrablanklineafter = NULL, |
|
| 818 |
restr_columns = NULL, |
|
| 819 |
colgroup = NULL, |
|
| 820 |
countsource = c("df", "altdf", "altdf_subset")
|
|
| 821 |
) {
|
|
| 822 | 550x |
checkmate::assert_string(id) |
| 823 | 550x |
checkmate::assert_subset(id, colnames(df), empty.ok = FALSE) |
| 824 | 549x |
checkmate::check_character(ref_path, min.len = 2L) |
| 825 | 549x |
checkmate::assert_true(length(ref_path) %% 2L == 0L) |
| 826 | ||
| 827 | 549x |
if (riskdiff && !is.null(ref_path) && is.null(.alt_df_full)) {
|
| 828 | ! |
stop("In order to get correct numbers in relative risk column, please specify alt_counts_df in build_table.")
|
| 829 |
} |
|
| 830 | ||
| 831 | 549x |
denom <- match.arg(denom) |
| 832 | 549x |
method <- match.arg(method) |
| 833 | ||
| 834 | 549x |
if (!is.null(labelstr) && is.na(.var)) {
|
| 835 | ! |
stop( |
| 836 | ! |
"Please specify var call to summarize_row_groups when using cfun = a_freq_j, i.e.,\n", |
| 837 | ! |
"summarize_row_groups('varname', cfun = a_freq_j)"
|
| 838 |
) |
|
| 839 |
} |
|
| 840 | ||
| 841 | 549x |
if (denom == "N_colgroup") {
|
| 842 | 84x |
if (is.null(colgroup)) {
|
| 843 | ! |
stop("Colgroup must be specified when denom = N_colgroup.")
|
| 844 |
} |
|
| 845 | ||
| 846 | 84x |
checkmate::assert_character(colgroup, null.ok = FALSE, max.len = 1) |
| 847 | ||
| 848 | 84x |
if (colgroup == tail(.spl_context$cur_col_split[[1]], 1)) {
|
| 849 | ! |
stop( |
| 850 | ! |
"N_colgroup cannot be used when colgroup is lowest column split." |
| 851 |
) |
|
| 852 |
} |
|
| 853 |
} |
|
| 854 | ||
| 855 | 549x |
check_alt_df_full(denom, c("n_altdf", "N_colgroup"), .alt_df_full)
|
| 856 | ||
| 857 | 549x |
res_dataprep <- h_a_freq_dataprep( |
| 858 | 549x |
df = df, |
| 859 | 549x |
labelstr = labelstr, |
| 860 | 549x |
.var = .var, |
| 861 | 549x |
val = val, |
| 862 | 549x |
drop_levels = drop_levels, |
| 863 | 549x |
excl_levels = excl_levels, |
| 864 | 549x |
new_levels = new_levels, |
| 865 | 549x |
new_levels_after = new_levels_after, |
| 866 | 549x |
addstr2levs = addstr2levs, |
| 867 | 549x |
.df_row = .df_row, |
| 868 | 549x |
.spl_context = .spl_context, |
| 869 | 549x |
.N_col = .N_col, |
| 870 | 549x |
id = id, |
| 871 | 549x |
denom = denom, |
| 872 | 549x |
variables = variables, |
| 873 | 549x |
label = label, |
| 874 | 549x |
label_fstr = label_fstr, |
| 875 | 549x |
label_map = label_map, |
| 876 | 549x |
.alt_df_full = .alt_df_full, |
| 877 | 549x |
denom_by = denom_by, |
| 878 | 549x |
.stats = .stats, |
| 879 | 549x |
countsource = countsource |
| 880 |
) |
|
| 881 |
# res_dataprep is list with elements |
|
| 882 |
# df .df_row val |
|
| 883 |
# drop_levels excl_levels |
|
| 884 |
# alt_df parentdf new_denomdf |
|
| 885 |
# .stats |
|
| 886 |
# make these elements available in current environment |
|
| 887 | 548x |
df <- res_dataprep$df |
| 888 | 548x |
.df_row <- res_dataprep$.df_row |
| 889 | 548x |
val <- res_dataprep$val |
| 890 | 548x |
drop_levels <- res_dataprep$drop_levels |
| 891 | 548x |
excl_levels <- res_dataprep$excl_levels |
| 892 | 548x |
alt_df <- res_dataprep$alt_df |
| 893 | 548x |
parentdf <- res_dataprep$parentdf |
| 894 | 548x |
new_denomdf <- res_dataprep$new_denomdf |
| 895 | 548x |
.stats <- res_dataprep$.stats |
| 896 | ||
| 897 |
## prepare for column based split |
|
| 898 | 548x |
col_expr <- .spl_context$cur_col_expr[[1]] |
| 899 |
## colid can be used to figure out if we're in the relative risk columns or not |
|
| 900 | 548x |
colid <- .spl_context$cur_col_id[[1]] |
| 901 | 548x |
inriskdiffcol <- grepl("difference", tolower(colid), fixed = TRUE)
|
| 902 | ||
| 903 | 548x |
if (!is.null(colgroup)) {
|
| 904 | 84x |
colexpr_substr <- h_colexpr_substr(colgroup, .spl_context$cur_col_expr[[1]]) |
| 905 | ||
| 906 | 84x |
if (is.null(colexpr_substr)) {
|
| 907 | ! |
stop("\n Problem a_freq_j: incorrect colgroup specification.")
|
| 908 |
} |
|
| 909 | ||
| 910 | 84x |
new_denomdf <- subset(.alt_df_full, eval(parse(text = colexpr_substr))) |
| 911 | 84x |
.df_row <- subset(.df_row, eval(parse(text = colexpr_substr))) |
| 912 |
} |
|
| 913 | ||
| 914 | 548x |
if (!inriskdiffcol) {
|
| 915 | 483x |
if (denom != "N_colgroup" && !is.null(new_denomdf)) {
|
| 916 |
### for this part : perform column split on denominator dataset |
|
| 917 | 399x |
new_denomdf <- subset(new_denomdf, eval(col_expr)) |
| 918 |
} |
|
| 919 | 483x |
if (denom == "N_colgroup") {
|
| 920 | 84x |
denom <- "n_altdf" |
| 921 |
} |
|
| 922 | ||
| 923 | 483x |
x_stats <- s_freq_j( |
| 924 | 483x |
df, |
| 925 | 483x |
.var = .var, |
| 926 | 483x |
.df_row = .df_row, |
| 927 | 483x |
val = val, |
| 928 | 483x |
drop_levels = drop_levels, |
| 929 | 483x |
excl_levels = excl_levels, |
| 930 | 483x |
alt_df = new_denomdf, |
| 931 | 483x |
parent_df = new_denomdf, |
| 932 | 483x |
id = id, |
| 933 | 483x |
denom = denom, |
| 934 | 483x |
.N_col = .N_col, |
| 935 | 483x |
countsource = countsource |
| 936 |
) |
|
| 937 |
## remove relrisk stat from .stats |
|
| 938 | 483x |
.stats_adj <- .stats[!(.stats %in% "rr_ci_3d")] |
| 939 |
} else {
|
|
| 940 | 65x |
if (riskdiff && is.null(ref_path)) {
|
| 941 | ! |
stop("argument ref_path cannot be NULL.")
|
| 942 |
} |
|
| 943 | 65x |
if (denom == "N_colgroup") {
|
| 944 | ! |
stop( |
| 945 | ! |
"denom N_colgroup cannot be used in a layout with risk diff columns." |
| 946 |
) |
|
| 947 |
} |
|
| 948 | 65x |
if (!riskdiff) {
|
| 949 | 8x |
trt_var <- NULL |
| 950 | 8x |
ctrl_grp <- NULL |
| 951 | 8x |
cur_trt_grp <- NULL |
| 952 |
} else {
|
|
| 953 | 57x |
trt_var <- ref_path[length(ref_path) - 1L] |
| 954 | 57x |
ctrl_grp <- ref_path[length(ref_path)] |
| 955 | 57x |
stopifnot(ctrl_grp %in% levels(df[[trt_var]])) |
| 956 | 57x |
cur_trt_grp <- h_get_cur_trt_grp(trt_var, .spl_context) |
| 957 | ||
| 958 | 57x |
if (!is.null(colgroup) && trt_var == colgroup) {
|
| 959 | ! |
stop( |
| 960 | ! |
"\n Problem: a_freq_j: colgroup and treatment variable from ref_path are the same. |
| 961 | ! |
This is not intented for usage with relative risk columns. |
| 962 | ! |
Either remove risk difference columns from layout, set riskdiff = FALSE, or update colgroup." |
| 963 |
) |
|
| 964 |
} |
|
| 965 | ||
| 966 | 57x |
new_denomdf <- upd_denom_df_combo( |
| 967 | 57x |
new_denomdf, |
| 968 | 57x |
trt_var, |
| 969 | 57x |
cur_trt_grp, |
| 970 | 57x |
.spl_context |
| 971 |
) |
|
| 972 |
} |
|
| 973 | ||
| 974 | 65x |
x_stats <- s_risk_diff_val_j( |
| 975 | 65x |
df, |
| 976 | 65x |
.var = .var, |
| 977 | 65x |
.df_row = .df_row, |
| 978 | 65x |
val = val, |
| 979 | 65x |
drop_levels = drop_levels, |
| 980 | 65x |
excl_levels = excl_levels, |
| 981 | 65x |
denom_df = new_denomdf, |
| 982 | 65x |
id = id, |
| 983 | 65x |
riskdiff = riskdiff, |
| 984 |
# treatment/ref group related arguments |
|
| 985 | 65x |
trt_var = trt_var, |
| 986 | 65x |
ctrl_grp = ctrl_grp, |
| 987 | 65x |
cur_trt_grp = cur_trt_grp, |
| 988 |
# relrisk specific arguments |
|
| 989 | 65x |
variables = variables, |
| 990 | 65x |
conf_level = conf_level, |
| 991 | 65x |
method = method, |
| 992 | 65x |
weights_method = weights_method |
| 993 |
) |
|
| 994 | ||
| 995 |
## this will ensure the following stats will be shown as empty column in relative risk column |
|
| 996 | 65x |
xy <- sapply( |
| 997 | 65x |
c( |
| 998 | 65x |
"count", |
| 999 | 65x |
"count_unique", |
| 1000 | 65x |
"n_df", |
| 1001 | 65x |
"n_altdf", |
| 1002 | 65x |
"n_rowdf", |
| 1003 | 65x |
"n_parentdf", |
| 1004 | 65x |
"denom" |
| 1005 |
), |
|
| 1006 | 65x |
function(x) {
|
| 1007 | 455x |
stats::setNames(list(x = NULL), x) |
| 1008 |
}, |
|
| 1009 | 65x |
USE.NAMES = TRUE, |
| 1010 | 65x |
simplify = FALSE |
| 1011 |
) |
|
| 1012 | 65x |
x_stats <- append(x_stats, xy) |
| 1013 | ||
| 1014 |
## restrict to relrisk stat from .stats |
|
| 1015 |
# when both count_unique_fraction and count_unique_denom_fraction are requested, the rr_ci_3d stat is in here twice |
|
| 1016 |
# this does not seem to introduce a problem, although might not be ideal |
|
| 1017 |
# see further |
|
| 1018 | 65x |
.stats_adj <- replace( |
| 1019 | 65x |
.stats, |
| 1020 | 65x |
.stats %in% |
| 1021 | 65x |
c( |
| 1022 | 65x |
"count_unique_fraction", |
| 1023 | 65x |
"count_unique_denom_fraction", |
| 1024 | 65x |
"fraction_count_unique_denom", |
| 1025 | 65x |
"unique_fraction" |
| 1026 |
), |
|
| 1027 | 65x |
"rr_ci_3d" |
| 1028 |
) |
|
| 1029 |
} |
|
| 1030 | ||
| 1031 | 548x |
res_prepinrows <- h_a_freq_prepinrows( |
| 1032 | 548x |
x_stats, |
| 1033 | 548x |
.stats_adj, |
| 1034 | 548x |
.formats, |
| 1035 | 548x |
labelstr, |
| 1036 | 548x |
label_fstr, |
| 1037 | 548x |
label, |
| 1038 | 548x |
.indent_mods, |
| 1039 | 548x |
.labels_n, |
| 1040 | 548x |
na_str |
| 1041 |
) |
|
| 1042 |
# res_prepinrows is list with elements |
|
| 1043 |
# x_stats .formats .labels .indent_mods .format_na_strs |
|
| 1044 |
# make these elements available in current environment |
|
| 1045 | 548x |
x_stats <- res_prepinrows$x_stats |
| 1046 | 548x |
.formats <- res_prepinrows$.formats |
| 1047 | 548x |
.labels <- res_prepinrows$.labels |
| 1048 | 548x |
.indent_mods <- res_prepinrows$.indent_mods |
| 1049 | 548x |
.format_na_strs <- res_prepinrows$.format_na_strs |
| 1050 | ||
| 1051 |
### blank out columns not in restr_columns |
|
| 1052 |
# get column label |
|
| 1053 | 548x |
colid_lbl <- utils::tail( |
| 1054 | 548x |
.spl_context$cur_col_split_val[[NROW(.spl_context)]], |
| 1055 | 548x |
1 |
| 1056 |
) |
|
| 1057 | 548x |
if (!is.null(restr_columns) && !(tolower(colid_lbl) %in% tolower(restr_columns))) {
|
| 1058 | 33x |
x_stats <- lapply(x_stats, FUN = function(x) {
|
| 1059 | 96x |
NULL |
| 1060 |
}) |
|
| 1061 |
} |
|
| 1062 | ||
| 1063 |
### final step: turn requested stats into rtables rows |
|
| 1064 | 548x |
inrows <- in_rows( |
| 1065 | 548x |
.list = x_stats, |
| 1066 | 548x |
.formats = .formats, |
| 1067 | 548x |
.labels = .labels, |
| 1068 | 548x |
.indent_mods = .indent_mods, |
| 1069 | 548x |
.format_na_strs = .format_na_strs |
| 1070 |
) |
|
| 1071 | ||
| 1072 |
### add extra blankline to the end of inrows --- as long as section_div is not working as expected |
|
| 1073 |
# nolint start |
|
| 1074 | 548x |
if (!is.null(inrows) && extrablankline || |
| 1075 | 548x |
(!is.null(extrablanklineafter) && length(.labels) == 1 && .labels == extrablanklineafter)) {
|
| 1076 | 5x |
inrows <- add_blank_line_rcells(inrows) |
| 1077 |
} # nolint end |
|
| 1078 | ||
| 1079 | 548x |
return(inrows) |
| 1080 |
} |
|
| 1081 | ||
| 1082 |
#' @describeIn a_freq_j Wrapper for the `afun` which can exclude |
|
| 1083 |
#' row split levels from producing the analysis. These have to be specified in the |
|
| 1084 |
#' `exclude_levels` argument, see `?do_exclude_split` for details. |
|
| 1085 |
#' @export |
|
| 1086 |
a_freq_j_with_exclude <- function( |
|
| 1087 |
df, |
|
| 1088 |
labelstr = NULL, |
|
| 1089 |
exclude_levels, |
|
| 1090 |
.var = NA, |
|
| 1091 |
.spl_context, |
|
| 1092 |
.df_row, |
|
| 1093 |
.N_col, |
|
| 1094 |
.alt_df_full = NULL, |
|
| 1095 |
.stats = "count_unique_denom_fraction", |
|
| 1096 |
.formats = NULL, |
|
| 1097 |
.indent_mods = NULL, |
|
| 1098 |
.labels_n = NULL, |
|
| 1099 |
... |
|
| 1100 |
) {
|
|
| 1101 | 4x |
if (do_exclude_split(exclude_levels, .spl_context)) {
|
| 1102 | 2x |
NULL |
| 1103 |
} else {
|
|
| 1104 | 2x |
a_freq_j( |
| 1105 | 2x |
df = df, |
| 1106 | 2x |
labelstr = labelstr, |
| 1107 | 2x |
.var = .var, |
| 1108 | 2x |
.spl_context = .spl_context, |
| 1109 | 2x |
.df_row = .df_row, |
| 1110 | 2x |
.N_col = .N_col, |
| 1111 | 2x |
.alt_df_full = .alt_df_full, |
| 1112 | 2x |
.stats = .stats, |
| 1113 | 2x |
.formats = .formats, |
| 1114 | 2x |
.indent_mods = .indent_mods, |
| 1115 | 2x |
.labels_n = .labels_n, |
| 1116 |
... |
|
| 1117 |
) |
|
| 1118 |
} |
|
| 1119 |
} |
| 1 |
get_all_comp_lvls <- function(colspan_trt_map) {
|
|
| 2 | 5x |
nonactlvl <- unique(as.character(colspan_trt_map[[1]]))[2] |
| 3 | 5x |
unique(as.character(colspan_trt_map[colspan_trt_map[[1]] == nonactlvl, 2])) |
| 4 |
} |
|
| 5 | ||
| 6 |
get_first_comp_lvl <- function(colspan_trt_map) {
|
|
| 7 | ! |
get_all_comp_lvls(colspan_trt_map)[1] |
| 8 |
} |
|
| 9 | ||
| 10 |
get_comp_path <- function(map, lvl) {
|
|
| 11 | 50x |
rw <- map[map[[2]] == lvl, ] |
| 12 | 50x |
c(names(map)[1], rw[[1]], names(map)[2], rw[[2]]) |
| 13 |
} |
|
| 14 | ||
| 15 | ||
| 16 |
## ugh. thisisfine.jpg XXX :( :( :( |
|
| 17 |
## |
|
| 18 |
## this is where we munge treatment names/labels into comparison versions |
|
| 19 |
do_sib_val_surgery <- function(splval, comp_lvl, newexargs, spl, comp_label) {
|
|
| 20 | 118x |
expr <- make_subset_expr(spl, splval) |
| 21 | 118x |
splval@subset_expression <- expr |
| 22 | 118x |
splval@value <- make_comp_name(splval@value, comp_lvl) |
| 23 | 118x |
splval@label <- make_comp_name(splval@label, comp_label) |
| 24 | 118x |
args <- c(newexargs, splval@extra) |
| 25 | 118x |
splval@extra <- args |
| 26 | 118x |
splval |
| 27 |
} |
|
| 28 | ||
| 29 | ||
| 30 |
## do "surgery" on all of the original values to enforce uniqueness and |
|
| 31 |
## correct comparison labels |
|
| 32 |
surgical_suite <- function(orig_ret, comp_lvl, newexargs, spl, combo_map) {
|
|
| 33 | 25x |
out <- orig_ret |
| 34 | 25x |
comp_label <- get_comp_label(comp_lvl, orig_ret, combo_map) |
| 35 | 25x |
out$values <- lapply( |
| 36 | 25x |
out$values, |
| 37 | 25x |
do_sib_val_surgery, |
| 38 | 25x |
comp_lvl = comp_lvl, |
| 39 | 25x |
newexargs = newexargs, |
| 40 | 25x |
spl = spl, |
| 41 | 25x |
comp_label = comp_label |
| 42 |
) |
|
| 43 | 25x |
out$labels <- vapply(out$values, obj_label, "") |
| 44 | 25x |
newnms <- vapply(out$values, value_names, "") |
| 45 | 25x |
out <- lapply( |
| 46 | 25x |
out, |
| 47 | 25x |
function(part) {
|
| 48 | 100x |
names(part) <- newnms |
| 49 | 100x |
part |
| 50 |
} |
|
| 51 |
) |
|
| 52 | 25x |
names(out) <- names(orig_ret) |
| 53 | 25x |
out |
| 54 |
} |
|
| 55 | ||
| 56 |
get_comp_label <- function(comp_level, orig_ret, combo_map) {
|
|
| 57 | 25x |
if (!is.null(combo_map) && comp_level %in% combo_map$valname) {
|
| 58 | 4x |
ret <- unique(combo_map$label[combo_map$valname == comp_level]) |
| 59 |
} else {
|
|
| 60 | 21x |
ind <- which(comp_level == names(orig_ret$values)) |
| 61 | 21x |
stopifnot(length(ind) == 1) |
| 62 | 21x |
ret <- obj_label(orig_ret$values[[ind]]) |
| 63 |
} |
|
| 64 | 25x |
ret |
| 65 |
} |
|
| 66 | ||
| 67 |
add_sib_facets <- function(comp_level, colspan_trt_map, combo_map_all) {
|
|
| 68 | 25x |
function(ret, spl, .spl_context, fulldf) {
|
| 69 | 25x |
combo_map <- NULL |
| 70 | 25x |
if (!is.null(combo_map_all)) {
|
| 71 | 14x |
combo_map <- combo_map_all[combo_map_all$comparator_level == comp_level, ] |
| 72 |
} |
|
| 73 | 25x |
comp_path <- get_comp_path(colspan_trt_map, comp_level) |
| 74 | 25x |
ret <- insert_subset_exprs(ret, spl, comp_path = comp_path) |
| 75 | 25x |
out <- ret |
| 76 | 25x |
exargs <- list(ref_path = get_comp_path(colspan_trt_map, comp_level)) |
| 77 | 25x |
if (NROW(combo_map) > 0) {
|
| 78 | 14x |
for (i in seq_len(NROW(combo_map))) {
|
| 79 | 18x |
out <- add_combo_facet( |
| 80 | 18x |
combo_map$valname[i], |
| 81 | 18x |
combo_map$label[i], |
| 82 | 18x |
combo_map$levelcombo[[i]], |
| 83 | 18x |
combo_map$exargs[[i]] |
| 84 |
)( |
|
| 85 | 18x |
ret = out, |
| 86 | 18x |
spl = spl, |
| 87 | 18x |
.spl_context = .spl_context, |
| 88 | 18x |
fulldf = fulldf |
| 89 |
) |
|
| 90 |
} # } |
|
| 91 |
} |
|
| 92 | 25x |
out <- surgical_suite(out, comp_level, exargs, spl = spl, combo_map = combo_map_all) |
| 93 | 25x |
names(out) <- names(ret) |
| 94 | 25x |
out |
| 95 |
} |
|
| 96 |
} |
|
| 97 | ||
| 98 |
#' Make Multi-comparator Split Function |
|
| 99 |
#' |
|
| 100 |
#' Create a custom splitting function suitable for creating risk |
|
| 101 |
#' difference columns against one or more comparators. This is used |
|
| 102 |
#' within `col_struct_w_risk_diffs`. |
|
| 103 |
#' |
|
| 104 |
#' @param colspan_trt_map (`data.frame`)\cr A data.frame defining the |
|
| 105 |
#' active and non-active groups of treatment arms, including |
|
| 106 |
#' combination arms defined in `combo_levels_map`, as returned by |
|
| 107 |
#' [create_colspan_map()]. |
|
| 108 |
#' |
|
| 109 |
#' @param combo_levels_map (`data.frame` or `NULL`)\cr NULL (the |
|
| 110 |
#' default) or a data.frame indicating combination levels to added |
|
| 111 |
#' to some or all blocks of comparisons. See Details. |
|
| 112 |
#' |
|
| 113 |
#' @param comp_level_map (`data.frame` or `NULL`)\cr A data.frame with |
|
| 114 |
#' columns `active` and `comparator` indivating which risk |
|
| 115 |
#' difference comparisons to include in the column structure, or |
|
| 116 |
#' `NULL` (the default), indicating all active vs non-active |
|
| 117 |
#' pairwise comparisons as defined `colspan_trt_map` treatment |
|
| 118 |
#' groupings. |
|
| 119 |
#' |
|
| 120 |
#' @param .pre `(list)`\cr A list of additional preprocessing |
|
| 121 |
#' functions to be provided to `make_split_fun`. Defaults to |
|
| 122 |
#' `list()`. |
|
| 123 |
#' |
|
| 124 |
#' @param .post `(list)`\cr A list of additional post-processing |
|
| 125 |
#' functions to be provided to `make_split_fun` *after* those |
|
| 126 |
#' which provide this function's primary multi-comparator |
|
| 127 |
#' functionality. Defaults to `list()` |
|
| 128 |
#' |
|
| 129 |
#' @return A split function suitable for use in both `split_rows_by` |
|
| 130 |
#' and `split_cols_by`. |
|
| 131 |
#' |
|
| 132 |
#' @details |
|
| 133 |
#' This split function is intended to create a set of risk difference |
|
| 134 |
#' or similar columns. As such it will automatically exclude the facet |
|
| 135 |
#' for each comparator level (e.g., Placebo vs Placebo) as determined |
|
| 136 |
#' by the last element of each element of `comp_level_paths`. |
|
| 137 |
#' |
|
| 138 |
#' Further control of facets is provided by `comp_level_map`. If |
|
| 139 |
#' `NULL` (the default), all non-control/reference groups will be |
|
| 140 |
#' compared pairwise with all control/reference groups as defined by |
|
| 141 |
#' the grouping in `colspan_trt_map`. |
|
| 142 |
#' |
|
| 143 |
#' If specified, `comp_level_map` must be a `data.frame` (including |
|
| 144 |
#' `tbl_df`) with three columns: |
|
| 145 |
#' |
|
| 146 |
#' - `active` - (character) the value to be compared to a reference level, |
|
| 147 |
#' - `comparator` - (character) the level that should be compared against, and |
|
| 148 |
#' - `active_is_combo` - (logical) is the level specified in `active` a virtual combination level. |
|
| 149 |
#' - `comparator_is_combo` - (logical) is the level specified in `comparator` a virtual combination level. |
|
| 150 |
#' |
|
| 151 |
#' If a `data.frame` with only the `active` and `comparator` columns |
|
| 152 |
#' is given for `comp_level_map`, `active_is_combo` and |
|
| 153 |
#' `comparator_is_combo` are inferred from `colspan_trt_map`. |
|
| 154 |
#' |
|
| 155 |
#' If any rows of `comp_level_map` have `active_is_combo == TRUE` or |
|
| 156 |
#' `comparator_is_combo`, the relevant values in those rows *must* |
|
| 157 |
#' also appear in `combo_levels_map` with the correct level for |
|
| 158 |
#' comp_level (or the `select_all_levels` sentinel value which |
|
| 159 |
#' indicates inclusion for all comparators). |
|
| 160 |
#' |
|
| 161 |
#' If specified, `combo_levels_map` must be a `data.frame` (including `tbl_df`) |
|
| 162 |
#' with the following columns: |
|
| 163 |
#' |
|
| 164 |
#' - `valname` - (`character`) The name(s) for the combination level(s), |
|
| 165 |
#' - `label` - (`character`) the label(s) for the combination level(s), |
|
| 166 |
#' - `levelcombo` - (`list` of `character`) the levels of the split |
|
| 167 |
#' variable to be combined, or `select_all_levels` for all levels, |
|
| 168 |
#' - `exargs` - (`list`) the extra_args values for each combo level. If not |
|
| 169 |
#' present this will be assumed to be `list()` for all combo levels. |
|
| 170 |
#' - `compare_against` - (`list` of `character`) Optional. The reference level(s) the |
|
| 171 |
#' combo level should be compared against, or `select_all_levels` for |
|
| 172 |
#' inclusion against all comparators. |
|
| 173 |
#' - `is_control` - (`logical`) Optional. Is this combination level going |
|
| 174 |
#' to be used as a reference level (must appear as the last element in |
|
| 175 |
#' one of `comp_level_paths` if so). |
|
| 176 |
#' |
|
| 177 |
#' When specifying `combo_levels_map` if the `compare_against` column |
|
| 178 |
#' is omitted, comparison against all reference levels will be |
|
| 179 |
#' performed for all combination levels. If `is_control` is omitted, |
|
| 180 |
#' it will be assumed as `FALSE` for all combination levels. |
|
| 181 |
#' |
|
| 182 |
#' Order of combination levels when multiple are present for a single |
|
| 183 |
#' comparator, as well as their position relative to non-combination |
|
| 184 |
#' comparisons, is determined by row order in `combo_levels_map`. |
|
| 185 |
#' |
|
| 186 |
#' Labels and names of comparison columns involving combination levels |
|
| 187 |
#' will be automatically computed in the form of |
|
| 188 |
#' `"<combo level name/label> vs <ref group name>"`. Note currently |
|
| 189 |
#' ref group *name* is always used as it needs to be inferable from |
|
| 190 |
#' `colspan_trt_map`. |
|
| 191 |
#' |
|
| 192 |
#' |
|
| 193 |
#' The comparator reference path is calculated based on |
|
| 194 |
#' `colspan_trt_map` and then added as `ref_path` to the extra_args |
|
| 195 |
#' associated with generated facet. As such, analysis (or content) |
|
| 196 |
#' functions used underneath a split using the generated split |
|
| 197 |
#' function must accept either `ref_path` or `...`. |
|
| 198 |
#' |
|
| 199 |
#' @note It is not currently possible to use a virtual combination |
|
| 200 |
#' level as a comparator/reference group. If you need this |
|
| 201 |
#' functionality please contact the maintainers by filing an issue |
|
| 202 |
#' at https://github.com/johnsonandjohnson/junco/issues |
|
| 203 |
#' |
|
| 204 |
#' @family std_col_struct |
|
| 205 |
#' |
|
| 206 |
#' @export |
|
| 207 |
make_multicomp_splfun <- function( |
|
| 208 |
colspan_trt_map, |
|
| 209 |
combo_levels_map = NULL, |
|
| 210 |
comp_level_map = NULL, |
|
| 211 |
.pre = list(), |
|
| 212 |
.post = list() |
|
| 213 |
) {
|
|
| 214 | 12x |
colspan_trt_map <- enrich_colspan_map(colspan_trt_map, combo_levels_map) |
| 215 | 12x |
if (is.null(comp_level_map)) {
|
| 216 | 5x |
comp_levels <- get_all_comp_lvls(colspan_trt_map) |
| 217 |
} else {
|
|
| 218 | 7x |
comp_levels <- unique(as.character(comp_level_map$comparator)) |
| 219 |
} |
|
| 220 | 12x |
if (!is.null(combo_levels_map)) {
|
| 221 | 6x |
combo_levels_map <- expand_combo_map(combo_levels_map, ref_lvls = comp_levels) |
| 222 |
} |
|
| 223 | ||
| 224 | 12x |
funlst <- list( |
| 225 | 12x |
function(ret, spl, .spl_context, fulldf) {
|
| 226 | 12x |
sib_sets <- lapply( |
| 227 | 12x |
comp_levels, |
| 228 | 12x |
function(lvl) {
|
| 229 | 25x |
sib_fac_fun <- add_sib_facets( |
| 230 | 25x |
lvl, |
| 231 | 25x |
colspan_trt_map = colspan_trt_map, |
| 232 | 25x |
combo_map_all = combo_levels_map |
| 233 |
) |
|
| 234 | 25x |
sib_fac_fun(ret, spl, .spl_context, fulldf) |
| 235 |
} |
|
| 236 |
) |
|
| 237 | 12x |
out <- lapply( |
| 238 | 12x |
names(ret), |
| 239 | 12x |
function(nm) {
|
| 240 | 36x |
unlist(lapply(seq_along(sib_sets), function(ii) sib_sets[[ii]][[nm]]), recursive = FALSE) |
| 241 |
} |
|
| 242 |
) |
|
| 243 | 12x |
names(out) <- names(ret) |
| 244 | 12x |
out |
| 245 |
} |
|
| 246 |
) |
|
| 247 | ||
| 248 | 12x |
make_split_fun( |
| 249 | 12x |
pre = .pre, |
| 250 | 12x |
post = c( |
| 251 | 12x |
funlst, |
| 252 | 12x |
apply_comp_map( |
| 253 | 12x |
splvar = names(colspan_trt_map)[2], |
| 254 | 12x |
comp_levels, |
| 255 | 12x |
comp_map = comp_level_map, |
| 256 | 12x |
combo_map = combo_levels_map |
| 257 |
), |
|
| 258 | 12x |
.post |
| 259 |
) |
|
| 260 |
) |
|
| 261 |
} |
|
| 262 | ||
| 263 |
## add combo levels to colspan map IFF *none* |
|
| 264 |
## are present |
|
| 265 |
enrich_colspan_map <- function(colspan_map, combodf) {
|
|
| 266 |
## assume if any combo levels are present that the |
|
| 267 |
## missing ones are intentional |
|
| 268 |
if ( |
|
| 269 | 12x |
is.null(combodf) || |
| 270 | 12x |
any(combodf$valname %in% colspan_map[[2]]) |
| 271 |
) {
|
|
| 272 | 9x |
return(colspan_map) |
| 273 |
} |
|
| 274 | 3x |
combo_levs <- combodf$valname |
| 275 | ||
| 276 | 3x |
if (!("is_control" %in% names(combodf))) {
|
| 277 | 2x |
combodf$is_control <- FALSE |
| 278 |
} |
|
| 279 | ||
| 280 |
## second one is control spanner |
|
| 281 | 3x |
span_levs <- unique(colspan_map[[1]]) |
| 282 | ||
| 283 | 3x |
in_ctrl <- function(spn) spn == span_levs[2] |
| 284 | 3x |
subset_combo_lvls <- function(spn) {
|
| 285 | 10x |
combo_levs[combodf$is_control == in_ctrl(spn)] |
| 286 |
} |
|
| 287 | ||
| 288 | 3x |
mapspl <- split(colspan_map, colspan_map[[1]]) |
| 289 | 3x |
rws <- lapply( |
| 290 | 3x |
span_levs, |
| 291 | 3x |
function(nm) {
|
| 292 | 6x |
cur_levs <- subset_combo_lvls(nm) |
| 293 | 6x |
rbind( |
| 294 | 6x |
mapspl[[nm]], |
| 295 | 6x |
if (length(cur_levs) > 0) {
|
| 296 | 4x |
setNames( |
| 297 | 4x |
data.frame(nm, subset_combo_lvls(nm)), |
| 298 | 4x |
names(colspan_map) |
| 299 |
) |
|
| 300 |
} |
|
| 301 |
) |
|
| 302 |
} |
|
| 303 |
) |
|
| 304 | ||
| 305 | 3x |
do.call(rbind, rws) |
| 306 |
} |
|
| 307 | ||
| 308 | ||
| 309 | 248x |
make_comp_name <- function(act_nm, comp_nm) paste0(act_nm, " vs ", comp_nm) |
| 310 | ||
| 311 |
## conversion of names/labels to comparison versions is now |
|
| 312 |
## handled (much) later, in surgical_suite and apply_comp_map |
|
| 313 |
expand_combo_map <- function(combo_map, ref_lvls) {
|
|
| 314 | 6x |
if (NROW(combo_map) == 0) {
|
| 315 | ! |
return(combo_map) |
| 316 |
} |
|
| 317 | ||
| 318 | 6x |
if (!("compare_against" %in% names(combo_map))) {
|
| 319 | 6x |
combo_map$compare_against <- lapply(seq_len(NROW(combo_map)), function(i) select_all_levels) |
| 320 |
} |
|
| 321 | ||
| 322 | 6x |
rws <- lapply( |
| 323 | 6x |
seq_len(NROW(combo_map)), |
| 324 | 6x |
function(ii) {
|
| 325 | 12x |
first_ref <- ref_lvls[1] |
| 326 | 12x |
remaining_refs <- ref_lvls[-1] |
| 327 | 12x |
mp_rw <- combo_map[ii, ] |
| 328 | 12x |
comp_against <- mp_rw$compare_against[[1]] |
| 329 | 12x |
if (is.null(comp_against) || is(comp_against, "AllLevelsSentinel")) {
|
| 330 | 12x |
comp_against <- ref_lvls |
| 331 |
} |
|
| 332 | ||
| 333 | 12x |
if (mp_rw$valname %in% ref_lvls) {
|
| 334 | 4x |
mp_rw$compare_against[[1]] <- character() |
| 335 | 4x |
mp_rw$comparator_level <- "" |
| 336 | 4x |
rws_out <- list(mp_rw) |
| 337 |
} else {
|
|
| 338 |
## TODO this seems overkill for what is left inside |
|
| 339 |
## the lapply, refactor into saner form |
|
| 340 | 8x |
rws_out <- lapply(comp_against, function(cur_ref_lvl) {
|
| 341 | 18x |
ref_lvl_ind <- match(cur_ref_lvl, ref_lvls) |
| 342 | 18x |
cur_rw <- combo_map[ii, ] |
| 343 | 18x |
cur_rw$comparator_level <- cur_ref_lvl |
| 344 | 18x |
cur_rw |
| 345 |
}) |
|
| 346 |
} |
|
| 347 | ||
| 348 | 12x |
do.call(rbind.data.frame, rws_out) |
| 349 |
} |
|
| 350 |
) |
|
| 351 | 6x |
ret <- do.call(rbind.data.frame, rws) |
| 352 |
## never compare level against itself |
|
| 353 |
## needed to (easily) support combo control grps |
|
| 354 | 6x |
ret <- ret[ret$valname != ret$comparator_level, ] |
| 355 | 6x |
ret |
| 356 |
} |
|
| 357 | ||
| 358 | ||
| 359 |
apply_comp_map <- function(splvar, ref_lvls, comp_map, combo_map) {
|
|
| 360 | 12x |
function(ret, spl, fulldf, ...) {
|
| 361 | 12x |
all_lvls <- levels(fulldf[[splvar]]) |
| 362 | ||
| 363 | 12x |
if (is.null(comp_map)) {
|
| 364 | 5x |
comp_map <- make_dflt_comp_map(fulldf, splvar, ref_lvls, combo_map) |
| 365 |
} |
|
| 366 | 12x |
lvls_to_keep <- make_comp_name(comp_map$active, comp_map$comparator) |
| 367 | 12x |
restrict_facets(lvls_to_keep, op = "keep")(ret, spl, fulldf) |
| 368 |
} |
|
| 369 |
} |
|
| 370 | ||
| 371 |
#' @export |
|
| 372 |
#' @rdname make_multicomp_splfun |
|
| 373 |
#' @param df (`data.frame`)\cr Data used to derive the available levels for |
|
| 374 |
#' the splitting variable `spl_var` and to construct the default comparison |
|
| 375 |
#' map when `comp_level_map` is not provided. |
|
| 376 |
#' @param spl_var (`character(1)`)\cr Name of the splitting variable (typically |
|
| 377 |
#' the treatment variable) whose levels define active and comparator groups. |
|
| 378 |
#' @param ref_lvls (`character`)\cr The level names to be treated as reference |
|
| 379 |
#' (control) groups. Comparisons will be formed against these levels. |
|
| 380 |
#' @param combo_map (`data.frame`)\cr A combination levels map (i.e., the value |
|
| 381 |
#' passed to `combo_levels_map`) used to include virtual combination levels in |
|
| 382 |
#' the default comparison map. |
|
| 383 |
#' @return For `make_dflt_comp_map()`, a `data.frame` with columns |
|
| 384 |
#' `active`, `comparator`, `active_is_combo`, and `comparator_is_combo`, |
|
| 385 |
#' listing the comparisons to keep and whether either side is a virtual |
|
| 386 |
#' combination level. |
|
| 387 |
make_dflt_comp_map <- function(df, spl_var, ref_lvls, combo_map) {
|
|
| 388 | 6x |
all_lvls <- as.character(levels(df[[spl_var]])) |
| 389 | ||
| 390 | 6x |
non_ref <- setdiff(all_lvls, ref_lvls) |
| 391 | 6x |
base_rws <- lapply( |
| 392 | 6x |
ref_lvls, |
| 393 | 6x |
function(ref_lvl_i) {
|
| 394 | 14x |
data.frame(active = non_ref, comparator = ref_lvl_i, active_is_combo = FALSE, comparator_is_combo = FALSE) |
| 395 |
} |
|
| 396 |
) |
|
| 397 | ||
| 398 | 6x |
if ("is_control" %in% names(combo_map)) {
|
| 399 | 3x |
act_combo_map <- combo_map[!combo_map$is_control, ] |
| 400 |
} else {
|
|
| 401 | 3x |
act_combo_map <- combo_map |
| 402 |
} |
|
| 403 | 6x |
combo_rws <- combodf_to_comp_map(combodf = act_combo_map, ref_lvls = ref_lvls, all_base_lvls = all_lvls) |
| 404 | 6x |
ret <- do.call(rbind.data.frame, c(base_rws, list(combo_rws))) |
| 405 | ||
| 406 | 6x |
ret$tmp_fact <- factor(ret$comparator, levels = ref_lvls) |
| 407 | 6x |
o <- order(ret$tmp_fact) |
| 408 | 6x |
ret <- ret[o, ] |
| 409 | 6x |
ret$tmp_fact <- NULL |
| 410 | 6x |
ret |
| 411 |
} |
|
| 412 | ||
| 413 | ||
| 414 |
combodf_to_comp_map <- function(combodf, ref_lvls, all_base_lvls) {
|
|
| 415 | 6x |
nrcombo <- NROW(combodf) |
| 416 | 6x |
if (nrcombo == 0) {
|
| 417 | 2x |
return(NULL) |
| 418 |
} |
|
| 419 | ||
| 420 | 4x |
if (!("is_control" %in% names(combodf))) {
|
| 421 | 1x |
combodf$is_control <- combodf$valname %in% ref_lvls |
| 422 |
} |
|
| 423 | ||
| 424 | 4x |
non_ref_lvls <- c( |
| 425 | 4x |
setdiff(all_base_lvls, ref_lvls), |
| 426 | 4x |
combodf$valname[!combodf$is_control] |
| 427 |
) |
|
| 428 | 4x |
if (!("compare_against" %in% names(combodf))) {
|
| 429 | 1x |
combodf$compare_against <- ifelse( |
| 430 | 1x |
combodf$is_control, |
| 431 | 1x |
replicate(nrcombo, list(non_ref_lvls)), |
| 432 | 1x |
replicate(nrcombo, list(ref_lvls)) |
| 433 |
) |
|
| 434 |
} |
|
| 435 | ||
| 436 | 4x |
if (any(combodf$is_control) && !all(combodf$valname[combodf$is_control] %in% ref_lvls)) {
|
| 437 | ! |
stop( |
| 438 | ! |
"Combination levels [", |
| 439 | ! |
paste(combodf$valname[combodf$is_control], collapse = ", "), |
| 440 | ! |
"] were listed as control groups in the combination df but are ", |
| 441 | ! |
"missing from specified reference levels" |
| 442 |
) |
|
| 443 |
} |
|
| 444 | ||
| 445 | 4x |
rws <- lapply( |
| 446 | 4x |
seq_len(nrcombo), |
| 447 | 4x |
function(i) {
|
| 448 | 12x |
cur_rw <- combodf[i, ] |
| 449 | 12x |
is_ctrl <- cur_rw$is_control |
| 450 | 12x |
comp_against <- cur_rw$compare_against[[1]] |
| 451 | 12x |
if (is(comp_against, "AllLevelsSentinel")) {
|
| 452 | 10x |
if (is_ctrl) {
|
| 453 | ! |
comp_against <- non_ref_lvls |
| 454 |
} else {
|
|
| 455 | 10x |
comp_against <- ref_lvls |
| 456 |
} |
|
| 457 |
} |
|
| 458 | 12x |
if (is_ctrl) {
|
| 459 | ! |
data.frame( |
| 460 | ! |
active = comp_against, |
| 461 | ! |
comparator = cur_rw$valname, |
| 462 | ! |
active_is_combo = !(comp_against %in% all_base_lvls), |
| 463 | ! |
comparator_is_combo = TRUE |
| 464 |
) |
|
| 465 |
} else {
|
|
| 466 | 12x |
data.frame( |
| 467 | 12x |
active = cur_rw$valname, |
| 468 | 12x |
comparator = comp_against, |
| 469 | 12x |
active_is_combo = TRUE, |
| 470 | 12x |
comparator_is_combo = comp_against %in% combodf$valname |
| 471 |
) |
|
| 472 |
} |
|
| 473 |
} |
|
| 474 |
) |
|
| 475 | ||
| 476 |
## if there are combinations acting as both active and reference |
|
| 477 |
## levels the comparisons between them will be duplicated |
|
| 478 | 4x |
ret <- do.call(rbind.data.frame, rws) |
| 479 | 4x |
dups <- duplicated(ret[, c("active", "comparator")])
|
| 480 | 4x |
ret[!dups, ] |
| 481 |
} |
|
| 482 | ||
| 483 | ||
| 484 |
#' Standard Column Structure With Grouped Treatments and Difference Columns |
|
| 485 |
#' |
|
| 486 |
#' @param lyt (`PreDataTableLayouts`). The layout to modify. This |
|
| 487 |
#' should virtually always be the object returned by |
|
| 488 |
#' `basic_table`. |
|
| 489 |
#' @param colspan_trt_map (`data.frame`). The spanning label map for |
|
| 490 |
#' the main columns, as given by `create_colspan_map`. |
|
| 491 |
#' @param combo_map_df (`data.frame` or `NULL`). A combination data |
|
| 492 |
#' frame as defined by [add_combo_levels()] with an additional |
|
| 493 |
#' `is_control` column indicating whether the virtual level will |
|
| 494 |
#' act as a reference (`TRUE`) or active (`FALSE`) group. |
|
| 495 |
#' @param comp_map (`data.frame` or `NULL`). A data.frame with columns |
|
| 496 |
#' `"active"`, `"comparator"`, `"active_is_combo"` and |
|
| 497 |
#' `"comparator_is_combo"`, or `NULL` indicating the default |
|
| 498 |
#' comparison behavior (See Details). |
|
| 499 |
#' @param diff_cols (`logical(1)`). Whether the risk difference |
|
| 500 |
#' column structure should be included (`TRUE`, the default) or |
|
| 501 |
#' not (`FALSE`). |
|
| 502 |
#' @param diffs_label (`character(1)`). The spanning label for the |
|
| 503 |
#' risk difference section of columns |
|
| 504 |
#' |
|
| 505 |
#' @param .main_pre (`list` of `function`s). Passed to |
|
| 506 |
#' [rtables::make_split_fun()] as `pre` for treatment split in main |
|
| 507 |
#' structure. |
|
| 508 |
#' @param .main_post (`list` of `function`s). Passed to |
|
| 509 |
#' [rtables::make_split_fun()] as `post` for treatment split in |
|
| 510 |
#' main structure. |
|
| 511 |
#' @param .rr_pre (`list` of `function`s). Passed to |
|
| 512 |
#' [make_multicomp_splfun()] as `.pre` for risk difference |
|
| 513 |
#' faceting. |
|
| 514 |
#' @param .rr_post (`list` of `function`s). Passed to |
|
| 515 |
#' [make_multicomp_splfun()] as `.post` for risk difference |
|
| 516 |
#' faceting. |
|
| 517 |
#' |
|
| 518 |
#' @details |
|
| 519 |
#' |
|
| 520 |
#' This function combines multiple `rtables` column splitting |
|
| 521 |
#' instructions with customized split functions to create a column |
|
| 522 |
#' structure with treatment columns for each treatment arm (optionally |
|
| 523 |
#' including combination arms), grouped by active and non-active, with |
|
| 524 |
#' risk difference columns comparing active arm(s) against one or more |
|
| 525 |
#' non-active controls. It is intended for use in layouts that will |
|
| 526 |
#' use [a_freq_j()] or similar `junco`-style analysis functions which |
|
| 527 |
#' support risk difference columns and accept a `ref_path` argument. |
|
| 528 |
#' |
|
| 529 |
#' It is equivalent to the following sequence of layout instructions: |
|
| 530 |
#' |
|
| 531 |
#' 1. splitting on a colspan labeling variable with |
|
| 532 |
#' [rtables::trim_levels_to_map()] as the split function; |
|
| 533 |
#' 2. splitting on treatment; |
|
| 534 |
#' 3. adding a (non-nested) overall column acting as the risk difference spanning label; and finally |
|
| 535 |
#' 4. splitting on treatment using [make_multicomp_splfun()] as the |
|
| 536 |
#' split function |
|
| 537 |
#' |
|
| 538 |
#' In addition, it supports: |
|
| 539 |
#' |
|
| 540 |
#' - comparison against multiple control groups (as specified by `colspan_trt_map` and/or `comp_map`), |
|
| 541 |
#' - virtual combination-levels as active an/or control "treatments" (via `combo_map_df`), |
|
| 542 |
#' - full control of which comparisons are performed, and their order (via `comp_map`). |
|
| 543 |
#' |
|
| 544 |
#' If combination levels are declared via `combo_map_df` but none |
|
| 545 |
#' appear in `colspan_trt_map`, all combinations will be added to the |
|
| 546 |
#' appropriate group within the map based on `combo_map_df$is_control` |
|
| 547 |
#' (assumed to be `FALSE` if the column is missing), with a warning. |
|
| 548 |
#' |
|
| 549 |
#' If some combination levels *do* appear in `combo_map_df` but |
|
| 550 |
#' others do not, a warning will be thrown but the missing combination |
|
| 551 |
#' levels will *not* be added to the treatment map. |
|
| 552 |
#' |
|
| 553 |
#' By default (when `comp_map` is `NULL`), all active treatments, |
|
| 554 |
#' including active combinations, will be compared against all control |
|
| 555 |
#' groups. |
|
| 556 |
#' |
|
| 557 |
#' The risk difference section of the structure is declared using |
|
| 558 |
#' [make_multicomp_splfun()]. Reference paths are inferred |
|
| 559 |
#' automatically from `colspan_trt_map` (after combination levels have |
|
| 560 |
#' been added if necessary). |
|
| 561 |
#' |
|
| 562 |
#' For the purposes of pathin in the resulting structure, |
|
| 563 |
#' `diffs_label` will be both the split name and split value of the |
|
| 564 |
#' parent containing the individual risk difference columns. |
|
| 565 |
#' |
|
| 566 |
#' @returns `lyt` updated with the specified main and risk difference |
|
| 567 |
#' column structures added |
|
| 568 |
#' |
|
| 569 |
#' |
|
| 570 |
#' @family std_col_struct |
|
| 571 |
#' @export |
|
| 572 |
#' @examples |
|
| 573 |
#' colspan_var <- create_colspan_var( |
|
| 574 |
#' data.frame(TRT01A = factor(c("Placebo", "Active 1", "Active 2"))),
|
|
| 575 |
#' non_active_grp = "Placebo", |
|
| 576 |
#' non_active_grp_span_lbl = "Control", |
|
| 577 |
#' active_grp_span_lbl = "Active Treatment", |
|
| 578 |
#' colspan_var = "colspan_trt", |
|
| 579 |
#' trt_var = "TRT01A" |
|
| 580 |
#' ) |
|
| 581 |
#' colspan_trt_map <- create_colspan_map( |
|
| 582 |
#' colspan_var, |
|
| 583 |
#' non_active_grp = "Placebo", |
|
| 584 |
#' non_active_grp_span_lbl = "Control", |
|
| 585 |
#' active_grp_span_lbl = "Active Treatment", |
|
| 586 |
#' colspan_var = "colspan_trt", |
|
| 587 |
#' trt_var = "TRT01A" |
|
| 588 |
#' ) |
|
| 589 |
#' |
|
| 590 |
#' lyt <- basic_table() |> |
|
| 591 |
#' grouped_cols_w_diffs(colspan_trt_map) |> |
|
| 592 |
#' analyze("TRT01A", afun = function(x, ...) length(x))
|
|
| 593 |
#' |
|
| 594 |
#' build_table(lyt, colspan_var) |
|
| 595 | ||
| 596 |
grouped_cols_w_diffs <- function( |
|
| 597 |
lyt, |
|
| 598 |
colspan_trt_map, |
|
| 599 |
combo_map_df = NULL, |
|
| 600 |
## default behavior for comp_map is taken care of in make_multicomp_splfun |
|
| 601 |
comp_map = NULL, |
|
| 602 |
diff_cols = TRUE, |
|
| 603 |
diffs_label = "Risk Differences", |
|
| 604 |
.main_pre = list(), |
|
| 605 |
.main_post = list(), |
|
| 606 |
.rr_pre = list(), |
|
| 607 |
.rr_post = list() |
|
| 608 |
) {
|
|
| 609 | 5x |
trtvar <- names(colspan_trt_map)[2] |
| 610 | 5x |
spanvar <- names(colspan_trt_map)[1] |
| 611 | ||
| 612 | 5x |
if (!is.null(combo_map_df)) {
|
| 613 | 3x |
if (!("is_control" %in% names(combo_map_df))) {
|
| 614 | 1x |
combo_map_df$is_control <- FALSE |
| 615 |
} |
|
| 616 | 3x |
main_post <- lapply( |
| 617 | 3x |
seq_len(NROW(combo_map_df)), |
| 618 | 3x |
function(i) {
|
| 619 | 6x |
force(i) |
| 620 | 6x |
add_combo_facet( |
| 621 | 6x |
name = combo_map_df$valname[i], |
| 622 | 6x |
label = combo_map_df$label[i], |
| 623 | 6x |
levels = combo_map_df$levelcombo[[i]], |
| 624 | 6x |
extra = combo_map_df$exargs[[i]] |
| 625 |
) |
|
| 626 |
} |
|
| 627 |
) |
|
| 628 | ||
| 629 | 3x |
combo_nms <- combo_map_df$valname |
| 630 | ||
| 631 |
## we're guaranteed to have some combo levels at this point |
|
| 632 | 3x |
combo_found <- combo_nms %in% colspan_trt_map[[trtvar]] |
| 633 | 3x |
if (!any(combo_found)) {
|
| 634 | 3x |
message( |
| 635 | 3x |
"none of the combination levels appeared in the colspan treatment map;", |
| 636 | 3x |
" adding them automatically." |
| 637 |
) |
|
| 638 | 3x |
colspan_trt_map <- add_combo_levs_to_trtmap(colspan_trt_map, combo_map_df) |
| 639 | ! |
} else if (any(!combo_found)) {
|
| 640 | ! |
warning("some combination levels defined in combo_map_df do not appear in colspan_trt_map")
|
| 641 |
} |
|
| 642 |
} else {
|
|
| 643 | 2x |
main_post <- list() |
| 644 |
} |
|
| 645 | ||
| 646 | 5x |
main_post <- c(main_post, .trtmap_to_post_funs(colspan_trt_map), .main_post) |
| 647 | ||
| 648 | 5x |
main_splfun <- make_split_fun(pre = .main_pre, post = main_post) |
| 649 | ||
| 650 | 5x |
lyt <- lyt |> |
| 651 | 5x |
split_cols_by(names(colspan_trt_map)[1]) |> |
| 652 | 5x |
split_cols_by(trtvar, split_fun = main_splfun) |
| 653 | 5x |
if (diff_cols) {
|
| 654 | 4x |
rr_splfun <- make_multicomp_splfun( |
| 655 | 4x |
colspan_trt_map, |
| 656 | 4x |
comp_level_map = comp_map, |
| 657 | 4x |
combo_levels_map = combo_map_df, |
| 658 | 4x |
.pre = .rr_pre, |
| 659 | 4x |
.post = .rr_post |
| 660 |
) |
|
| 661 | ||
| 662 | 4x |
lyt <- lyt |> |
| 663 |
## this is inherently non-nested |
|
| 664 | 4x |
add_overall_col(label = diffs_label) |> |
| 665 | 4x |
split_cols_by(trtvar, split_fun = rr_splfun) |
| 666 |
} |
|
| 667 | 5x |
lyt |
| 668 |
} |
|
| 669 | ||
| 670 | ||
| 671 |
combos_to_trtmap_rows <- function(combo_map, trtmap_sect) {
|
|
| 672 | 5x |
ret <- data.frame( |
| 673 | 5x |
trtmap_sect[[1]][1], |
| 674 | 5x |
combo_map$valname |
| 675 |
) |
|
| 676 | 5x |
names(ret) <- names(trtmap_sect) |
| 677 | 5x |
ret |
| 678 |
} |
|
| 679 | ||
| 680 | ||
| 681 |
add_combo_levs_to_trtmap <- function(trtmap, combo_map) {
|
|
| 682 | 3x |
span_vec <- trtmap[[1]] |
| 683 | 3x |
spltrtmap <- split(trtmap, span_vec) |
| 684 | 3x |
spanorder <- unique(span_vec) ## need this because split sorts things... like a clown. |
| 685 | 3x |
activedf <- spltrtmap[[spanorder[1]]] |
| 686 | 3x |
ctrldf <- spltrtmap[[spanorder[2]]] |
| 687 | ||
| 688 | 3x |
if (any(combo_map$is_control)) {
|
| 689 | 2x |
ctrldf <- rbind( |
| 690 | 2x |
ctrldf, |
| 691 | 2x |
combos_to_trtmap_rows(combo_map[combo_map$is_control, ], ctrldf) |
| 692 |
) |
|
| 693 |
} |
|
| 694 | ||
| 695 | 3x |
if (any(!combo_map$is_control)) {
|
| 696 | 3x |
activedf <- rbind( |
| 697 | 3x |
activedf, |
| 698 | 3x |
combos_to_trtmap_rows(combo_map[!combo_map$is_control, ], activedf) |
| 699 |
) |
|
| 700 |
} |
|
| 701 | 3x |
rbind(activedf, ctrldf) |
| 702 |
} |
|
| 703 | ||
| 704 | ||
| 705 |
.trtmap_to_post_funs <- function(trtmap) {
|
|
| 706 | 8x |
splmap <- split(trtmap, trtmap[[1]]) |
| 707 | 8x |
lapply( |
| 708 | 8x |
splmap, |
| 709 | 8x |
.map_sect_to_post_fun |
| 710 |
) |
|
| 711 |
} |
|
| 712 | ||
| 713 |
.map_sect_to_post_fun <- function(map) {
|
|
| 714 | 16x |
cond_rm_facets(map[[2]], value = unique(map[[1]]), keep_matches = TRUE) |
| 715 |
} |
|
| 716 | ||
| 717 | ||
| 718 |
spans_trtvar_no_diffs <- function( |
|
| 719 |
lyt, |
|
| 720 |
colspan_trt_map, |
|
| 721 |
combo_map_df = NULL, |
|
| 722 |
trtvar = names(colspan_trt_map)[2], |
|
| 723 |
.pre = list(), |
|
| 724 |
.post = list() |
|
| 725 |
) {
|
|
| 726 | ||
| 727 | 6x |
spanvar <- names(colspan_trt_map)[1] |
| 728 | ||
| 729 | 6x |
if (!is.null(combo_map_df)) {
|
| 730 | ! |
if (!("is_control" %in% names(combo_map_df))) {
|
| 731 | ! |
combo_map_df$is_control <- FALSE |
| 732 |
} |
|
| 733 | ! |
main_post <- lapply( |
| 734 | ! |
seq_len(NROW(combo_map_df)), |
| 735 | ! |
function(i) {
|
| 736 | ! |
force(i) |
| 737 | ! |
add_combo_facet( |
| 738 | ! |
name = combo_map_df$valname[i], |
| 739 | ! |
label = combo_map_df$label[i], |
| 740 | ! |
levels = combo_map_df$levelcombo[[i]], |
| 741 | ! |
extra = combo_map_df$exargs[[i]] |
| 742 |
) |
|
| 743 |
} |
|
| 744 |
) |
|
| 745 | ||
| 746 | ! |
combo_nms <- combo_map_df$valname |
| 747 | ||
| 748 |
## we're guaranteed to have some combo levels at this point |
|
| 749 | ! |
combo_found <- combo_nms %in% colspan_trt_map[[trtvar]] |
| 750 | ! |
if (!any(combo_found)) {
|
| 751 | ! |
message( |
| 752 | ! |
"none of the combination levels appeared in the colspan treatment map;", |
| 753 | ! |
" adding them automatically." |
| 754 |
) |
|
| 755 | ! |
colspan_trt_map <- add_combo_levs_to_trtmap(colspan_trt_map, combo_map_df) |
| 756 | ! |
} else if (any(!combo_found)) {
|
| 757 | ! |
warning("some combination levels defined in combo_map_df do not appear in colspan_trt_map")
|
| 758 |
} |
|
| 759 |
} else {
|
|
| 760 | 6x |
main_post <- list() |
| 761 |
} |
|
| 762 | ||
| 763 | 6x |
main_post <- c( |
| 764 | 6x |
main_post, |
| 765 | 6x |
if (!is.null(colspan_trt_map)) .trtmap_to_post_funs(colspan_trt_map), |
| 766 | 6x |
.post |
| 767 |
) |
|
| 768 | ||
| 769 | 6x |
main_splfun <- make_split_fun(pre = .pre, post = main_post) |
| 770 | ||
| 771 | 6x |
if (!is.null(spanvar)) {
|
| 772 | 3x |
lyt <- lyt |> |
| 773 | 3x |
split_cols_by( |
| 774 | 3x |
spanvar, |
| 775 | 3x |
split_fun = keep_split_levels( |
| 776 | 3x |
only = unique(colspan_trt_map[[spanvar]]), |
| 777 | 3x |
reorder = TRUE |
| 778 |
) |
|
| 779 |
) |
|
| 780 |
} |
|
| 781 | 6x |
lyt <- split_cols_by(lyt, trtvar, split_fun = main_splfun, show_colcounts = TRUE) |
| 782 | 6x |
lyt |
| 783 |
} |
|
| 784 | ||
| 785 |
#' @rdname grouped_cols_w_diffs |
|
| 786 |
#' @param trtvar (`character(1)` or `NULL`)\cr the treatment variable |
|
| 787 |
#' to split by. Defaults to the treatment variable in |
|
| 788 |
#' `colspan_trt_map`. |
|
| 789 |
#' @param subgrp_var (`character(1)` or `NULL`)\cr the name of the |
|
| 790 |
#' subgroup variable to split by within the `trtvar` split |
|
| 791 |
#' @param subgrp_lbl (`character(1)` or `NULL`)\cr the spanning label |
|
| 792 |
#' to place over the subgroups, if different than `subgrp_var` |
|
| 793 |
#' @param total_facet (`logical(1)`)\cr should an overall facet be |
|
| 794 |
#' prepended to the partition defined by `subgrp_var`? Defaults to |
|
| 795 |
#' `TRUE`. |
|
| 796 |
#' @param total_lbl (`character(1)`)\cr Label to be used for the |
|
| 797 |
#' overall facet, if applicable. Defaults to `"Total"`; ignored if |
|
| 798 |
#' `total_facet` is `FALSE`. |
|
| 799 |
#' @param .pre (`list` of `function`s)\cr Passed to |
|
| 800 |
#' [rtables::make_split_fun()] as `pre` for treatment splitting. |
|
| 801 |
#' @param .post (`list` of `function`s)\cr Passed to |
|
| 802 |
#' [rtables::make_split_fun()] as `post` for treatment splitting |
|
| 803 |
#' after the standard column-structure processing. |
|
| 804 |
#' @details `grouped_cols_w_subgrps` creates a hierarchical column |
|
| 805 |
#' structure that splits by `trtvar`, underneath which is a |
|
| 806 |
#' spanning label over a split with a Total column along with |
|
| 807 |
#' columns for each level of `subgrp_var`. |
|
| 808 |
#' @export |
|
| 809 |
#' @examples |
|
| 810 |
#' dat <- create_colspan_var( |
|
| 811 |
#' data.frame( |
|
| 812 |
#' TRT01A = factor(rep(c("Placebo", "Active 1"), each = 2)),
|
|
| 813 |
#' SEX = factor(rep(c("Female", "Male"), 2))
|
|
| 814 |
#' ), |
|
| 815 |
#' non_active_grp = "Placebo", |
|
| 816 |
#' non_active_grp_span_lbl = "Control", |
|
| 817 |
#' active_grp_span_lbl = "Active Treatment", |
|
| 818 |
#' colspan_var = "colspan_trt", |
|
| 819 |
#' trt_var = "TRT01A" |
|
| 820 |
#' ) |
|
| 821 |
#' colspan_trt_map <- create_colspan_map( |
|
| 822 |
#' dat, |
|
| 823 |
#' non_active_grp = "Placebo", |
|
| 824 |
#' non_active_grp_span_lbl = "Control", |
|
| 825 |
#' active_grp_span_lbl = "Active Treatment", |
|
| 826 |
#' colspan_var = "colspan_trt", |
|
| 827 |
#' trt_var = "TRT01A" |
|
| 828 |
#' ) |
|
| 829 |
#' |
|
| 830 |
#' lyt <- basic_table() |> |
|
| 831 |
#' grouped_cols_w_subgrps( |
|
| 832 |
#' colspan_trt_map, |
|
| 833 |
#' subgrp_var = "SEX", |
|
| 834 |
#' subgrp_lbl = "SUB_*" |
|
| 835 |
#' ) |> |
|
| 836 |
#' analyze("TRT01A", afun = function(x, ...) length(x))
|
|
| 837 |
#' |
|
| 838 |
#' build_table(lyt, dat) |
|
| 839 |
grouped_cols_w_subgrps <- function( |
|
| 840 |
lyt, |
|
| 841 |
colspan_trt_map = NULL, |
|
| 842 |
combo_map_df = NULL, |
|
| 843 |
trtvar = names(colspan_trt_map)[2], |
|
| 844 |
subgrp_var = NULL, |
|
| 845 |
subgrp_lbl = subgrp_var, |
|
| 846 |
total_facet = TRUE, |
|
| 847 |
total_lbl = "Total", |
|
| 848 |
.pre = list(), |
|
| 849 |
.post = list() |
|
| 850 |
) {
|
|
| 851 | 4x |
if (is.null(trtvar)) {
|
| 852 | ! |
stop("trtvar must be specified if no colspan map is provided.")
|
| 853 |
} |
|
| 854 | ||
| 855 | 4x |
if (is.null(subgrp_var)) {
|
| 856 | ! |
stop( |
| 857 | ! |
"no subgroup variable specified, use grouped_cols_w_diffs with diff_cols=FALSE ", |
| 858 | ! |
"to create a grouped column structure with no subgrouping." |
| 859 |
) |
|
| 860 |
} |
|
| 861 | ||
| 862 |
## handles spanning variable if necessary and the treatment var split |
|
| 863 | 4x |
lyt <- spans_trtvar_no_diffs( |
| 864 | 4x |
lyt, |
| 865 | 4x |
colspan_trt_map = colspan_trt_map, |
| 866 | 4x |
combo_map_df = combo_map_df, |
| 867 | 4x |
trtvar = trtvar, |
| 868 | 4x |
.pre = .pre, |
| 869 | 4x |
.post = .post |
| 870 |
) |
|
| 871 | ||
| 872 | 4x |
lyt <- lyt |> |
| 873 | 4x |
split_cols_by( |
| 874 | 4x |
trtvar, |
| 875 | 4x |
split_fun = make_split_fun( |
| 876 | 4x |
post = list( |
| 877 | 4x |
add_overall_facet(subgrp_lbl, subgrp_lbl), |
| 878 | 4x |
restrict_facets(subgrp_lbl, op = "keep") |
| 879 |
) |
|
| 880 |
) |
|
| 881 |
) |> |
|
| 882 | 4x |
split_cols_by( |
| 883 | 4x |
subgrp_var, |
| 884 |
## NULL is default behavior so this is ok |
|
| 885 | 4x |
split_fun = if (total_facet) add_overall_level(total_lbl, first = TRUE) |
| 886 |
) |
|
| 887 | 4x |
lyt |
| 888 |
} |
|
| 889 | ||
| 890 |
#' Create Column Structure for a J&J-Style Shift Table |
|
| 891 |
#' |
|
| 892 |
#' @param lyt (`PreDataTableLayouts`)\cr The layout, typically as |
|
| 893 |
#' returned directly from `basic_table`. |
|
| 894 |
#' @param var (`character(1)`)\cr The variable defining the partition |
|
| 895 |
#' to create a shift table for. |
|
| 896 |
#' @param span_lbl (`character(1)`)\cr The spanning label for the |
|
| 897 |
#' shift portion of the column structure. Defaults to |
|
| 898 |
#' `"Baseline"`. |
|
| 899 |
#' @param .outer_spl_var (`character(1)`)\cr The variable to |
|
| 900 |
#' "split on" when creating the spanning labels. Must exist in the |
|
| 901 |
#' dataset(s) used in `build_table` but its values are irrelevant. |
|
| 902 |
#' |
|
| 903 |
#' @details This function creates a column structure akin to the following |
|
| 904 |
#' (with Grade 1 through Grade 5 representing the levels of `var`): |
|
| 905 |
#' |
|
| 906 |
#' ``` |
|
| 907 |
#' Baseline |
|
| 908 |
#' N Grade 1 Grade 2 Grade 3 Grade 4 Grade 5 Total |
|
| 909 |
#' —————————————————————————————————————————————————————————————— |
|
| 910 |
#' ``` |
|
| 911 |
#' |
|
| 912 |
#' @examples |
|
| 913 |
#' shift_data <- data.frame( |
|
| 914 |
#' BASE = factor(c("Grade 1", "Grade 2", "Grade 3")),
|
|
| 915 |
#' CHG = c("Improved", "Stable", "Worsened")
|
|
| 916 |
#' ) |
|
| 917 |
#' |
|
| 918 |
#' lyt <- basic_table() |> |
|
| 919 |
#' shift_tbl_col_struct("BASE") |>
|
|
| 920 |
#' analyze("CHG", afun = function(x, ...) length(x))
|
|
| 921 |
#' |
|
| 922 |
#' build_table(lyt, shift_data) |
|
| 923 |
#' @return `lyt` updated with the specified shift table column structure. |
|
| 924 |
#' @family std_col_struct |
|
| 925 |
#' @export |
|
| 926 |
shift_tbl_col_struct <- function(lyt, var, span_lbl = "Baseline", .outer_spl_var = var) {
|
|
| 927 | 1x |
outer_splfun <- make_split_fun( |
| 928 | 1x |
post = list( |
| 929 | 1x |
add_overall_facet("N", label = " "),
|
| 930 | 1x |
add_overall_facet("shift_table", label = span_lbl),
|
| 931 | 1x |
restrict_facets(c("N", "shift_table"), op = "keep")
|
| 932 |
) |
|
| 933 |
) |
|
| 934 | ||
| 935 | 1x |
inner_splfun <- make_split_fun( |
| 936 | 1x |
post = list( |
| 937 | 1x |
add_overall_facet("Total", label = "Total"),
|
| 938 | 1x |
function(ret, spl, fulldf, .spl_context) {
|
| 939 | 2x |
if (.spl_context$value[[1]] == "N") {
|
| 940 | 1x |
make_split_result("N", labels = c(N = "N"), list(N = fulldf), subset_exprs = list(N = quote(TRUE)))
|
| 941 |
} else {
|
|
| 942 | 1x |
ret |
| 943 |
} |
|
| 944 |
} |
|
| 945 |
) |
|
| 946 |
) |
|
| 947 | ||
| 948 | 1x |
lyt <- lyt |> |
| 949 | 1x |
split_cols_by(.outer_spl_var, split_fun = outer_splfun) |> |
| 950 | 1x |
split_cols_by(var, split_fun = inner_splfun) |
| 951 | 1x |
lyt |
| 952 |
} |
|
| 953 | ||
| 954 |
#' Standard All vs Some (e.g. Related AEs) column structure |
|
| 955 |
#' |
|
| 956 |
#' @inheritParams grouped_cols_w_subgrps |
|
| 957 |
#' @param subgrp_lbl (`character(1)`)\cr The label to put above the combination level representing `subgrp_lvls` |
|
| 958 |
#' @param all_lbl (`character(1)`)\cr The label to put above the "All" column |
|
| 959 |
#' @param subgrp_lvls (`characgter`)\cr All level(s) to be included in the `subgrp_lbl` column. |
|
| 960 |
#' |
|
| 961 |
#' @details |
|
| 962 |
#' This column structure generating function is for comparing a single portion of the data (as represented by |
|
| 963 |
#' level(s) of `subgrp_var`) against the full data, comparison of AE counts to treatment-related AE counts |
|
| 964 |
#' being a motivating example. |
|
| 965 |
#' |
|
| 966 |
#' |
|
| 967 |
#' @examples |
|
| 968 |
#' |
|
| 969 |
#' library(junco) |
|
| 970 |
#' dat <- create_colspan_var( |
|
| 971 |
#' data.frame( |
|
| 972 |
#' TRT01A = factor(rep(c("Placebo", "Active 1", "Active 2"), each = 5)),
|
|
| 973 |
#' GRADE = factor(rep(paste("Grade ", 1:5), 3))
|
|
| 974 |
#' ), |
|
| 975 |
#' non_active_grp = "Placebo", |
|
| 976 |
#' non_active_grp_span_lbl = "Control", |
|
| 977 |
#' active_grp_span_lbl = "Active Treatment", |
|
| 978 |
#' colspan_var = "colspan_trt", |
|
| 979 |
#' trt_var = "TRT01A" |
|
| 980 |
#' ) |
|
| 981 |
#' colspan_trt_map <- create_colspan_map( |
|
| 982 |
#' dat, |
|
| 983 |
#' non_active_grp = "Placebo", |
|
| 984 |
#' non_active_grp_span_lbl = "Control", |
|
| 985 |
#' active_grp_span_lbl = "Active Treatment", |
|
| 986 |
#' colspan_var = "colspan_trt", |
|
| 987 |
#' trt_var = "TRT01A" |
|
| 988 |
#' ) |
|
| 989 |
#' |
|
| 990 |
#' lyt <- basic_table() |> |
|
| 991 |
#' some_v_all_col_struct( |
|
| 992 |
#' colspan_trt_map, |
|
| 993 |
#' subgrp_var = "GRADE", |
|
| 994 |
#' subgrp_lvls = c("Grade 4", "Grade 5"),
|
|
| 995 |
#' subgrp_lbl = "High Grade", |
|
| 996 |
#' all_lbl = "All Grades") |
|
| 997 |
#' |
|
| 998 |
#' build_table(lyt, dat) |
|
| 999 |
#' |
|
| 1000 |
#' @export |
|
| 1001 |
some_v_all_col_struct <- function( |
|
| 1002 |
lyt, |
|
| 1003 |
colspan_trt_map = NULL, |
|
| 1004 |
combo_map_df = NULL, |
|
| 1005 |
trtvar = names(colspan_trt_map)[2], |
|
| 1006 |
subgrp_var = NULL, |
|
| 1007 |
subgrp_lbl = subgrp_var, |
|
| 1008 |
all_lbl, |
|
| 1009 |
subgrp_lvls, |
|
| 1010 |
.pre = list(), |
|
| 1011 |
.post = list() |
|
| 1012 |
) {
|
|
| 1013 | 2x |
if (is.null(trtvar)) {
|
| 1014 | ! |
stop("trtvar must be specified if no colspan map is provided.")
|
| 1015 |
} |
|
| 1016 | ||
| 1017 | 2x |
if (is.null(subgrp_var)) {
|
| 1018 | ! |
stop( |
| 1019 | ! |
"no subgroup variable specified, use grouped_cols_w_diffs with diff_cols=FALSE ", |
| 1020 | ! |
"to create a grouped column structure with no subgrouping." |
| 1021 |
) |
|
| 1022 |
} |
|
| 1023 | ||
| 1024 | 2x |
lyt <- spans_trtvar_no_diffs( |
| 1025 | 2x |
lyt, |
| 1026 | 2x |
colspan_trt_map = colspan_trt_map, |
| 1027 | 2x |
combo_map_df = combo_map_df, |
| 1028 | 2x |
trtvar = trtvar, |
| 1029 | 2x |
.pre = .pre, |
| 1030 | 2x |
.post = .post |
| 1031 |
) |
|
| 1032 | ||
| 1033 | 2x |
lyt <- lyt |> |
| 1034 | 2x |
split_cols_by( |
| 1035 | 2x |
subgrp_var, |
| 1036 | 2x |
split_fun = make_split_fun( |
| 1037 | 2x |
post = list( |
| 1038 | 2x |
add_overall_facet(all_lbl, label = all_lbl), |
| 1039 | 2x |
add_combo_facet( |
| 1040 | 2x |
name = paste0(subgrp_var, "_subset"), |
| 1041 | 2x |
label = subgrp_lbl, |
| 1042 | 2x |
levels = subgrp_lvls |
| 1043 |
), |
|
| 1044 | 2x |
restrict_facets( |
| 1045 | 2x |
c(all_lbl, paste0(subgrp_var, "_subset")), |
| 1046 | 2x |
op = "keep" |
| 1047 |
) |
|
| 1048 |
) |
|
| 1049 |
) |
|
| 1050 |
) |
|
| 1051 | ||
| 1052 | 2x |
lyt |
| 1053 |
} |
| 1 |
#' Patient years exposure |
|
| 2 |
#' @description Statistical/Analysis Function for presenting Patient years exposure summary data |
|
| 3 |
#' |
|
| 4 |
#' @name a_patyrs_j |
|
| 5 |
#' @order 1 |
|
| 6 |
NULL |
|
| 7 | ||
| 8 |
#' @describeIn a_patyrs_j Statistical Function for Patient years exposure summary data |
|
| 9 |
#' @param df (`data.frame`)\cr data set containing all analysis variables. |
|
| 10 |
#' @param .var (`string`)\cr variable name containing the patient years data. |
|
| 11 |
#' @param id (`string`)\cr subject variable name. |
|
| 12 |
#' @param .alt_df_full (`dataframe`)\cr alternative dataset for calculations. |
|
| 13 |
#' @param source (`string`)\cr source of data, either "alt_df" or "df". |
|
| 14 |
#' @param inriskdiffcol (`logical`)\cr flag indicating if the function is called within a risk difference column. |
|
| 15 |
#' |
|
| 16 |
#' @return |
|
| 17 |
#' * `s_patyrs_j()` returns a list containing the patient years statistics. |
|
| 18 |
#' The list of available statistics for can be viewed by running `junco_get_stats("a_patyrs_j")`.
|
|
| 19 |
#' Currently, this is just a single statistic `patyrs`, patient years of exposure. |
|
| 20 |
#' |
|
| 21 |
#' @keywords internal |
|
| 22 |
s_patyrs_j <- function( |
|
| 23 |
df, |
|
| 24 |
.var, |
|
| 25 |
id = "USUBJID", |
|
| 26 |
.alt_df_full, |
|
| 27 |
source = c("alt_df", "df"),
|
|
| 28 |
inriskdiffcol = FALSE |
|
| 29 |
) {
|
|
| 30 | 12x |
source <- match.arg(source) |
| 31 | ||
| 32 | 12x |
if (source == "alt_df") {
|
| 33 | 12x |
if (is.null(.alt_df_full)) {
|
| 34 | ! |
stop(paste( |
| 35 | ! |
"s_patyrs_j with source = alt_df requires a non-null .alt_df_full" |
| 36 |
)) |
|
| 37 |
} |
|
| 38 | 12x |
df <- .alt_df_full |
| 39 |
} |
|
| 40 | 12x |
df <- unique(df[, c(id, .var)]) |
| 41 | ||
| 42 | 12x |
x <- list() |
| 43 | ||
| 44 | 12x |
if (!inriskdiffcol) {
|
| 45 | 7x |
patyrs <- sum(df[[.var]]) |
| 46 |
} else {
|
|
| 47 | 5x |
patyrs <- list(x = NULL) |
| 48 |
} |
|
| 49 | 12x |
x[["patyrs"]] <- stats::setNames(patyrs, nm = "patyrs") |
| 50 | ||
| 51 | 12x |
return(x) |
| 52 |
} |
|
| 53 | ||
| 54 |
#' @describeIn a_patyrs_j Formatted analysis function for patient years summary which is used |
|
| 55 |
#' as `afun` in `analyze` or `cfun` in `summarize_row_groups`. |
|
| 56 |
#' |
|
| 57 |
#' |
|
| 58 |
#' @param df (`data.frame`)\cr data set containing all analysis variables. |
|
| 59 |
#' @param .var (`string`)\cr variable name containing the patient years data. |
|
| 60 |
#' @param .df_row (`data.frame`)\cr data frame across all of the columns for the given row split. |
|
| 61 |
#' @param id (`string`)\cr subject variable name. |
|
| 62 |
#' @param .alt_df_full (`dataframe`)\cr alternative dataset for calculations. |
|
| 63 |
#' @param .formats (named 'character' or 'list')\cr formats for the statistics. |
|
| 64 |
#' @param .labels (named 'character')\cr labels for the statistics. |
|
| 65 |
#' @param source (`string`)\cr source of data, either "alt_df" or "df". |
|
| 66 |
#' @param .spl_context (`data.frame`)\cr gives information about ancestor split states. |
|
| 67 |
#' @param .stats (`character`)\cr statistics to select for the table. |
|
| 68 |
#' |
|
| 69 |
#' @return |
|
| 70 |
#' * `a_patyrs_j` returns the corresponding list with formatted [rtables::CellValue()]. |
|
| 71 |
#' |
|
| 72 |
#' @export |
|
| 73 |
#' |
|
| 74 |
#' @examples |
|
| 75 |
#' library(tern) |
|
| 76 |
#' library(dplyr) |
|
| 77 |
#' trtvar <- "ARM" |
|
| 78 |
#' ctrl_grp <- "B: Placebo" |
|
| 79 |
#' cutoffd <- as.Date("2023-09-24")
|
|
| 80 |
#' |
|
| 81 |
#' |
|
| 82 |
#' adexsum <- ex_adsl |> |
|
| 83 |
#' create_colspan_var( |
|
| 84 |
#' non_active_grp = ctrl_grp, |
|
| 85 |
#' non_active_grp_span_lbl = " ", |
|
| 86 |
#' active_grp_span_lbl = "Active Study Agent", |
|
| 87 |
#' colspan_var = "colspan_trt", |
|
| 88 |
#' trt_var = trtvar |
|
| 89 |
#' ) |> |
|
| 90 |
#' mutate( |
|
| 91 |
#' rrisk_header = "Risk Difference (95% CI)", |
|
| 92 |
#' rrisk_label = paste(!!rlang::sym(trtvar), "vs", ctrl_grp), |
|
| 93 |
#' TRTDURY = case_when( |
|
| 94 |
#' !is.na(EOSDY) ~ EOSDY, |
|
| 95 |
#' TRUE ~ as.integer(cutoffd - as.Date(TRTSDTM) + 1) |
|
| 96 |
#' ) |
|
| 97 |
#' ) |> |
|
| 98 |
#' select(USUBJID, !!rlang::sym(trtvar), colspan_trt, rrisk_header, rrisk_label, TRTDURY) |
|
| 99 |
#' |
|
| 100 |
#' adae <- ex_adae |> |
|
| 101 |
#' group_by(USUBJID, AEDECOD) |> |
|
| 102 |
#' select(USUBJID, AEDECOD, ASTDY) |> |
|
| 103 |
#' mutate(rwnum = row_number()) |> |
|
| 104 |
#' mutate(AOCCPFL = case_when( |
|
| 105 |
#' rwnum == 1 ~ "Y", |
|
| 106 |
#' TRUE ~ NA |
|
| 107 |
#' )) |> |
|
| 108 |
#' filter(AOCCPFL == "Y") |
|
| 109 |
#' |
|
| 110 |
#' aefup <- left_join(adae, adexsum, by = "USUBJID") |
|
| 111 |
#' |
|
| 112 |
#' colspan_trt_map <- create_colspan_map(adexsum, |
|
| 113 |
#' non_active_grp = ctrl_grp, |
|
| 114 |
#' non_active_grp_span_lbl = " ", |
|
| 115 |
#' active_grp_span_lbl = "Active Study Agent", |
|
| 116 |
#' colspan_var = "colspan_trt", |
|
| 117 |
#' trt_var = trtvar |
|
| 118 |
#' ) |
|
| 119 |
#' |
|
| 120 |
#' ref_path <- c("colspan_trt", " ", trtvar, ctrl_grp)
|
|
| 121 |
#' |
|
| 122 |
#' ################################################################################ |
|
| 123 |
#' # Define layout and build table: |
|
| 124 |
#' ################################################################################ |
|
| 125 |
#' |
|
| 126 |
#' lyt <- basic_table(show_colcounts = TRUE, colcount_format = "N=xx", top_level_section_div = " ") |> |
|
| 127 |
#' split_cols_by("colspan_trt", split_fun = trim_levels_to_map(map = colspan_trt_map)) |>
|
|
| 128 |
#' split_cols_by(trtvar) |> |
|
| 129 |
#' split_cols_by("rrisk_header", nested = FALSE) |>
|
|
| 130 |
#' split_cols_by(trtvar, labels_var = "rrisk_label", split_fun = remove_split_levels(ctrl_grp)) |> |
|
| 131 |
#' analyze("TRTDURY",
|
|
| 132 |
#' nested = FALSE, |
|
| 133 |
#' show_labels = "hidden", |
|
| 134 |
#' afun = a_patyrs_j |
|
| 135 |
#' ) |
|
| 136 |
#' result <- build_table(lyt, aefup, alt_counts_df = adexsum) |
|
| 137 |
#' result |
|
| 138 |
#' |
|
| 139 |
a_patyrs_j <- function( |
|
| 140 |
df, |
|
| 141 |
.var, |
|
| 142 |
.df_row, |
|
| 143 |
id = "USUBJID", |
|
| 144 |
.alt_df_full = NULL, |
|
| 145 |
.formats = NULL, |
|
| 146 |
.labels = NULL, |
|
| 147 |
source = c("alt_df", "df"),
|
|
| 148 |
.spl_context, |
|
| 149 |
.stats = "patyrs" |
|
| 150 |
) {
|
|
| 151 | 12x |
source <- match.arg(source) |
| 152 | ||
| 153 | 12x |
if (length(.stats) > 1 || (length(.stats) == 1 && .stats != "patyrs")) {
|
| 154 | ! |
stop("a_patyrs_j: .stats must be 'patyrs'.")
|
| 155 |
} |
|
| 156 | ||
| 157 | 12x |
check_alt_df_full(source, "alt_df", .alt_df_full) |
| 158 | ||
| 159 | 12x |
cur_col_expr <- .spl_context$cur_col_expr[[1]] |
| 160 |
## colid can be used to figure out if we're in the relative risk columns or not |
|
| 161 | 12x |
colid <- .spl_context$cur_col_id[[1]] |
| 162 | 12x |
inriskdiffcol <- grepl("difference", tolower(colid), fixed = TRUE)
|
| 163 | ||
| 164 | 12x |
if (source == "alt_df") {
|
| 165 |
### derive appropriate alt_df based upon .spl_context and .alt_df_full |
|
| 166 |
### note that this is not yet within the current column |
|
| 167 | ||
| 168 | 12x |
alt_df <- h_create_altdf( |
| 169 | 12x |
.spl_context, |
| 170 | 12x |
.df_row, |
| 171 | 12x |
.alt_df_full, |
| 172 | 12x |
denom_by = NULL, |
| 173 | 12x |
id = id, |
| 174 | 12x |
variables = NULL, |
| 175 | 12x |
denom = "n_altdf" |
| 176 |
) |
|
| 177 |
## restrict to current column |
|
| 178 | 12x |
new_denomdf <- subset(alt_df, eval(cur_col_expr)) |
| 179 |
} else {
|
|
| 180 | ! |
new_denomdf <- df |
| 181 |
} |
|
| 182 | ||
| 183 | 12x |
x_stats <- s_patyrs_j( |
| 184 | 12x |
df, |
| 185 | 12x |
.alt_df_full = new_denomdf, |
| 186 | 12x |
.var = .var, |
| 187 | 12x |
id = id, |
| 188 | 12x |
source = source, |
| 189 | 12x |
inriskdiffcol = inriskdiffcol |
| 190 |
) |
|
| 191 | ||
| 192 | 12x |
x_stats <- x_stats[.stats] |
| 193 | ||
| 194 | 12x |
levels_per_stats <- lapply(x_stats, names) |
| 195 | ||
| 196 | 12x |
.formats <- junco_get_formats_from_stats(.stats, .formats, levels_per_stats) |
| 197 | 12x |
.labels <- junco_get_labels_from_stats(.stats, .labels, levels_per_stats) |
| 198 | 12x |
.labels <- .unlist_keep_nulls(.labels) |
| 199 | ||
| 200 | 12x |
x_stats <- x_stats[.stats] |
| 201 | ||
| 202 |
# Unlist stats + names |
|
| 203 | 12x |
x_stats <- .unlist_keep_nulls(x_stats) |
| 204 | 12x |
names(x_stats) <- names(.formats) |
| 205 | ||
| 206 |
### final step: turn requested stats into rtables rows |
|
| 207 | 12x |
inrows <- in_rows( |
| 208 | 12x |
.list = x_stats, |
| 209 | 12x |
.formats = .formats, |
| 210 | 12x |
.labels = .labels |
| 211 |
) |
|
| 212 | ||
| 213 | 12x |
return(inrows) |
| 214 |
} |
|
| 215 | ||
| 216 |
#' Exposure-Adjusted Incidence Rate |
|
| 217 |
#' @description |
|
| 218 |
#' Statistical/Analysis Function for presenting Exposure-Adjusted Incidence Rate summary data |
|
| 219 |
#' |
|
| 220 |
#' |
|
| 221 |
#' @name a_eair100_j |
|
| 222 |
NULL |
|
| 223 | ||
| 224 |
#' @describeIn a_eair100_j |
|
| 225 |
#' calculates exposure-adjusted incidence rates (EAIR) per `num_p_year` person-years for a |
|
| 226 |
#' specific level of a variable. |
|
| 227 |
#' |
|
| 228 |
#' |
|
| 229 |
#' @param levii (`string`)\cr the specific level of the variable to calculate EAIR for. |
|
| 230 |
#' @param df (`data.frame`)\cr data set containing all analysis variables. |
|
| 231 |
#' @param .df_row (`data.frame`)\cr data frame across all of the columns for the given row split. |
|
| 232 |
#' @param .var (`string`)\cr variable name that is passed by `rtables`. |
|
| 233 |
#' @param .alt_df_full (`dataframe`)\cr alternative dataset for calculations. |
|
| 234 |
#' @param id (`string`)\cr subject variable name. |
|
| 235 |
#' @param fup_var (`string`)\cr name of the variable containing the total follow-up duration |
|
| 236 |
#' for each subject, expressed in **years**. |
|
| 237 |
#' Used as the at-risk exposure time for subjects who did **not** experience the event. |
|
| 238 |
#' @param occ_var (`string` or `NULL`)\cr name of the flag variable identifying the |
|
| 239 |
#' **first** occurrence of the event within each subject and level, encoded as `"Y"`. |
|
| 240 |
#' Only records where `occ_var == "Y"` contribute to the event count (`n_event`), |
|
| 241 |
#' and only one event record per subject is allowed. |
|
| 242 |
#' \cr If `NULL`, event occurrence filtering is skipped and time at risk is taken |
|
| 243 |
#' from `fup_var` in `alt_counts_df` for all subjects. |
|
| 244 |
#' @param occ_dy (`string` or `NULL`)\cr When `occ_var` not `NULL`: name of the variable |
|
| 245 |
#' containing the relative day of the |
|
| 246 |
#' first event occurrence, expressed in **days** (e.g., `ASTDY`). |
|
| 247 |
#' For subjects with an event (`occ_var == "Y"`), this value is converted to years |
|
| 248 |
#' as `occ_dy / 365.25` and used as the at-risk exposure time instead of `fup_var`. |
|
| 249 |
#' Must be on the same time origin as `fup_var` (i.e., relative to start of exposure). |
|
| 250 |
#' @param num_p_year (`numeric`)\cr scaling factor for the incidence rate, representing |
|
| 251 |
#' the number of person-years used as the denominator base. |
|
| 252 |
#' The EAIR is calculated as `num_p_year * n_event / total_person_years`. |
|
| 253 |
#' Defaults to `100`, yielding a rate per 100 person-years. |
|
| 254 |
#' For example, use `1` for a rate per person-year or `1000` for a rate per |
|
| 255 |
#' 1000 person-years. |
|
| 256 |
#' @param count_multiple_events (`logical`)\cr if `TRUE`, counts the total number of events |
|
| 257 |
#' per subject rather than treating each subject as contributing 1 to `n_event`. |
|
| 258 |
#' Only considered when `occ_var = NULL`. |
|
| 259 |
#' Defaults to `FALSE`. |
|
| 260 |
#' |
|
| 261 |
#' @param conf_level (`proportion`)\cr confidence level of the interval. |
|
| 262 |
#' @param conf_type_diff (`string`)\cr method used to compute the confidence interval for the EAIR difference. |
|
| 263 |
#' One of `"wald"` (Wald without continuity correction), `"waldcc"` (Wald with continuity correction), |
|
| 264 |
#' `"mn"` (Miettinen-Nurminen score), or `"scas"` (skewness-corrected asymptotic score). |
|
| 265 |
#' Defaults to `"wald"`. |
|
| 266 |
#' @param conf_type_eair (`string`)\cr method used to compute the confidence interval for the EAIR rate. |
|
| 267 |
#' One of `"exact"`, `"normal"`, `"normal_log"`, `"byar"` |
|
| 268 |
#' \cr See `?tern:::h_incidence_rate` and `?tern::incidence_rate`, argument `control` for more information. |
|
| 269 |
#' Defaults to `"exact"`. |
|
| 270 |
#' @param vs_ref_group (`logical`)\cr if TRUE, risk difference (against reference group) calculations will be performed. |
|
| 271 |
#' @param cur_col_expr (`expression`)\cr current column expression taken from `rtables` `.spl_context` |
|
| 272 |
#' splitting machinery (`cur_col_expr`). |
|
| 273 |
#' @param ref_col_expr (`expression` or `NULL`)\cr column expression for the reference group. |
|
| 274 |
#' Required to be an expression if `vs_ref_group` is `TRUE` |
|
| 275 |
#' @param inriskdiffcol (`logical`)\cr flag indicating if the function is called within a risk difference column. |
|
| 276 |
#' |
|
| 277 |
#' @return |
|
| 278 |
#' * `s_eair100_levii_j()` returns a list containing the following statistics: |
|
| 279 |
#' \itemize{
|
|
| 280 |
#' \item n_event: Number of subjects with the event, or total event count if `count_multiple_events = TRUE` |
|
| 281 |
#' \item person_years: Total person-years of follow-up |
|
| 282 |
#' \item eair: Exposure-adjusted incidence rate per `num_p_year` person-years |
|
| 283 |
#' \item n_eair: Combination of `n_event` and `eair` statistics. |
|
| 284 |
#' \item eair_n: Combination of `eair` and `n_event` statistics. |
|
| 285 |
#' \item eair_n_py: Combination of `eair`, `n_event` and `person_years` statistics. |
|
| 286 |
#' \item eair_ci: Confidence interval for `eair`, method specified in `conf_type_eair`. |
|
| 287 |
#' \item eair_est_ci: Combination of `eair` and `eair_ci`. |
|
| 288 |
#' \item eair_diff_est: Difference in EAIR between current group and reference group |
|
| 289 |
#' (if `diff`=TRUE and `inriskdiffcol`=TRUE) |
|
| 290 |
#' \item eair_diff_ci: Confidence interval for difference in EAIR between current group and reference group |
|
| 291 |
#' (if `diff`=TRUE and `inriskdiffcol`=TRUE) |
|
| 292 |
#' \item eair_diff_est_ci: Combination of `eair_diff_est` and `eair_diff_ci`. |
|
| 293 |
#' } |
|
| 294 |
#' The list of available statistics (core columns) can also be viewed by |
|
| 295 |
#' running `junco_get_stats("a_eair100_j")`.
|
|
| 296 |
#' @details |
|
| 297 |
#' The exposure-adjusted incidence rate (EAIR) is defined as: |
|
| 298 |
#' - When (`occ_var` not `NULL`) or `count_multiple_events = FALSE`: |
|
| 299 |
#' \cr the **number of subjects** |
|
| 300 |
#' with at least one occurrence of a specified adverse event divided by the total |
|
| 301 |
#' at-risk exposure time across all subjects. |
|
| 302 |
#' |
|
| 303 |
#' - When `occ_var = NULL` and `count_multiple_events = TRUE`: |
|
| 304 |
#' \cr the total **number of events** across of a specified adverse event divided by the total |
|
| 305 |
#' at-risk exposure time across all subjects. |
|
| 306 |
#' |
|
| 307 |
#' Total at-risk exposure time is calculated for each subject |
|
| 308 |
#' as: |
|
| 309 |
#' - When `occ_var` not `NULL` |
|
| 310 |
#' \itemize{
|
|
| 311 |
#' \item for subjects with the event: relative day (from the start of exposure) of |
|
| 312 |
#' the onset date of the **first** occurrence of the specified event (`occ_var` and `occ_dy`), |
|
| 313 |
#' transformed from days into years as `occ_dy/365.25`, or |
|
| 314 |
#' \item for subjects without the event: total duration of follow-up (expressed in years) |
|
| 315 |
#' (e.g., treatment discontinuation, completion, or censoring) (`fup_var`). |
|
| 316 |
#' } |
|
| 317 |
#' - When `occ_var = NULL`\cr |
|
| 318 |
#' for all subjects: total duration of follow-up (expressed in years) |
|
| 319 |
#' (e.g., treatment discontinuation, completion, or censoring) (`fup_var`). |
|
| 320 |
#' |
|
| 321 |
#' The EAIR is expressed as: |
|
| 322 |
#' |
|
| 323 |
#' \deqn{
|
|
| 324 |
#' \mathrm{EAIR} = \hat r = \frac{n}{T}
|
|
| 325 |
#' } |
|
| 326 |
#' |
|
| 327 |
#' where: |
|
| 328 |
#' \itemize{
|
|
| 329 |
#' \item \eqn{n} is the number of subjects with at least one occurrence of the event
|
|
| 330 |
#' \item T = \eqn{ \sum_{i=1}^{N} t_i}, with
|
|
| 331 |
#' \item \eqn{t_i} is the at-risk exposure time for subject \eqn{i},
|
|
| 332 |
#' expressed in subject-years |
|
| 333 |
#' } |
|
| 334 |
#' |
|
| 335 |
#' The difference between two EAIRs is |
|
| 336 |
#' \eqn{\hat \theta = (\hat r_1 - \hat r_2)}.
|
|
| 337 |
#' |
|
| 338 |
#' Wald's method for the confidence interval approximation is used (Liu et al., 2006), which uses the standard error |
|
| 339 |
#' \deqn{
|
|
| 340 |
#' \mathrm{se}(\hat r_1 - \hat r_2) =
|
|
| 341 |
#' \sqrt{\frac{n_1}{T_1^2} + \frac{n_2}{T_2^2}}
|
|
| 342 |
#' }{
|
|
| 343 |
#' se = sqrt(n1/T1^2 + n2/T2^2) |
|
| 344 |
#' } |
|
| 345 |
#' and the \eqn{100(1-\alpha)\%} confidence interval is
|
|
| 346 |
#' \deqn{
|
|
| 347 |
#' (\hat r_1 - \hat r_2) \pm z_{1-\alpha/2}\,\mathrm{se}(\hat r_1 - \hat r_2)
|
|
| 348 |
#' }{
|
|
| 349 |
#' (r1 - r2) +/- z_(1-a/2) * SE |
|
| 350 |
#' } |
|
| 351 |
#' where \eqn{z_{1-\alpha/2}} is the standard normal quantile. [1](https://metricgate.com/docs/poisson-rate-comparison/)
|
|
| 352 |
#' \cr |
|
| 353 |
#' \cr Other methods for confidence interval for difference can be specified in `conf_type_diff` argument. |
|
| 354 |
#' |
|
| 355 |
#' EAIR is commonly reported per 100 subject-years: |
|
| 356 |
#' |
|
| 357 |
#' \deqn{
|
|
| 358 |
#' \mathrm{EAIR}_{100} = 100 \times \hat r
|
|
| 359 |
#' } |
|
| 360 |
#' @references |
|
| 361 |
#' Liu, G. F., Wang, J., Liu, K., and Snavely, D. B. (2006). |
|
| 362 |
#' Confidence intervals for an exposure adjusted incidence rate difference |
|
| 363 |
#' with applications to clinical trials. |
|
| 364 |
#' \emph{Statistics in Medicine}, \strong{25}(8), 1275--1286.
|
|
| 365 |
#' \doi{10.1002/sim.2335}
|
|
| 366 |
#' @keywords internal |
|
| 367 |
s_eair100_levii_j <- function( |
|
| 368 |
levii, |
|
| 369 |
df, |
|
| 370 |
.df_row, |
|
| 371 |
.var, |
|
| 372 |
.alt_df_full = NULL, |
|
| 373 |
id = "USUBJID", |
|
| 374 |
fup_var, |
|
| 375 |
occ_var, |
|
| 376 |
occ_dy, |
|
| 377 |
num_p_year = 100, |
|
| 378 |
count_multiple_events = FALSE, |
|
| 379 |
conf_level = 0.95, |
|
| 380 |
conf_type_diff = c("wald", "waldcc", "mn", "scas"),
|
|
| 381 |
conf_type_eair = c("exact", "normal", "normal_log", "byar"),
|
|
| 382 |
vs_ref_group = FALSE, |
|
| 383 |
.in_ref_col, |
|
| 384 |
cur_col_expr, |
|
| 385 |
ref_col_expr, |
|
| 386 |
inriskdiffcol = FALSE |
|
| 387 |
) {
|
|
| 388 | 346x |
conf_type_diff <- match.arg(conf_type_diff) |
| 389 | 346x |
method_part <- switch(conf_type_diff, |
| 390 | 346x |
"wald" = "Wald, without correction", |
| 391 | 346x |
"waldcc" = "Wald, with correction", |
| 392 | 346x |
"mn" = "Miettinen and Nurminen", |
| 393 | 346x |
"scas" = "skewness-corrected asymptotic score", |
| 394 | 346x |
stop(paste(conf_type_diff, "does not have a description")) |
| 395 |
) |
|
| 396 | 346x |
method_part <- paste0("(", method_part, ")")
|
| 397 | 346x |
conf_type_eair <- match.arg(conf_type_eair) |
| 398 | 346x |
method_eair_part <- switch(conf_type_eair, |
| 399 | 346x |
"exact" = "Exact", |
| 400 | 346x |
"byar" = "byar", |
| 401 | 346x |
"normal" = "normal", |
| 402 | 346x |
"normal_log" = "log-normal", |
| 403 | 346x |
stop(paste(conf_type_eair, "does not have a description")) |
| 404 |
) |
|
| 405 | 346x |
method_eair_part <- paste0(" (", method_eair_part, ")")
|
| 406 | ||
| 407 | 346x |
if (vs_ref_group && (!is.expression(cur_col_expr))) {
|
| 408 | ! |
stop("cur_col_expr must be an expression when vs_ref_group = TRUE")
|
| 409 |
} |
|
| 410 |
# subset .alt_df_full to current column |
|
| 411 | 346x |
.alt_df_full_cur_group <- subset(.alt_df_full, eval(cur_col_expr)) |
| 412 | 346x |
cur_dfs <- h_get_eair_df( |
| 413 | 346x |
levii, |
| 414 | 346x |
df, |
| 415 | 346x |
denom_df = .alt_df_full_cur_group, |
| 416 | 346x |
.var = .var, |
| 417 | 346x |
id = id, |
| 418 | 346x |
fup_var = fup_var, |
| 419 | 346x |
occ_var = occ_var, |
| 420 | 346x |
occ_dy = occ_dy, |
| 421 | 346x |
count_multiple_events = count_multiple_events |
| 422 |
) |
|
| 423 | 345x |
cur_df_num <- cur_dfs$df_num |
| 424 | 345x |
cur_df_denom <- cur_dfs$df_denom |
| 425 |
### statistics derivation |
|
| 426 |
# note: variables n_events and mod_fup_var are derived by h_get_eair_df |
|
| 427 |
# when count_multiple_events == TRUE, n_events: number of events per subject |
|
| 428 |
# when count_multiple_events == FALSE, n_events: 1 for subjects with event |
|
| 429 | 345x |
x1 <- sum(cur_df_num[["n_events"]]) |
| 430 | 345x |
n1 <- sum(cur_df_denom[["mod_fup_var"]]) |
| 431 | 345x |
cur_eair <- num_p_year * x1 / n1 |
| 432 | 345x |
cur_n_eair <- c(x1, cur_eair) |
| 433 | ||
| 434 | 345x |
x <- list() |
| 435 | 345x |
eair_lbl <- paste0("eair (per ", num_p_year, " person years)")
|
| 436 | 345x |
n_event_lbl <- |
| 437 | 345x |
if (is.null(occ_var) && count_multiple_events) {
|
| 438 | 10x |
"Number of events" |
| 439 |
} else {
|
|
| 440 | 335x |
"Number of subjects with event" |
| 441 |
} |
|
| 442 | ||
| 443 | 345x |
x$n_event <- c("n_event" = x1)
|
| 444 | 345x |
x$person_years <- c("person_years" = n1)
|
| 445 | 345x |
x$eair <- stats::setNames(c("eair" = cur_eair), "eair")
|
| 446 | 345x |
x$n_eair <- stats::setNames(c("n_eair" = cur_n_eair), c("n_event", "eair"))
|
| 447 | 345x |
x$eair_n <- stats::setNames(c("eair_n" = x$n_eair[c(2, 1)]), c("eair", "n_event"))
|
| 448 | 345x |
x$eair_n_py <- stats::setNames(c("eair_n_py" = c(x$eair_n, n1)), c("eair", "n_event", "py"))
|
| 449 | ||
| 450 | 345x |
attr(x$n_event, "label") <- n_event_lbl |
| 451 | 345x |
attr(x$eair, "label") <- eair_lbl |
| 452 | 345x |
attr(x$n_eair, "label") <- paste0(n_event_lbl, " (", eair_lbl, ")")
|
| 453 | 345x |
attr(x$eair_n, "label") <- paste0(eair_lbl, " (", n_event_lbl, ")")
|
| 454 | 345x |
attr(x$eair_n_py, "label") <- paste0(eair_lbl, " (", n_event_lbl, "/", "person-years", ")")
|
| 455 | ||
| 456 |
# ci for rates - utilize tern function to derive rate_ci |
|
| 457 | 345x |
tern_incid_rate <- h_incidence_rate( |
| 458 | 345x |
person_years = n1, |
| 459 | 345x |
n_events = x1, |
| 460 | 345x |
control = list( |
| 461 | 345x |
num_pt_year = num_p_year, |
| 462 | 345x |
input_time_unit = "year", |
| 463 | 345x |
conf_level = conf_level, |
| 464 | 345x |
conf_type = conf_type_eair |
| 465 |
) |
|
| 466 |
) |
|
| 467 | ||
| 468 | 345x |
x$eair_ci <- stats::setNames(c("eair_ci" = tern_incid_rate$rate_ci), "eair_ci")
|
| 469 | 345x |
attr(x$eair_ci, "label") <- paste0(f_conf_level(conf_level), method_eair_part) |
| 470 | ||
| 471 | 345x |
x$eair_est_ci <- stats::setNames(c("eair_est_ci" = c(x$eair, tern_incid_rate$rate_ci)), "eair_est_ci")
|
| 472 | 345x |
attr(x$eair_est_ci, "label") <- paste0(eair_lbl, " with (", f_conf_level(conf_level), method_eair_part, ")")
|
| 473 | ||
| 474 | ||
| 475 | 345x |
if (vs_ref_group) {
|
| 476 |
# perform vs ref group derivations |
|
| 477 | 136x |
if (inriskdiffcol) {
|
| 478 |
# stats that will be blanked out in risk diff columns (only applicable to horizontal layout) |
|
| 479 | 136x |
x$n_event <- c("n_event" = NULL)
|
| 480 | 136x |
x$person_years <- c("person_years" = NULL)
|
| 481 | 136x |
x$eair_ci <- c("eair_ci" = NULL)
|
| 482 |
} |
|
| 483 | ||
| 484 |
# retrieve ref_group info from ref_col_expr for both .alt_df_full and .df_row |
|
| 485 | 136x |
alt_df_full_ref_group <- subset(.alt_df_full, eval(ref_col_expr)) |
| 486 | 136x |
ref_group <- subset(.df_row, eval(ref_col_expr)) |
| 487 | ||
| 488 | 136x |
ref_dfs <- h_get_eair_df( |
| 489 | 136x |
levii, |
| 490 | 136x |
df = ref_group, |
| 491 | 136x |
denom_df = alt_df_full_ref_group, |
| 492 | 136x |
.var = .var, |
| 493 | 136x |
id = id, |
| 494 | 136x |
fup_var = fup_var, |
| 495 | 136x |
occ_var = occ_var, |
| 496 | 136x |
occ_dy = occ_dy, |
| 497 | 136x |
count_multiple_events = count_multiple_events |
| 498 |
) |
|
| 499 | ||
| 500 | 136x |
ref_df_num <- ref_dfs$df_num |
| 501 | 136x |
ref_df_denom <- ref_dfs$df_denom |
| 502 | ||
| 503 |
### statistics derivation |
|
| 504 | 136x |
x2 <- sum(ref_df_num[["n_events"]]) |
| 505 | 136x |
n2 <- sum(ref_df_denom[["mod_fup_var"]]) |
| 506 | ||
| 507 | 136x |
ref_eair <- num_p_year * x2 / n2 |
| 508 | 136x |
ref_n_eair <- c(x2, ref_eair) |
| 509 | ||
| 510 | 136x |
rdiff <- cur_eair - ref_eair |
| 511 | 136x |
eair_diff_est_stats <- h_s_eair_diff(x1, n1, |
| 512 | 136x |
x2, n2, |
| 513 | 136x |
conf_type = conf_type_diff, |
| 514 | 136x |
conf_level = conf_level |
| 515 |
) |
|
| 516 |
# combine est of difference with its ci |
|
| 517 | 136x |
eair_diff_est_ci <- num_p_year * |
| 518 | 136x |
c(eair_diff_est_stats$diff_est, eair_diff_est_stats$diff_lcl, eair_diff_est_stats$diff_ucl) |
| 519 | ||
| 520 | 136x |
x$eair_diff_est_ci <- stats::setNames( |
| 521 | 136x |
c("eair_diff_est_ci" = eair_diff_est_ci),
|
| 522 | 136x |
nm = c("estimate", "lcl", "ucl")
|
| 523 |
) |
|
| 524 | 136x |
attr(x$eair_diff_est_ci, "label") <- |
| 525 | 136x |
paste0("Difference in eair with (", f_conf_level(conf_level), ") ", method_part)
|
| 526 | ||
| 527 | 136x |
x$eair_diff_ci <- x$eair_diff_est_ci[2:3] |
| 528 | 136x |
attr(x$eair_diff_ci, "label") <- |
| 529 | 136x |
paste0(f_conf_level(conf_level), " for difference in eair ", method_part) |
| 530 | ||
| 531 | 136x |
x$eair_diff_est <- x$eair_diff_est_ci[1] |
| 532 | 136x |
attr(x$eair_diff_est, "label") <- "Difference in eair" |
| 533 |
} else {
|
|
| 534 |
# no risk diff calculations - set stats to NULL, ie blank |
|
| 535 | 209x |
x$eair_diff_est_ci <- c("eair_diff_est_ci" = NULL)
|
| 536 | 209x |
x$eair_diff_est <- c("eair_diff_est" = NULL)
|
| 537 | 209x |
x$eair_diff_ci <- c("eair_diff_ci" = NULL)
|
| 538 |
} |
|
| 539 | ||
| 540 | 345x |
return(x) |
| 541 |
} |
|
| 542 | ||
| 543 |
#' @describeIn a_eair100_j |
|
| 544 |
#' Formatted analysis function for exposure adjusted incidence rate summary which is |
|
| 545 |
#' used as `afun` in `analyze` or `cfun` in `summarize_row_groups`.\cr |
|
| 546 |
#' |
|
| 547 |
#' @param labelstr (`string`)\cr Not intended to be set by user, only present to be able to use function as a `cfun`. |
|
| 548 |
#' @param .spl_context (`data.frame`)\cr gives information about ancestor split states. |
|
| 549 |
#' @param drop_levels (`logical`)\cr if TRUE, non-observed levels will not be included. |
|
| 550 |
#' @param riskdiff (`logical`)\cr if TRUE, risk difference calculations will be performed. |
|
| 551 |
#' @param ref_path (`string`)\cr column path specifications for the control group. |
|
| 552 |
#' @param .stats (`character`)\cr statistics to select for the table.\cr |
|
| 553 |
#' @param .formats (named 'character' or 'list')\cr formats for the statistics. |
|
| 554 |
#' @param .labels (named 'character')\cr labels for the statistics. |
|
| 555 |
#' @param .indent_mods (named `integer`)\cr indent modifiers for the labels. |
|
| 556 |
#' @param na_str (`string`)\cr string used to replace all NA or empty values in the output. |
|
| 557 |
#' @param row_labels_adj (`logical`)\cr If `TRUE` and number of requested statistics = 1, |
|
| 558 |
#' the row label on the table will reflect the level of the incoming variable rather than |
|
| 559 |
#' the label for the statistic. |
|
| 560 |
#' \cr Set to default `TRUE` for backward-compatibility reasons. |
|
| 561 |
#' @param riskdiff_setup (`string`)\cr Are risk differences presented in horizontal way, or vertical way. |
|
| 562 |
#' \cr When horizontal, the statistics for `eair`, `n_eair`, `eair_n` will be replaced by |
|
| 563 |
#' `ear_diff_est_ci` in the risk difference columns. |
|
| 564 |
#' If `.stats` include any of difference in eair rates statistics, |
|
| 565 |
#' these will be removed as `ear_diff_est_ci` will be displayed in the risk difference columns |
|
| 566 |
#' |
|
| 567 |
#' @return |
|
| 568 |
#' * `a_eair100_j` returns the corresponding list with formatted [rtables::CellValue()].\cr |
|
| 569 |
#' Within a risk difference column, the statistics `eair` and `n_eair` are replaced by `eair_diff_est_ci` |
|
| 570 |
#' (difference in EAIR between current group and reference group, together with it's confidence interval). |
|
| 571 | ||
| 572 |
#' @export |
|
| 573 |
#' |
|
| 574 |
#' @examples |
|
| 575 |
#' library(tern) |
|
| 576 |
#' library(dplyr) |
|
| 577 |
#' trtvar <- "ARM" |
|
| 578 |
#' ctrl_grp <- "B: Placebo" |
|
| 579 |
#' cutoffd <- as.Date("2023-09-24")
|
|
| 580 |
#' |
|
| 581 |
#' |
|
| 582 |
#' adexsum <- ex_adsl |> |
|
| 583 |
#' create_colspan_var( |
|
| 584 |
#' non_active_grp = ctrl_grp, |
|
| 585 |
#' non_active_grp_span_lbl = " ", |
|
| 586 |
#' active_grp_span_lbl = "Active Study Agent", |
|
| 587 |
#' colspan_var = "colspan_trt", |
|
| 588 |
#' trt_var = trtvar |
|
| 589 |
#' ) |> |
|
| 590 |
#' mutate( |
|
| 591 |
#' rrisk_header = "Risk Difference (95% CI)", |
|
| 592 |
#' rrisk_label = paste(!!rlang::sym(trtvar), "vs", ctrl_grp), |
|
| 593 |
#' TRTDURY = case_when( |
|
| 594 |
#' !is.na(EOSDY) ~ EOSDY, |
|
| 595 |
#' TRUE ~ as.integer(cutoffd - as.Date(TRTSDTM) + 1) |
|
| 596 |
#' ) |
|
| 597 |
#' ) |> |
|
| 598 |
#' select(USUBJID, !!rlang::sym(trtvar), colspan_trt, rrisk_header, rrisk_label, TRTDURY) |
|
| 599 |
#' |
|
| 600 |
#' adexsum$TRTDURY <- as.numeric(adexsum$TRTDURY) |
|
| 601 |
#' |
|
| 602 |
#' adae <- ex_adae |> |
|
| 603 |
#' group_by(USUBJID, AEDECOD) |> |
|
| 604 |
#' select(USUBJID, AEDECOD, ASTDY) |> |
|
| 605 |
#' mutate(rwnum = row_number()) |> |
|
| 606 |
#' mutate(AOCCPFL = case_when( |
|
| 607 |
#' rwnum == 1 ~ "Y", |
|
| 608 |
#' TRUE ~ NA |
|
| 609 |
#' )) |> |
|
| 610 |
#' filter(AOCCPFL == "Y") |
|
| 611 |
#' |
|
| 612 |
#' aefup <- left_join(adae, adexsum, by = "USUBJID") |
|
| 613 |
#' |
|
| 614 |
#' colspan_trt_map <- create_colspan_map(adexsum, |
|
| 615 |
#' non_active_grp = ctrl_grp, |
|
| 616 |
#' non_active_grp_span_lbl = " ", |
|
| 617 |
#' active_grp_span_lbl = "Active Study Agent", |
|
| 618 |
#' colspan_var = "colspan_trt", |
|
| 619 |
#' trt_var = trtvar |
|
| 620 |
#' ) |
|
| 621 |
#' |
|
| 622 |
#' ref_path <- c("colspan_trt", " ", trtvar, ctrl_grp)
|
|
| 623 |
#' |
|
| 624 |
#' ################################################################################ |
|
| 625 |
#' # Define layout and build table: |
|
| 626 |
#' ################################################################################ |
|
| 627 |
#' |
|
| 628 |
#' lyt <- basic_table(show_colcounts = TRUE, colcount_format = "N=xx", top_level_section_div = " ") |> |
|
| 629 |
#' split_cols_by("colspan_trt", split_fun = trim_levels_to_map(map = colspan_trt_map)) |>
|
|
| 630 |
#' split_cols_by(trtvar) |> |
|
| 631 |
#' split_cols_by("rrisk_header", nested = FALSE) |>
|
|
| 632 |
#' split_cols_by(trtvar, labels_var = "rrisk_label", split_fun = remove_split_levels(ctrl_grp)) |> |
|
| 633 |
#' analyze("TRTDURY",
|
|
| 634 |
#' show_labels = "hidden", |
|
| 635 |
#' afun = a_patyrs_j |
|
| 636 |
#' ) |> |
|
| 637 |
#' analyze( |
|
| 638 |
#' vars = "AEDECOD", |
|
| 639 |
#' afun = a_eair100_j, |
|
| 640 |
#' extra_args = list( |
|
| 641 |
#' fup_var = "TRTDURY", |
|
| 642 |
#' occ_var = "AOCCPFL", |
|
| 643 |
#' occ_dy = "ASTDY", |
|
| 644 |
#' ref_path = ref_path, |
|
| 645 |
#' drop_levels = TRUE |
|
| 646 |
#' ) |
|
| 647 |
#' ) |
|
| 648 |
#' |
|
| 649 |
#' result <- build_table(lyt, aefup, alt_counts_df = adexsum) |
|
| 650 |
#' head(result, 5) |
|
| 651 |
#' |
|
| 652 |
a_eair100_j <- function( |
|
| 653 |
df, |
|
| 654 |
labelstr = NULL, |
|
| 655 |
.var, |
|
| 656 |
.df_row, |
|
| 657 |
.spl_context, |
|
| 658 |
.alt_df_full = NULL, |
|
| 659 |
id = "USUBJID", |
|
| 660 |
fup_var, |
|
| 661 |
occ_var, |
|
| 662 |
occ_dy = NULL, |
|
| 663 |
num_p_year = 100, |
|
| 664 |
count_multiple_events = FALSE, |
|
| 665 |
drop_levels = FALSE, |
|
| 666 |
riskdiff = TRUE, |
|
| 667 |
ref_path = NULL, |
|
| 668 |
.stats = c("eair"),
|
|
| 669 |
.formats = NULL, |
|
| 670 |
.labels = NULL, |
|
| 671 |
.indent_mods = NULL, |
|
| 672 |
na_str = rep("NA", 3),
|
|
| 673 |
# treatment/ref group related arguments |
|
| 674 |
conf_level = 0.95, |
|
| 675 |
conf_type_diff = c("wald", "waldcc", "mn", "scas"),
|
|
| 676 |
conf_type_eair = c("normal", "normal_log", "exact", "byar"),
|
|
| 677 |
row_labels_adj = FALSE, |
|
| 678 |
riskdiff_setup = c("horizontal", "vertical")
|
|
| 679 |
) {
|
|
| 680 | 85x |
conf_type_diff <- match.arg(conf_type_diff) |
| 681 | 85x |
conf_type_eair <- match.arg(conf_type_eair) |
| 682 | 85x |
riskdiff_setup <- match.arg(riskdiff_setup) |
| 683 | ||
| 684 | 85x |
if (riskdiff && is.null(ref_path)) {
|
| 685 | ! |
stop("ref_path cannot be NULL when riskdiff = TRUE, please specify it. See ?get_ref_info for details.")
|
| 686 |
} |
|
| 687 | ||
| 688 | 85x |
ref_info <- get_ref_info_expanded( |
| 689 | 85x |
df, |
| 690 | 85x |
.var, |
| 691 | 85x |
.df_row, |
| 692 | 85x |
.spl_context, |
| 693 | 85x |
ref_path, |
| 694 | 85x |
riskdiff, |
| 695 | 85x |
riskdiff_setup |
| 696 |
) |
|
| 697 | ||
| 698 | 85x |
perform_vs_ref_stats <- ref_info$perform_vs_ref_stats |
| 699 | 85x |
ref_col_expr <- ref_info$ref_col_expr |
| 700 | 85x |
.in_ref_col <- ref_info$.in_ref_col |
| 701 | ||
| 702 |
## prepare for column based split |
|
| 703 | 85x |
cur_col_expr <- .spl_context$cur_col_expr[[1]] |
| 704 |
## colid can be used to figure out if we're in the relative risk columns or not |
|
| 705 | 85x |
colid <- .spl_context$cur_col_id[[1]] |
| 706 | 85x |
inriskdiffcol <- grepl("difference", tolower(colid), fixed = TRUE)
|
| 707 | ||
| 708 |
# if no stats requested, get all stats |
|
| 709 | 85x |
.stats <- junco_get_stats( |
| 710 | 85x |
"a_eair100_j", |
| 711 | 85x |
stats_in = .stats, |
| 712 | 85x |
custom_stats_in = NULL |
| 713 |
) |
|
| 714 | 85x |
if (any(.stats %in% c("eair_diff_est_ci", "eair_diff_est", "eair_diff_ci")) && riskdiff_setup == "horizontal") {
|
| 715 | ! |
.stats <- .stats[!(.stats %in% c("eair_diff_est_ci", "eair_diff_est", "eair_diff_ci"))]
|
| 716 |
} |
|
| 717 | ||
| 718 | 85x |
checkmate::assert_string(fup_var, null.ok = FALSE) |
| 719 | 85x |
checkmate::assert_string(occ_var, null.ok = TRUE) |
| 720 | 85x |
if (!is.null(occ_var)) {
|
| 721 | 70x |
checkmate::assert_string(occ_dy, null.ok = FALSE) |
| 722 |
} |
|
| 723 | ||
| 724 | 85x |
checkmate::assert_names(colnames(df), must.include = c(occ_var, occ_dy)) |
| 725 | ||
| 726 | 84x |
if (is.null(.alt_df_full)) {
|
| 727 | ! |
stop(paste("a_eair100_j: .alt_df_full cannot be NULL. Specify `alt_counts_df` to `build_table`"))
|
| 728 |
} |
|
| 729 | 84x |
checkmate::assert_names(colnames(.alt_df_full), must.include = c(fup_var)) |
| 730 | ||
| 731 | 84x |
inrowsplit <- FALSE |
| 732 | 84x |
if (.var == tail(.spl_context$split, 1L)) {
|
| 733 |
# note: this will catch the rowsplit only if parent_name in split_rows_by call is .var (default) |
|
| 734 | 45x |
drop_levels <- TRUE |
| 735 | 45x |
inrowsplit <- TRUE |
| 736 |
} |
|
| 737 | ||
| 738 |
### combine all preprocessing of incoming df/.df_row in one function |
|
| 739 |
### do this outside stats derivation functions (s_freq_j/) |
|
| 740 |
### use all of val/excl_levels/drop_levels//new_levels/label/label_map/labelstr/label_fstr |
|
| 741 | 84x |
upd_dfrow <- h_upd_dfrow( |
| 742 | 84x |
.df_row, |
| 743 | 84x |
.var, |
| 744 | 84x |
val = NULL, |
| 745 | 84x |
excl_levels = NULL, |
| 746 | 84x |
drop_levels = drop_levels, |
| 747 | 84x |
new_levels = NULL, |
| 748 | 84x |
new_levels_after = FALSE, |
| 749 | 84x |
label = NULL, |
| 750 | 84x |
label_map = NULL, |
| 751 | 84x |
labelstr = labelstr, |
| 752 | 84x |
label_fstr = NULL, |
| 753 | 84x |
.spl_context = .spl_context |
| 754 |
) |
|
| 755 | ||
| 756 | 84x |
.df_row <- upd_dfrow$df_row |
| 757 | 84x |
df <- upd_dfrow$df |
| 758 | ||
| 759 |
### derive appropriate alt_df based upon .spl_context and .alt_df_full |
|
| 760 |
### note that only row-based splits are done |
|
| 761 |
### for now only for variables from the first split_rows_by |
|
| 762 | 84x |
alt_df <- h_create_altdf( |
| 763 | 84x |
.spl_context, |
| 764 | 84x |
.df_row, |
| 765 | 84x |
.alt_df_full, |
| 766 | 84x |
denom_by = NULL, |
| 767 | 84x |
id = id, |
| 768 | 84x |
variables = NULL, |
| 769 | 84x |
denom = "n_altdf" |
| 770 |
) |
|
| 771 | 84x |
new_denomdf <- alt_df |
| 772 | ||
| 773 |
# gather arguments to pass onto s_function |
|
| 774 | 84x |
fn_Args <- list( |
| 775 | 84x |
df = df, |
| 776 | 84x |
.df_row = .df_row, |
| 777 | 84x |
.var = .var, |
| 778 | 84x |
id = id, |
| 779 | 84x |
fup_var = fup_var, |
| 780 | 84x |
occ_var = occ_var, |
| 781 | 84x |
occ_dy = occ_dy, |
| 782 | 84x |
num_p_year = num_p_year, |
| 783 | 84x |
count_multiple_events = count_multiple_events, |
| 784 | 84x |
cur_col_expr = cur_col_expr, |
| 785 | 84x |
ref_col_expr = ref_col_expr, |
| 786 | 84x |
vs_ref_group = perform_vs_ref_stats, |
| 787 | 84x |
.in_ref_col = !perform_vs_ref_stats, |
| 788 | 84x |
inriskdiffcol = riskdiff_setup == "horizontal" && inriskdiffcol, |
| 789 | 84x |
conf_level = conf_level, |
| 790 | 84x |
conf_type_eair = conf_type_eair, |
| 791 | 84x |
.alt_df_full = .alt_df_full, |
| 792 | 84x |
conf_type_diff = conf_type_diff |
| 793 |
) |
|
| 794 | ||
| 795 | 84x |
levs <- levels(.df_row[[.var]]) |
| 796 | 84x |
y <- mapply( |
| 797 | 84x |
s_eair100_levii_j, |
| 798 | 84x |
levii = levs, |
| 799 | 84x |
MoreArgs = fn_Args, |
| 800 | 84x |
SIMPLIFY = FALSE |
| 801 |
) |
|
| 802 | ||
| 803 |
### rearrange list y to list to x_stats |
|
| 804 |
# y is a nested list with |
|
| 805 |
# outer elements levels of incoming variable |
|
| 806 |
# inner elements stat elements statistics from |
|
| 807 | ||
| 808 |
#### this is to ensure the remainder of the code can stay the same as in a_freq_j |
|
| 809 | 83x |
stnms <- junco_get_stats("a_eair100_j")
|
| 810 | 83x |
x_stats <- extract_x_stats(y, stnms) |
| 811 | ||
| 812 |
# x_stats is a nested list with |
|
| 813 |
# outer elements stat elements statistics (all 5 from stnms) |
|
| 814 |
# inner elements levels of incoming variable |
|
| 815 |
# this will ensure we can perform subsetting on .stats |
|
| 816 |
# like x_stats[.stats] |
|
| 817 | ||
| 818 |
# transpose_named_list cannot be used as this will not ensure all 5 poss stats are in |
|
| 819 | 83x |
if (inriskdiffcol && riskdiff_setup == "horizontal") {
|
| 820 | 33x |
.stats_adj <- replace(.stats, .stats %in% c("eair", "n_eair", "eair_n", "eair_n_py"), "eair_diff_est_ci")
|
| 821 |
} else {
|
|
| 822 | 50x |
.stats_adj <- .stats |
| 823 |
} |
|
| 824 | ||
| 825 | 83x |
.stats <- .stats_adj |
| 826 | ||
| 827 |
# Fill in formatting defaults |
|
| 828 | ||
| 829 | ||
| 830 | 83x |
x_stats <- x_stats[.stats] |
| 831 | 83x |
x_stats_orig <- x_stats |
| 832 |
# from now onwards, retransform x_stats back into structure |
|
| 833 |
# list with outer elements stats levels from incoming variable |
|
| 834 |
# inner elements statistics |
|
| 835 | 83x |
x_stats <- transpose_named_list(x_stats) |
| 836 | ||
| 837 | 83x |
levels_per_stats <- lapply(x_stats, names) |
| 838 |
# in case no default label get the label from the x_stats directly |
|
| 839 |
# this is to cover statistics with dynamic label, such as |
|
| 840 |
# n_event Either Number of events or Number of subjects with events |
|
| 841 | 83x |
.labels_pre <- junco_get_labels_from_attrib(.stats, .labels, x_stats) |
| 842 | ||
| 843 | 83x |
.formats <- junco_get_formats_from_stats(.stats, .formats, levels_per_stats) |
| 844 | 83x |
.labels <- junco_get_labels_from_stats(.stats, .labels_pre, levels_per_stats) |
| 845 | 83x |
.labels <- .unlist_keep_nulls(.labels) |
| 846 | ||
| 847 |
# make adjustments to rowlabels when not inrowsplit |
|
| 848 | 83x |
if (!inrowsplit && length(.stats) > 1) {
|
| 849 |
# if more than one stat requested and variable is not in a prior splitrowscall |
|
| 850 |
# prepend the level of the incoming variable (which is now in names of x_stats) to the label |
|
| 851 | 10x |
.labels <- paste(rep(names(x_stats), each = length(.stats)), .labels) |
| 852 | 73x |
} else if (!inrowsplit && length(.stats) == 1 && row_labels_adj) {
|
| 853 |
# if one stat requested and variable is not in a prior splitrowscall |
|
| 854 |
# show the levels of the incoming variable, rather than the statistic |
|
| 855 | 7x |
.labels <- names(x_stats) |
| 856 |
} |
|
| 857 | 83x |
.labels <- gsub("^\\s+", "", .labels)
|
| 858 | ||
| 859 | 83x |
.indent_mods <- junco_get_indents_from_stats( |
| 860 | 83x |
.stats, |
| 861 | 83x |
.indent_mods, |
| 862 | 83x |
levels_per_stats |
| 863 |
) |
|
| 864 | 83x |
.indent_mods <- .unlist_keep_nulls(.indent_mods) |
| 865 | ||
| 866 |
# .format_na_strs processing |
|
| 867 |
# if na_str = c(NA, NA, NA) |
|
| 868 |
# this will ensure the ci (NA, NA, NA) will be represented as NE (NE, NE) |
|
| 869 |
# the value NE is defined as the default to replace NA in our jjcs format |
|
| 870 | ||
| 871 | 83x |
if (!is.null(na_str)) {
|
| 872 | 83x |
.format_na_strs <- lapply(names(.formats), FUN = function(x) {
|
| 873 | 455x |
na_str |
| 874 |
}) |
|
| 875 |
} else {
|
|
| 876 | ! |
.format_na_strs <- NULL |
| 877 |
} |
|
| 878 | ||
| 879 |
# Unlist stats + names |
|
| 880 | 83x |
x_stats <- .unlist_keep_nulls(x_stats) |
| 881 | 83x |
names(x_stats) <- names(.formats) |
| 882 | ||
| 883 |
### final step: turn requested stats into rtables rows |
|
| 884 | 83x |
inrows <- in_rows( |
| 885 | 83x |
.list = x_stats, |
| 886 | 83x |
.formats = .formats, |
| 887 | 83x |
.labels = .labels, |
| 888 | 83x |
.indent_mods = .indent_mods, |
| 889 | 83x |
.format_na_strs = .format_na_strs |
| 890 |
) |
|
| 891 | ||
| 892 | 83x |
return(inrows) |
| 893 |
} |
| 1 |
#' @name jj_complex_scorefun |
|
| 2 |
#' @title Complex Scoring Function |
|
| 3 |
#' @description |
|
| 4 |
#' A function used for sorting AE tables (and others) as required. |
|
| 5 |
#' @details |
|
| 6 |
#' This sort function sorts as follows: |
|
| 7 |
#' * Takes all the columns from a specified spanning column header (default= colspan_trt) |
|
| 8 |
#' and sorts by the last treatment |
|
| 9 |
#' column within this. |
|
| 10 |
#' * If no spanning column header variable exists (e.g you have only one active treatment arm and have decided to |
|
| 11 |
#' remove the spanning header from your layout), it will sort by the first treatment column in your table. |
|
| 12 |
#' |
|
| 13 |
#' This function is not really designed for tables that have sub-columns. However, if users wish to override any |
|
| 14 |
#' default sorting behavior, they can simply specify their own colpath to use for sorting on (default = NULL) |
|
| 15 |
#' |
|
| 16 |
#' @param spanningheadercolvar (`character`)\cr |
|
| 17 |
#' Name of spanning header variable that defines the active treatment columns. |
|
| 18 |
#' If you do not have an active treatment spanning header column then user can define this as NA. |
|
| 19 |
#' @param usefirstcol (`logical`)\cr |
|
| 20 |
#' This allows you to just use the first column of the table to sort on. |
|
| 21 |
#' @param colpath (`character`)\cr |
|
| 22 |
#' Name of column path that is needed to sort by (default=NULL). |
|
| 23 |
#' This overrides other arguments if specified |
|
| 24 |
#' (except firstcat and lastcat which will be applied if requested on this colpath). |
|
| 25 |
#' @param firstcat (`logical`)\cr |
|
| 26 |
#' If you wish to put any category at the top of the list despite any n's, user can specify it here. |
|
| 27 |
#' @param lastcat (`logical`)\cr |
|
| 28 |
#' If you wish to put any category at the bottom of the list despite any n's, user can specify it here. |
|
| 29 |
#' @param cellvalue_index (`integer`)\cr |
|
| 30 |
#' Index of the cell value to perform the scoring on. |
|
| 31 |
#' @export |
|
| 32 |
#' @returns A function which can be used as a score function (scorefun in `sort_at_path`). |
|
| 33 |
# @examples #result <- sort_at_path(result, c('root', 'AEBODSYS'), scorefun = jj_complex_scorefun())
|
|
| 34 |
#' @examples |
|
| 35 |
#' library(dplyr) |
|
| 36 |
#' ADAE <- data.frame( |
|
| 37 |
#' USUBJID = c( |
|
| 38 |
#' "XXXXX01", "XXXXX02", "XXXXX03", "XXXXX04", "XXXXX05", |
|
| 39 |
#' "XXXXX06", "XXXXX07", "XXXXX08", "XXXXX09", "XXXXX10" |
|
| 40 |
#' ), |
|
| 41 |
#' AEBODSYS = c( |
|
| 42 |
#' "SOC 1", "SOC 2", "SOC 1", "SOC 2", "SOC 2", |
|
| 43 |
#' "SOC 2", "SOC 2", "SOC 1", "SOC 2", "SOC 1" |
|
| 44 |
#' ), |
|
| 45 |
#' AEDECOD = c( |
|
| 46 |
#' "Coded Term 2", "Coded Term 1", "Coded Term 3", "Coded Term 4", |
|
| 47 |
#' "Coded Term 4", "Coded Term 4", "Coded Term 5", "Coded Term 3", |
|
| 48 |
#' "Coded Term 1", "Coded Term 2" |
|
| 49 |
#' ), |
|
| 50 |
#' TRT01A = c( |
|
| 51 |
#' "ARMA", "ARMB", "ARMA", "ARMB", "ARMB", |
|
| 52 |
#' "Placebo", "Placebo", "Placebo", "ARMA", "ARMB" |
|
| 53 |
#' ), |
|
| 54 |
#' TRTEMFL = c("Y", "Y", "N", "Y", "Y", "Y", "Y", "N", "Y", "Y")
|
|
| 55 |
#' ) |
|
| 56 |
#' |
|
| 57 |
#' ADAE <- ADAE |> |
|
| 58 |
#' dplyr::mutate(TRT01A = as.factor(TRT01A)) |
|
| 59 |
#' |
|
| 60 |
#' ADAE$colspan_trt <- factor(ifelse(ADAE$TRT01A == "Placebo", " ", "Active Study Agent"), |
|
| 61 |
#' levels = c("Active Study Agent", " ")
|
|
| 62 |
#' ) |
|
| 63 |
#' |
|
| 64 |
#' ADAE$rrisk_header <- "Risk Difference (%) (95% CI)" |
|
| 65 |
#' ADAE$rrisk_label <- paste(ADAE$TRT01A, paste("vs", "Placebo"))
|
|
| 66 |
#' |
|
| 67 |
#' colspan_trt_map <- create_colspan_map(ADAE, |
|
| 68 |
#' non_active_grp = "Placebo", |
|
| 69 |
#' non_active_grp_span_lbl = " ", |
|
| 70 |
#' active_grp_span_lbl = "Active Study Agent", |
|
| 71 |
#' colspan_var = "colspan_trt", |
|
| 72 |
#' trt_var = "TRT01A" |
|
| 73 |
#' ) |
|
| 74 |
#' |
|
| 75 |
#' ref_path <- c("colspan_trt", " ", "TRT01A", "Placebo")
|
|
| 76 |
#' |
|
| 77 |
#' ADSL <- unique(ADAE |> select(USUBJID, "colspan_trt", "rrisk_header", "rrisk_label", "TRT01A")) |
|
| 78 |
#' |
|
| 79 |
#' lyt <- basic_table() |> |
|
| 80 |
#' split_cols_by( |
|
| 81 |
#' "colspan_trt", |
|
| 82 |
#' split_fun = trim_levels_to_map(map = colspan_trt_map) |
|
| 83 |
#' ) |> |
|
| 84 |
#' split_cols_by("TRT01A") |>
|
|
| 85 |
#' split_cols_by("rrisk_header", nested = FALSE) |>
|
|
| 86 |
#' split_cols_by( |
|
| 87 |
#' "TRT01A", |
|
| 88 |
#' labels_var = "rrisk_label", |
|
| 89 |
#' split_fun = remove_split_levels("Placebo")
|
|
| 90 |
#' ) |> |
|
| 91 |
#' analyze( |
|
| 92 |
#' "TRTEMFL", |
|
| 93 |
#' a_freq_j, |
|
| 94 |
#' show_labels = "hidden", |
|
| 95 |
#' extra_args = list( |
|
| 96 |
#' method = "wald", |
|
| 97 |
#' label = "Subjects with >=1 AE", |
|
| 98 |
#' ref_path = ref_path, |
|
| 99 |
#' .stats = "count_unique_fraction" |
|
| 100 |
#' ) |
|
| 101 |
#' ) |> |
|
| 102 |
#' split_rows_by("AEBODSYS",
|
|
| 103 |
#' split_label = "System Organ Class", |
|
| 104 |
#' split_fun = trim_levels_in_group("AEDECOD"),
|
|
| 105 |
#' label_pos = "topleft", |
|
| 106 |
#' section_div = c(" "),
|
|
| 107 |
#' nested = FALSE |
|
| 108 |
#' ) |> |
|
| 109 |
#' summarize_row_groups( |
|
| 110 |
#' "AEBODSYS", |
|
| 111 |
#' cfun = a_freq_j, |
|
| 112 |
#' extra_args = list( |
|
| 113 |
#' method = "wald", |
|
| 114 |
#' ref_path = ref_path, |
|
| 115 |
#' .stats = "count_unique_fraction" |
|
| 116 |
#' ) |
|
| 117 |
#' ) |> |
|
| 118 |
#' analyze( |
|
| 119 |
#' "AEDECOD", |
|
| 120 |
#' afun = a_freq_j, |
|
| 121 |
#' extra_args = list( |
|
| 122 |
#' method = "wald", |
|
| 123 |
#' ref_path = ref_path, |
|
| 124 |
#' .stats = "count_unique_fraction" |
|
| 125 |
#' ) |
|
| 126 |
#' ) |
|
| 127 |
#' |
|
| 128 |
#' result <- build_table(lyt, ADAE, alt_counts_df = ADSL) |
|
| 129 |
#' |
|
| 130 |
#' result |
|
| 131 |
#' |
|
| 132 |
#' result <- sort_at_path( |
|
| 133 |
#' result, |
|
| 134 |
#' c("root", "AEBODSYS"),
|
|
| 135 |
#' scorefun = jj_complex_scorefun() |
|
| 136 |
#' ) |
|
| 137 |
#' |
|
| 138 |
#' result <- sort_at_path( |
|
| 139 |
#' result, |
|
| 140 |
#' c("root", "AEBODSYS", "*", "AEDECOD"),
|
|
| 141 |
#' scorefun = jj_complex_scorefun() |
|
| 142 |
#' ) |
|
| 143 |
#' |
|
| 144 |
#' result |
|
| 145 |
#' @rdname complex_scoring_function |
|
| 146 |
#' @aliases jj_complex_scorefun |
|
| 147 |
jj_complex_scorefun <- function( |
|
| 148 |
spanningheadercolvar = "colspan_trt", |
|
| 149 |
usefirstcol = FALSE, |
|
| 150 |
colpath = NULL, |
|
| 151 |
firstcat = NULL, |
|
| 152 |
lastcat = NULL, |
|
| 153 |
cellvalue_index = 1 |
|
| 154 |
) {
|
|
| 155 | 12x |
paths <- NULL |
| 156 | ||
| 157 | 12x |
function(tt) {
|
| 158 | 225x |
if (is.null(paths)) {
|
| 159 | 12x |
paths <<- col_paths(tt) |
| 160 | 12x |
if (is.null(colpath)) {
|
| 161 | 10x |
if (is.na(spanningheadercolvar)) {
|
| 162 | 6x |
atrt_paths <- vapply(paths, function(pth) pth[[1]] != " ", TRUE) |
| 163 | 6x |
use_paths <- atrt_paths |
| 164 | 6x |
act_trt_lst <- paths[use_paths] |
| 165 | 6x |
first_list <- sapply(act_trt_lst, utils::head, 1) |
| 166 | 6x |
first_at_value <- utils::head(first_list, n = 1) |
| 167 | 6x |
colpath <<- first_at_value |
| 168 |
} else {
|
|
| 169 | 4x |
atrt_paths <- vapply(paths, function(pth) pth[[1]] == spanningheadercolvar && pth[[2]] != " ", TRUE) |
| 170 | 4x |
use_paths <- atrt_paths |
| 171 | 4x |
act_trt_lst <- paths[use_paths] |
| 172 | 4x |
last_list <- sapply(act_trt_lst, utils::tail, 1) |
| 173 | 4x |
last_at_value <- utils::tail(last_list, n = 1) |
| 174 | 4x |
colpath <<- last_at_value |
| 175 |
} |
|
| 176 | ||
| 177 | 10x |
if (usefirstcol) {
|
| 178 | 4x |
atrt_paths <- vapply(paths, function(pth) pth[[1]] != " ", TRUE) |
| 179 | 4x |
use_paths <- atrt_paths |
| 180 | 4x |
act_trt_lst <- paths[use_paths] |
| 181 | 4x |
first_list <- sapply(act_trt_lst, utils::head, 1) |
| 182 | 4x |
first_at_value <- utils::head(first_list, n = 1) |
| 183 | 4x |
colpath <<- first_at_value |
| 184 |
} |
|
| 185 |
} |
|
| 186 |
} |
|
| 187 | 225x |
unl_cv <- unlist(cell_values(tt, colpath = colpath), use.names = FALSE) |
| 188 | 225x |
if (length(unl_cv) < cellvalue_index) {
|
| 189 | ! |
score <- NULL |
| 190 |
} else {
|
|
| 191 | 225x |
score <- unl_cv[cellvalue_index] |
| 192 |
} |
|
| 193 | ||
| 194 | 225x |
if (length(score) == 0) {
|
| 195 | ! |
score <- 1 |
| 196 |
} |
|
| 197 | ||
| 198 | 225x |
if (!is.null(firstcat)) {
|
| 199 | 30x |
if (obj_name(tt) == firstcat) {
|
| 200 | 3x |
score <- Inf |
| 201 |
} |
|
| 202 |
} |
|
| 203 | 225x |
if (!is.null(lastcat)) {
|
| 204 | 30x |
if (obj_name(tt) == lastcat) {
|
| 205 | 3x |
score <- -99999 |
| 206 |
} |
|
| 207 |
} |
|
| 208 | 225x |
if (obj_name(tt) == " ") {
|
| 209 | ! |
score <- -Inf |
| 210 |
} |
|
| 211 | ||
| 212 | 225x |
return(score) |
| 213 |
} |
|
| 214 |
} |
| 1 |
#' @name safe_prune_table |
|
| 2 |
#' |
|
| 3 |
#' @title Safely Prune Table With Empty Table Message If Needed |
|
| 4 |
#' |
|
| 5 |
#' @inheritParams rtables::prune_table |
|
| 6 |
#' |
|
| 7 |
#' @param empty_msg (`character(1)`)\cr The message to place in the table |
|
| 8 |
#' if no rows were left after pruning |
|
| 9 |
#' |
|
| 10 |
#' @param spancols (`logical(1)`)\cr Should `empty_msg` be spanned |
|
| 11 |
#' across the table's columns (`TRUE`) or placed in the |
|
| 12 |
#' rows row label (`FALSE`). Defaults to `FALSE` currently. |
|
| 13 |
#' |
|
| 14 |
#' @rdname safe_prune_table |
|
| 15 |
#' @return `tt` pruned based on the arguments, or, if |
|
| 16 |
#' pruning would remove all rows, a TableTree with the |
|
| 17 |
#' same column structure, and one row containing the |
|
| 18 |
#' empty message spanning all columns. |
|
| 19 |
#' |
|
| 20 |
#' @export |
|
| 21 |
#' @examples |
|
| 22 |
#' prfun <- function(tt) TRUE |
|
| 23 |
#' |
|
| 24 |
#' lyt <- basic_table() |> |
|
| 25 |
#' split_cols_by("ARM") |>
|
|
| 26 |
#' split_cols_by("STRATA1") |>
|
|
| 27 |
#' split_rows_by("SEX") |>
|
|
| 28 |
#' analyze("AGE")
|
|
| 29 |
#' tbl <- build_table(lyt, ex_adsl) |
|
| 30 |
#' |
|
| 31 |
#' safe_prune_table(tbl, prfun) |
|
| 32 |
safe_prune_table <- function( |
|
| 33 |
tt, |
|
| 34 |
prune_func = prune_empty_level, |
|
| 35 |
stop_depth = NA, |
|
| 36 |
empty_msg = " - No Data To Display - ", |
|
| 37 |
spancols = FALSE) {
|
|
| 38 | 9x |
ret <- prune_table(tt = tt, prune_func = prune_func, stop_depth = stop_depth, depth = 0) |
| 39 | 9x |
if (is.null(ret)) {
|
| 40 | 3x |
ret <- tt[integer(), , keep_titles = TRUE, keep_topleft = TRUE, keep_footers = TRUE] |
| 41 | ||
| 42 | 3x |
if (spancols) {
|
| 43 |
## this will eventually be the only option... |
|
| 44 | 1x |
ret <- sanitize_table_struct(ret, empty_msg = empty_msg) |
| 45 |
} else {
|
|
| 46 | 2x |
tree_children(ret) <- list(rrowl(empty_msg, rep(" ", ncol(tt))))
|
| 47 |
} |
|
| 48 |
} |
|
| 49 | 9x |
ret |
| 50 |
} |
|
| 51 | ||
| 52 |
#' @name count_pruner |
|
| 53 |
#' |
|
| 54 |
#' @title Count Pruner |
|
| 55 |
#' |
|
| 56 |
#' @description |
|
| 57 |
#' This is a pruning constructor function which identifies records to be pruned |
|
| 58 |
#' based on the count (assumed to be the first statistic displayed when a compound |
|
| 59 |
#' statistic (e.g., ## / ## (XX.X percent) is presented). |
|
| 60 |
#' |
|
| 61 |
#' @param count (`numeric`)\cr count threshold. Function will keep all records strictly greater |
|
| 62 |
#' than this threshold. |
|
| 63 |
#' @param cols (`character`)\cr column path (character or integer (column indices)) |
|
| 64 |
#' @param cat_include (`character`)\cr Category to be considered for pruning |
|
| 65 |
#' @param cat_exclude (`character`)\cr Category to be excluded from pruning |
|
| 66 |
#' @export |
|
| 67 |
#' |
|
| 68 |
#' @examples |
|
| 69 |
#' ADSL <- data.frame( |
|
| 70 |
#' USUBJID = c( |
|
| 71 |
#' "XXXXX01", "XXXXX02", "XXXXX03", "XXXXX04", "XXXXX05", |
|
| 72 |
#' "XXXXX06", "XXXXX07", "XXXXX08", "XXXXX09", "XXXXX10" |
|
| 73 |
#' ), |
|
| 74 |
#' TRT01P = factor( |
|
| 75 |
#' c( |
|
| 76 |
#' "ARMA", "ARMB", "ARMA", "ARMB", "ARMB", |
|
| 77 |
#' "Placebo", "Placebo", "Placebo", "ARMA", "ARMB" |
|
| 78 |
#' ) |
|
| 79 |
#' ), |
|
| 80 |
#' FASFL = c("Y", "Y", "Y", "Y", "N", "Y", "Y", "Y", "Y", "Y"),
|
|
| 81 |
#' SAFFL = c("N", "N", "N", "N", "N", "N", "N", "N", "N", "N"),
|
|
| 82 |
#' PKFL = c("N", "N", "N", "N", "N", "N", "N", "N", "N", "N")
|
|
| 83 |
#' ) |
|
| 84 |
#' |
|
| 85 |
#' lyt <- basic_table() |> |
|
| 86 |
#' split_cols_by("TRT01P") |>
|
|
| 87 |
#' add_overall_col("Total") |>
|
|
| 88 |
#' analyze("FASFL",
|
|
| 89 |
#' var_labels = "Analysis set:", |
|
| 90 |
#' afun = a_freq_j, |
|
| 91 |
#' extra_args = list(label = "Full", val = "Y"), |
|
| 92 |
#' show_labels = "visible" |
|
| 93 |
#' ) |> |
|
| 94 |
#' analyze("SAFFL",
|
|
| 95 |
#' var_labels = "Analysis set:", |
|
| 96 |
#' afun = a_freq_j, |
|
| 97 |
#' extra_args = list(label = "Safety", val = "Y"), |
|
| 98 |
#' show_labels = "visible" |
|
| 99 |
#' ) |> |
|
| 100 |
#' analyze("PKFL",
|
|
| 101 |
#' var_labels = "Analysis set:", |
|
| 102 |
#' afun = a_freq_j, |
|
| 103 |
#' extra_args = list(label = "PK", val = "Y"), |
|
| 104 |
#' show_labels = "visible" |
|
| 105 |
#' ) |
|
| 106 |
#' |
|
| 107 |
#' result <- build_table(lyt, ADSL) |
|
| 108 |
#' |
|
| 109 |
#' result |
|
| 110 |
#' |
|
| 111 |
#' result <- prune_table( |
|
| 112 |
#' result, |
|
| 113 |
#' prune_func = count_pruner(cat_exclude = c("Safety"), cols = "Total")
|
|
| 114 |
#' ) |
|
| 115 |
#' |
|
| 116 |
#' result |
|
| 117 |
#' @rdname count_pruner |
|
| 118 |
#' @returns Function that can be utilized as pruning function in prune_table. |
|
| 119 |
#' |
|
| 120 |
count_pruner <- function(count = 0, cat_include = NULL, cat_exclude = NULL, cols = c("TRT01A")) {
|
|
| 121 | 6x |
colpaths <- NULL |
| 122 | ||
| 123 | 6x |
function(tt) {
|
| 124 |
# Do not ever prune the following rows. a row that should be kept in the table will get the value of FALSE |
|
| 125 | ||
| 126 | 6x |
if ( # nolint start |
| 127 | 1089x |
!methods::is(tt, "TableRow") || |
| 128 | 1089x |
methods::is(tt, "LabelRow") || |
| 129 | 1089x |
obj_label(tt) == " " || |
| 130 | 1089x |
(!is.null(cat_include) && |
| 131 | 1089x |
!obj_label(tt) %in% cat_include) || |
| 132 | 1089x |
(!is.null(cat_exclude) && obj_label(tt) %in% cat_exclude) |
| 133 | 6x |
) { # nolint end
|
| 134 | 655x |
return(FALSE) |
| 135 |
} |
|
| 136 | ||
| 137 |
# Check the remaining rows to see if any meet the specified threshold. |
|
| 138 | ||
| 139 | 434x |
if (!is.null(cols)) {
|
| 140 | 434x |
tt <- subset_cols(tt, cols) |
| 141 |
} |
|
| 142 | ||
| 143 |
# init return value to FALSE (not remove row) |
|
| 144 | 434x |
remove <- FALSE |
| 145 | ||
| 146 | 434x |
if (is.null(colpaths)) {
|
| 147 | 5x |
colpaths <<- col_paths(tt) |
| 148 |
} |
|
| 149 | ||
| 150 |
# identify non relative risk columns as in rrisk columns first element is not a count, but diff percentage with |
|
| 151 |
# small total columns and count > 0 (eg count = 1, n per group = 10), you can run into count1 = 1, countpbo = |
|
| 152 |
# 0, diffpct = 10, this row should not be considered though |
|
| 153 | 434x |
cp_nonrelrisk <- vapply( |
| 154 | 434x |
colpaths, |
| 155 | 434x |
function(pth) {
|
| 156 | 1308x |
!any(grepl("rrisk", tolower(pth)))
|
| 157 |
}, |
|
| 158 | 434x |
FUN.VALUE = TRUE |
| 159 |
) |
|
| 160 | ||
| 161 |
## only continue if at least one non relative risk column selected |
|
| 162 | 434x |
if (any(cp_nonrelrisk)) {
|
| 163 |
# get cell_values of non rel risk columns |
|
| 164 | 434x |
cell_vals <- cell_values(tt)[cp_nonrelrisk] |
| 165 | ||
| 166 | 434x |
len_cell_vals <- lapply(cell_vals, function(x) {
|
| 167 | 1302x |
length(x) |
| 168 |
}) |
|
| 169 | ||
| 170 | 434x |
if (any(len_cell_vals == 0)) {
|
| 171 | ! |
stop("column cell values has not appropriate structure (a column with NULL value).")
|
| 172 |
} |
|
| 173 | ||
| 174 |
# get count (first element) |
|
| 175 | 434x |
counts <- unlist( |
| 176 | 434x |
lapply(cell_vals, function(x) {
|
| 177 | 1302x |
x[[1]] |
| 178 |
}), |
|
| 179 | 434x |
use.names = FALSE |
| 180 |
) |
|
| 181 | ||
| 182 |
# check that we do have counts only |
|
| 183 | 434x |
checkmate::assert_integerish(counts) |
| 184 | ||
| 185 | 434x |
keep <- counts > count |
| 186 |
# the row should be kept if at least one column has count above threshold |
|
| 187 | 434x |
keep <- any(keep) |
| 188 | ||
| 189 |
# pruning function should return TRUE if row has to be removed, ie opposite of keep |
|
| 190 | 434x |
remove <- !keep |
| 191 |
} |
|
| 192 | ||
| 193 | 434x |
return(remove) |
| 194 |
} |
|
| 195 |
} |
|
| 196 | ||
| 197 | ||
| 198 |
#' @name bspt_pruner |
|
| 199 |
#' |
|
| 200 |
#' @title Pruning Function for pruning based on a fraction and/or a difference from the control arm |
|
| 201 |
#' |
|
| 202 |
#' @description |
|
| 203 |
#' This is a pruning constructor function which identifies records to be pruned |
|
| 204 |
#' based on the the fraction from the percentages. In addition to just looking at a |
|
| 205 |
#' fraction within an arm, |
|
| 206 |
#' this function also allows further flexibility to also prune based on a comparison versus the control arm. |
|
| 207 |
#' @param fraction (`proportion`)\cr Fraction threshold. |
|
| 208 |
#' Function will keep all records strictly greater |
|
| 209 |
#' than this threshold. |
|
| 210 |
#' @param cols (`character`)\cr Column path. |
|
| 211 |
#' @param keeprowtext (`character`)\cr Row to be excluded from pruning. |
|
| 212 |
#' @param reg_expr (`logical`)\cr Apply keeprowtext as a regular expression (grepl with fixed = TRUE) |
|
| 213 |
#' @param control (`character`)\cr Control Group |
|
| 214 |
#' @param diff_from_control (`numeric`)\cr Difference from control threshold. |
|
| 215 |
#' @param only_more_often (`logical`)\cr TRUE: Only consider when column pct is more often |
|
| 216 |
#' than control. FALSE: Also select a row where column pct is less often than |
|
| 217 |
#' control and abs(diff) above threshold |
|
| 218 |
#' |
|
| 219 |
#' @export |
|
| 220 |
#' |
|
| 221 |
#' |
|
| 222 |
#' @examples |
|
| 223 |
#' ADSL <- data.frame( |
|
| 224 |
#' USUBJID = c( |
|
| 225 |
#' "XXXXX01", "XXXXX02", "XXXXX03", "XXXXX04", "XXXXX05", |
|
| 226 |
#' "XXXXX06", "XXXXX07", "XXXXX08", "XXXXX09", "XXXXX10" |
|
| 227 |
#' ), |
|
| 228 |
#' TRT01P = c( |
|
| 229 |
#' "ARMA", "ARMB", "ARMA", "ARMB", "ARMB", |
|
| 230 |
#' "Placebo", "Placebo", "Placebo", "ARMA", "ARMB" |
|
| 231 |
#' ), |
|
| 232 |
#' FASFL = c("Y", "Y", "Y", "Y", "N", "Y", "Y", "Y", "Y", "Y"),
|
|
| 233 |
#' SAFFL = c("N", "N", "N", "N", "N", "N", "N", "N", "N", "N"),
|
|
| 234 |
#' PKFL = c("N", "N", "N", "N", "N", "N", "N", "N", "N", "N")
|
|
| 235 |
#' ) |
|
| 236 |
#' |
|
| 237 |
#' ADSL <- ADSL |> |
|
| 238 |
#' dplyr::mutate(TRT01P = as.factor(TRT01P)) |> |
|
| 239 |
#' dplyr::mutate(SAFFL = factor(SAFFL, c("Y", "N"))) |>
|
|
| 240 |
#' dplyr::mutate(PKFL = factor(PKFL, c("Y", "N")))
|
|
| 241 |
#' |
|
| 242 |
#' lyt <- basic_table() |> |
|
| 243 |
#' split_cols_by("TRT01P") |>
|
|
| 244 |
#' add_overall_col("Total") |>
|
|
| 245 |
#' split_rows_by( |
|
| 246 |
#' "FASFL", |
|
| 247 |
#' split_fun = drop_and_remove_levels("N"),
|
|
| 248 |
#' child_labels = "hidden" |
|
| 249 |
#' ) |> |
|
| 250 |
#' analyze("FASFL",
|
|
| 251 |
#' var_labels = "Analysis set:", |
|
| 252 |
#' afun = a_freq_j, |
|
| 253 |
#' show_labels = "visible", |
|
| 254 |
#' extra_args = list(label = "Full", .stats = "count_unique_fraction") |
|
| 255 |
#' ) |> |
|
| 256 |
#' split_rows_by( |
|
| 257 |
#' "SAFFL", |
|
| 258 |
#' split_fun = remove_split_levels("N"),
|
|
| 259 |
#' child_labels = "hidden" |
|
| 260 |
#' ) |> |
|
| 261 |
#' analyze("SAFFL",
|
|
| 262 |
#' var_labels = "Analysis set:", |
|
| 263 |
#' afun = a_freq_j, |
|
| 264 |
#' show_labels = "visible", |
|
| 265 |
#' extra_args = list(label = "Safety", .stats = "count_unique_fraction") |
|
| 266 |
#' ) |> |
|
| 267 |
#' split_rows_by( |
|
| 268 |
#' "PKFL", |
|
| 269 |
#' split_fun = remove_split_levels("N"),
|
|
| 270 |
#' child_labels = "hidden" |
|
| 271 |
#' ) |> |
|
| 272 |
#' analyze("PKFL",
|
|
| 273 |
#' var_labels = "Analysis set:", |
|
| 274 |
#' afun = a_freq_j, |
|
| 275 |
#' show_labels = "visible", |
|
| 276 |
#' extra_args = list(label = "PK", .stats = "count_unique_fraction") |
|
| 277 |
#' ) |
|
| 278 |
#' |
|
| 279 |
#' result <- build_table(lyt, ADSL) |
|
| 280 |
#' |
|
| 281 |
#' result |
|
| 282 |
#' |
|
| 283 |
#' result <- prune_table( |
|
| 284 |
#' result, |
|
| 285 |
#' prune_func = bspt_pruner( |
|
| 286 |
#' fraction = 0.05, |
|
| 287 |
#' keeprowtext = "Safety", |
|
| 288 |
#' cols = c("Total")
|
|
| 289 |
#' ) |
|
| 290 |
#' ) |
|
| 291 |
#' |
|
| 292 |
#' result |
|
| 293 |
#' @rdname bspt_pruner |
|
| 294 |
#' @returns Function that can be utilized as pruning function in prune_table. |
|
| 295 |
#' |
|
| 296 |
bspt_pruner <- function( |
|
| 297 |
fraction = 0.05, |
|
| 298 |
keeprowtext = "Analysis set: Safety", |
|
| 299 |
reg_expr = FALSE, |
|
| 300 |
control = NULL, |
|
| 301 |
diff_from_control = NULL, |
|
| 302 |
only_more_often = TRUE, |
|
| 303 |
cols = c("TRT01A")) {
|
|
| 304 | 12x |
if (is.null(fraction) && is.null(diff_from_control)) {
|
| 305 | 1x |
stop("At least one of fraction or diff_from_control must be non-NULL.")
|
| 306 |
} |
|
| 307 | 11x |
if (!is.null(diff_from_control) && is.null(control)) {
|
| 308 | 1x |
stop("control must be specified when diff_from_control is not NULL.")
|
| 309 |
} |
|
| 310 | ||
| 311 | 10x |
function(tt) {
|
| 312 |
# Do not ever prune the following rows. |
|
| 313 | 137x |
if (!methods::is(tt, "TableRow") || methods::is(tt, "LabelRow")) {
|
| 314 | 24x |
return(FALSE) |
| 315 |
} |
|
| 316 | ||
| 317 | 10x |
if ( |
| 318 | 113x |
reg_expr && |
| 319 | 113x |
any(sapply(keeprowtext, function(x) {
|
| 320 | ! |
grepl(x, obj_label(tt), fixed = TRUE) |
| 321 |
})) |
|
| 322 |
) {
|
|
| 323 | ! |
return(FALSE) |
| 324 |
} |
|
| 325 | 113x |
if (!reg_expr && obj_label(tt) %in% keeprowtext) {
|
| 326 | 10x |
return(FALSE) |
| 327 |
} |
|
| 328 | ||
| 329 |
# init return value to FALSE (not remove row) |
|
| 330 | 103x |
remove <- FALSE |
| 331 | ||
| 332 |
# needed later for control column |
|
| 333 | 103x |
tt_all_cols <- tt |
| 334 | ||
| 335 | 103x |
if (!is.null(cols)) {
|
| 336 | 103x |
tt <- subset_cols(tt, cols) |
| 337 |
} |
|
| 338 | ||
| 339 | 103x |
colpaths <- col_paths(tt) |
| 340 |
# identify non relative risk columns |
|
| 341 | 103x |
cp_nonrelrisk <- vapply( |
| 342 | 103x |
colpaths, |
| 343 | 103x |
function(pth) {
|
| 344 | 329x |
!any(grepl("rrisk", tolower(pth)))
|
| 345 |
}, |
|
| 346 | 103x |
FUN.VALUE = TRUE |
| 347 |
) |
|
| 348 | ||
| 349 |
## only continue if at least one non relative risk column selected |
|
| 350 | 103x |
if (any(cp_nonrelrisk)) {
|
| 351 |
# get rid of rel risk columns (if present) |
|
| 352 | 103x |
cell_vals <- cell_values(tt)[cp_nonrelrisk] |
| 353 | ||
| 354 | 103x |
len_cell_vals <- lapply(cell_vals, function(x) {
|
| 355 | 296x |
length(x) |
| 356 |
}) |
|
| 357 | ||
| 358 | 103x |
if (any(len_cell_vals < 2)) {
|
| 359 | ! |
stop("column cell values has not appropriate structure (less than 2 values in cell).")
|
| 360 |
} |
|
| 361 | ||
| 362 |
# get count and percentage columns counts in first col pcts in second col |
|
| 363 | 103x |
counts <- unname(lst_slicer(cell_vals, 1, numeric(1))) |
| 364 | 103x |
pcts <- unname(lst_slicer(cell_vals, 2, numeric(1))) |
| 365 | ||
| 366 | 103x |
checkmate::check_integerish(counts) |
| 367 |
# similarly check that pcts is indeed a percentage |
|
| 368 | 103x |
if (!all(pcts >= 0 & pcts <= 1)) {
|
| 369 | ! |
stop("second value column cell is not a percentage.")
|
| 370 |
} |
|
| 371 | ||
| 372 | 103x |
if (!is.null(fraction)) {
|
| 373 |
# avoid problems with FALSE for 0.05 >= 0.05 |
|
| 374 | 103x |
check_pcts <- pcts >= fraction | |
| 375 | 103x |
vapply( |
| 376 | 103x |
pcts, |
| 377 | 103x |
function(x) {
|
| 378 | 296x |
isTRUE(all.equal(x, fraction)) |
| 379 |
}, |
|
| 380 | 103x |
TRUE |
| 381 |
) |
|
| 382 |
} |
|
| 383 | ||
| 384 | 103x |
if (!is.null(diff_from_control)) {
|
| 385 |
# get control group from all columns tt (in case the columns selected would not include the control |
|
| 386 |
# group) |
|
| 387 | 78x |
colpaths0 <- col_paths(subset_cols(tt_all_cols, c(control))) |
| 388 | 78x |
cp0_nonrelrisk <- vapply( |
| 389 | 78x |
colpaths0, |
| 390 | 78x |
function(pth) {
|
| 391 | 83x |
!any(grepl("rrisk", tolower(pth)))
|
| 392 |
}, |
|
| 393 | 78x |
FUN.VALUE = TRUE |
| 394 |
) |
|
| 395 | 78x |
colpaths0 <- colpaths0[cp0_nonrelrisk] |
| 396 | ||
| 397 | 78x |
if (length(colpaths0) != 1) {
|
| 398 | 1x |
stop("control group spec does not result in single column.")
|
| 399 |
} else {
|
|
| 400 | 77x |
colpaths0 <- colpaths0[[1]] |
| 401 |
# get percent from control group |
|
| 402 | 77x |
pct0 <- cell_values(tt_all_cols, colpath = c(control))[[1]][2] |
| 403 | ||
| 404 | 77x |
if (!(pct0 >= 0 && pct0 <= 1)) {
|
| 405 | ! |
stop("second value of control column cell is not a percentage.")
|
| 406 |
} |
|
| 407 |
} |
|
| 408 | ||
| 409 |
# check if control group column was also present in main column selector |
|
| 410 | 77x |
colid0 <- which(sapply(colpaths, function(x) {
|
| 411 | 248x |
all(x == colpaths0) |
| 412 |
})) |
|
| 413 | ||
| 414 | 77x |
pctdiffs <- pcts - pct0 |
| 415 | ||
| 416 |
# exclude control column from this (would only be relevant if we want to check criteria on all columns |
|
| 417 |
# rather than any column) |
|
| 418 | 66x |
if (!identical(colid0, integer(0))) pctdiffs <- pctdiffs[-colid0] |
| 419 | ||
| 420 |
# if not only_more_often be equally strict on worse from control, better as control ie also select rows |
|
| 421 |
# where column is less often than control pct = 0.07 pct0 = 0.10 (diff = -0.03 ) |
|
| 422 | 22x |
if (!only_more_often) pctdiffs <- abs(pctdiffs) |
| 423 | ||
| 424 |
# avoid problems with FALSE for 0.02 >= 0.02 |
|
| 425 | 77x |
check_diffpcts <- pctdiffs >= diff_from_control | |
| 426 | 77x |
vapply( |
| 427 | 77x |
pctdiffs, |
| 428 | 77x |
function(x) {
|
| 429 | 161x |
isTRUE(all.equal(x, diff_from_control)) |
| 430 |
}, |
|
| 431 | 77x |
TRUE |
| 432 |
) |
|
| 433 |
} |
|
| 434 | ||
| 435 |
# final step to check if row should be kept in table |
|
| 436 | 102x |
if (!is.null(fraction) && is.null(diff_from_control)) {
|
| 437 |
# keep row in table if any column pct above threshold |
|
| 438 | 25x |
keep <- any(check_pcts) |
| 439 | 77x |
} else if ((!is.null(fraction) && !is.null(diff_from_control))) {
|
| 440 |
# if any column pct above threshold AND any diff pct above threshold note any is used in both |
|
| 441 |
# expressions separately situation pct = 0.04 pct0 = 0.06 should be kept (fraction = 0.05, abs(diff |
|
| 442 |
# control) = 0.02) this problem only arises in the non-default situation when only_more_often is FALSE |
|
| 443 |
# (ie when column pct is less often than control) this would not be the case if we would any(check_pcts |
|
| 444 |
# & check_diffpcts) as 0.04 is below fraction |
|
| 445 | 77x |
keep <- any(check_pcts) && any(check_diffpcts) |
| 446 | ! |
} else if ((is.null(fraction) && !is.null(diff_from_control))) {
|
| 447 | ! |
keep <- any(check_diffpcts) |
| 448 |
} |
|
| 449 | ||
| 450 |
# pruning function should return TRUE if row has to be removed, ie opposite of keep |
|
| 451 | 102x |
remove <- !keep |
| 452 |
} |
|
| 453 | ||
| 454 | 102x |
return(remove) |
| 455 |
} |
|
| 456 |
} |
|
| 457 | ||
| 458 |
lst_slicer <- function(lst, ind, type) {
|
|
| 459 | 206x |
vapply(lst, `[[`, i = ind, type) |
| 460 |
} |
|
| 461 | ||
| 462 |
#' @name remove_rows |
|
| 463 |
#' |
|
| 464 |
#' @title |
|
| 465 |
#' Pruning function to remove specific rows of a table regardless of counts |
|
| 466 |
#' @description |
|
| 467 |
#' This function will remove all rows of a table based on the row text |
|
| 468 |
#' provided by the user. |
|
| 469 |
#' @param removerowtext (`character`)\cr Define a text string for which any row with row text will be removed. |
|
| 470 |
#' @param reg_expr (`logical`)\cr Apply removerowtext as a regular expression |
|
| 471 |
#' (grepl with fixed = TRUE) |
|
| 472 |
#' @export |
|
| 473 |
#' @rdname remove_rows |
|
| 474 |
#' |
|
| 475 |
#' @examples |
|
| 476 |
#' ADSL <- data.frame( |
|
| 477 |
#' USUBJID = c( |
|
| 478 |
#' "XXXXX01", "XXXXX02", "XXXXX03", "XXXXX04", "XXXXX05", |
|
| 479 |
#' "XXXXX06", "XXXXX07", "XXXXX08", "XXXXX09", "XXXXX10" |
|
| 480 |
#' ), |
|
| 481 |
#' TRT01P = c( |
|
| 482 |
#' "ARMA", "ARMB", "ARMA", "ARMB", "ARMB", "Placebo", |
|
| 483 |
#' "Placebo", "Placebo", "ARMA", "ARMB" |
|
| 484 |
#' ), |
|
| 485 |
#' Category = c( |
|
| 486 |
#' "Cat 1", "Cat 2", "Cat 1", "Unknown", "Cat 2", |
|
| 487 |
#' "Cat 1", "Unknown", "Cat 1", "Cat 2", "Cat 1" |
|
| 488 |
#' ), |
|
| 489 |
#' SAFFL = c("N", "N", "N", "N", "N", "N", "N", "N", "N", "N"),
|
|
| 490 |
#' PKFL = c("N", "N", "N", "N", "N", "N", "N", "N", "N", "N")
|
|
| 491 |
#' ) |
|
| 492 |
#' |
|
| 493 |
#' ADSL <- ADSL |> |
|
| 494 |
#' dplyr::mutate(TRT01P = as.factor(TRT01P)) |
|
| 495 |
#' |
|
| 496 |
#' lyt <- basic_table() |> |
|
| 497 |
#' split_cols_by("TRT01P") |>
|
|
| 498 |
#' analyze( |
|
| 499 |
#' "Category", |
|
| 500 |
#' afun = a_freq_j, |
|
| 501 |
#' extra_args = list(.stats = "count_unique_fraction") |
|
| 502 |
#' ) |
|
| 503 |
#' |
|
| 504 |
#' result <- build_table(lyt, ADSL) |
|
| 505 |
#' |
|
| 506 |
#' result |
|
| 507 |
#' |
|
| 508 |
#' result <- prune_table(result, prune_func = remove_rows(removerowtext = "Unknown")) |
|
| 509 |
#' |
|
| 510 |
#' result |
|
| 511 |
#' @aliases remove_rows |
|
| 512 |
#' @returns Function that can be utilized as pruning function in prune_table. |
|
| 513 |
#' |
|
| 514 |
remove_rows <- function(removerowtext = NULL, reg_expr = FALSE) {
|
|
| 515 | 5x |
function(tt) {
|
| 516 | 97x |
if (!methods::is(tt, "TableRow") || methods::is(tt, "LabelRow")) {
|
| 517 | 5x |
return(FALSE) |
| 518 |
} |
|
| 519 | ||
| 520 | 92x |
ret <- FALSE |
| 521 | 92x |
if (!is.null(removerowtext)) {
|
| 522 | 72x |
if (!reg_expr) {
|
| 523 | 37x |
ret <- obj_label(tt) %in% removerowtext |
| 524 |
} else {
|
|
| 525 | 35x |
ret <- any(sapply(removerowtext, function(x) {
|
| 526 | 52x |
grepl(x, obj_label(tt), fixed = TRUE) |
| 527 |
})) |
|
| 528 |
} |
|
| 529 |
} |
|
| 530 | ||
| 531 | 92x |
ret |
| 532 |
} |
|
| 533 |
} |
|
| 534 | ||
| 535 |
#' @name keep_non_null_rows |
|
| 536 |
#' |
|
| 537 |
#' @title Pruning Function to accommodate removal of completely NULL rows within a table |
|
| 538 |
#' |
|
| 539 |
#' @description |
|
| 540 |
#' Condition function on individual analysis rows. Flag as FALSE when all |
|
| 541 |
#' columns are NULL, as then the row should not be kept. To be utilized as a |
|
| 542 |
#' row_condition in function tern::keep_rows |
|
| 543 |
#' |
|
| 544 |
#' @param tr (`TableTree`)\cr The TableTree object to prune. |
|
| 545 |
#' @export |
|
| 546 |
#' |
|
| 547 |
#' @examples |
|
| 548 |
#' library(dplyr) |
|
| 549 |
#' |
|
| 550 |
#' ADSL <- data.frame( |
|
| 551 |
#' USUBJID = c( |
|
| 552 |
#' "XXXXX01", "XXXXX02", "XXXXX03", "XXXXX04", "XXXXX05", |
|
| 553 |
#' "XXXXX06", "XXXXX07", "XXXXX08", "XXXXX09", "XXXXX10" |
|
| 554 |
#' ), |
|
| 555 |
#' TRT01P = c( |
|
| 556 |
#' "ARMA", "ARMB", "ARMA", "ARMB", "ARMB", "Placebo", |
|
| 557 |
#' "Placebo", "Placebo", "ARMA", "ARMB" |
|
| 558 |
#' ), |
|
| 559 |
#' AGE = c(34, 56, 75, 81, 45, 75, 48, 19, 32, 31), |
|
| 560 |
#' SAFFL = c("N", "N", "N", "N", "N", "N", "N", "N", "N", "N"),
|
|
| 561 |
#' PKFL = c("N", "N", "N", "N", "N", "N", "N", "N", "N", "N")
|
|
| 562 |
#' ) |
|
| 563 |
#' |
|
| 564 |
#' ADSL <- ADSL |> |
|
| 565 |
#' mutate(TRT01P = as.factor(TRT01P)) |
|
| 566 |
#' |
|
| 567 |
#' create_blank_line <- function(x) {
|
|
| 568 |
#' list( |
|
| 569 |
#' "Mean" = rcell(mean(x), format = "xx.x"), |
|
| 570 |
#' " " = rcell(NULL), |
|
| 571 |
#' "Max" = rcell(max(x)) |
|
| 572 |
#' ) |
|
| 573 |
#' } |
|
| 574 |
#' |
|
| 575 |
#' lyt <- basic_table() |> |
|
| 576 |
#' split_cols_by("TRT01P") |>
|
|
| 577 |
#' analyze("AGE", afun = create_blank_line)
|
|
| 578 |
#' |
|
| 579 |
#' result <- build_table(lyt, ADSL) |
|
| 580 |
#' |
|
| 581 |
#' result |
|
| 582 |
#' result <- prune_table(result, prune_func = tern::keep_rows(keep_non_null_rows)) |
|
| 583 |
#' |
|
| 584 |
#' result |
|
| 585 |
#' @rdname keep_non_null_rows |
|
| 586 |
#' @returns A function that can be utilized as a row_condition in the tern::keep_rows function. |
|
| 587 |
#' |
|
| 588 |
keep_non_null_rows <- function(tr) {
|
|
| 589 | 55x |
if (methods::is(tr, "DataRow")) {
|
| 590 | 54x |
r_cellvalue_null <- unlist(lapply(cell_values(tr), is.null)) |
| 591 | 54x |
check <- all(r_cellvalue_null) |
| 592 | ||
| 593 |
### if check TRUE (all cell values NULL -- need to return FALSE as need to remove) |
|
| 594 | 54x |
ret <- !check |
| 595 |
} else {
|
|
| 596 | 1x |
ret <- TRUE |
| 597 |
} |
|
| 598 | 55x |
ret |
| 599 |
} |
| 1 |
#' Conditional proportion with adaptive CI (exact vs. Wald) |
|
| 2 |
#' |
|
| 3 |
#' The analysis function [a_cond_proportion_j()] is used to create a layout |
|
| 4 |
#' element that estimates a response proportion with confidence interval, |
|
| 5 |
#' automatically selecting between exact and Wald methods based on observed |
|
| 6 |
#' counts and user-defined limits. |
|
| 7 |
#' |
|
| 8 |
#' @description `r lifecycle::badge("experimental")`
|
|
| 9 |
#' |
|
| 10 |
#' @details |
|
| 11 |
#' The statistics function mirrors [tern::s_proportion()] usage and output but |
|
| 12 |
#' removes the `method` argument and decides internally between |
|
| 13 |
#' "clopper-pearson" and "wald". |
|
| 14 |
#' |
|
| 15 |
#' The exact method is used when any of these are true: |
|
| 16 |
#' (a) the number of responders is `num_limit` (or less), |
|
| 17 |
#' (b) all subjects except `num_limit` (or less) have observed response, |
|
| 18 |
#' or (c) the observed group size is less than `denom_limit`. |
|
| 19 |
#' |
|
| 20 |
#' Depending on the `method_scope` choice, this decision is either taken |
|
| 21 |
#' based on the response data for an individual cell (`method_scope = "cell"`), |
|
| 22 |
#' or based on the response data for the entire row (`method_scope = "row"`): |
|
| 23 |
#' |
|
| 24 |
#' - With `method_scope = "cell"`, the counts used for the decision are: |
|
| 25 |
#' - numerator: the number of responders in the current cell; |
|
| 26 |
#' - denominator: the total number of observations in the cell (using `df`) |
|
| 27 |
#' if `denom = "n"`, or across the row (using `.df_row`) if `denom = "N_row"`. |
|
| 28 |
#' - With `method_scope = "row"`, the counts used for the decision are: |
|
| 29 |
#' - numerator: the total number of responders across the row (using `.df_row`); |
|
| 30 |
#' - denominator: the total number of observations across the row (from `.df_row`). |
|
| 31 |
#' |
|
| 32 |
#' CI computation follows [tern::s_proportion()] conventions: helper functions |
|
| 33 |
#' are called with `n = denom`, so when `denom = "N_row"`, this row-level |
|
| 34 |
#' denominator is used for the interval. |
|
| 35 |
#' |
|
| 36 |
#' @inheritParams proposal_argument_convention |
|
| 37 |
#' @param df (`logical` or `data.frame`)\cr if only a logical vector is used, |
|
| 38 |
#' it indicates whether each subject is a responder or not. `TRUE` represents |
|
| 39 |
#' a successful outcome. If a `data.frame` is provided, the logical vector of |
|
| 40 |
#' responses must be indicated as a variable name in `.var`. |
|
| 41 |
#' @param .var (`string`) |
|
| 42 |
#' @param conf_level (`numeric`) |
|
| 43 |
#' @param denom (`character`)\cr denominator to use for percentage and CI computation: |
|
| 44 |
#' "n" (default, number of observed records), or "N_row" (number of observations in the row). |
|
| 45 |
#' @param long (`flag`)\cr whether a long description is required. |
|
| 46 |
#' @param na.rm (`flag`)\cr whether `NA` responses should be removed before analysis. |
|
| 47 |
#' If `FALSE` and `NA` values are present, an error is raised. |
|
| 48 |
#' Note that missing values are also removed from the row-wise counts |
|
| 49 |
#' which is relevant when `method_scope = "row"` or `denom = "N_row"`. |
|
| 50 |
#' @param num_limit (`int`)\cr numerator limit to trigger the exact method. |
|
| 51 |
#' @param denom_limit (`int`)\cr denominator limit to trigger the exact method. |
|
| 52 |
#' @param method_scope (`string`)\cr select the CI method using counts from the |
|
| 53 |
#' current cell (`"cell"`) or all columns in the current row (`"row"`). See details. |
|
| 54 |
#' @param .df_row (`data.frame`)\cr data for the current row across all columns, |
|
| 55 |
#' automatically supplied by `rtables`. |
|
| 56 |
#' @param method (`string` or `NULL`)\cr selected CI method to show in the |
|
| 57 |
#' label. `NULL` retains the combined method description. |
|
| 58 |
#' @param method_denom (`int`)\cr denominator used for the selected CI method. |
|
| 59 |
#' @param method_rsp (`int`)\cr numerator used for the selected CI method. |
|
| 60 |
#' |
|
| 61 |
#' @name cond_proportion_j |
|
| 62 |
NULL |
|
| 63 | ||
| 64 |
#' Helper Function to Extract Count Responders |
|
| 65 |
#' |
|
| 66 |
#' Converts a response vector, or a response column from a data frame, to logical |
|
| 67 |
#' values and returns its responder and observation counts. |
|
| 68 |
#' |
|
| 69 |
#' @param x (`vector` or `data.frame`)\cr Response data. |
|
| 70 |
#' @param .var (`string`)\cr Response column in `x` when `x` is a data frame. |
|
| 71 |
#' @param na.rm (`flag`)\cr Whether to remove missing responses before counting. |
|
| 72 |
#' |
|
| 73 |
#' @return A named list with `rsp` (logical response vector), `n_rsp` (responders) and `len_rsp` |
|
| 74 |
#' (non-missing observations, when `na.rm = TRUE`). |
|
| 75 |
#' |
|
| 76 |
#' @keywords internal |
|
| 77 |
h_get_rsp_counts <- function(x, .var, na.rm = FALSE) {
|
|
| 78 | 53x |
rsp <- if (checkmate::test_atomic_vector(x)) {
|
| 79 | 20x |
x |
| 80 |
} else {
|
|
| 81 | 33x |
tern::assert_df_with_variables(x, list(rsp = .var)) |
| 82 | 32x |
x[[.var]] |
| 83 |
} |
|
| 84 | 52x |
rsp <- safe_as_logical(rsp, na.rm = na.rm) |
| 85 | 49x |
list(rsp = rsp, n_rsp = sum(rsp), len_rsp = length(rsp)) |
| 86 |
} |
|
| 87 | ||
| 88 |
#' @describeIn cond_proportion_j Statistics function estimating a proportion |
|
| 89 |
#' along with its confidence interval, with adaptive method selection. |
|
| 90 |
#' |
|
| 91 |
#' @return |
|
| 92 |
#' * `s_cond_proportion_j()` returns statistics `n_prop` (`n` responders and proportion) |
|
| 93 |
#' and `prop_ci` (proportion CI), formatted consistently with [tern::s_proportion()]. |
|
| 94 |
#' |
|
| 95 |
#' @examples |
|
| 96 |
#' # Logical vector input |
|
| 97 |
#' rsp_v <- c(TRUE, FALSE, TRUE, TRUE, FALSE, TRUE, FALSE, FALSE) |
|
| 98 |
#' s_cond_proportion_j(rsp_v) |
|
| 99 |
#' |
|
| 100 |
#' # Data frame input |
|
| 101 |
#' dta <- data.frame(rsp = c(TRUE, TRUE, FALSE, TRUE, FALSE, NA)) |
|
| 102 |
#' s_cond_proportion_j(dta, .var = "rsp", na.rm = TRUE) |
|
| 103 |
#' |
|
| 104 |
#' # Using method_scope = "row" (requires .df_row in ...) |
|
| 105 |
#' df_row <- data.frame(rsp = rep(rsp_v, 2)) |
|
| 106 |
#' s_cond_proportion_j( |
|
| 107 |
#' dta, .var = "rsp", method_scope = "row", |
|
| 108 |
#' .df_row = df_row, na.rm = TRUE |
|
| 109 |
#' ) |
|
| 110 |
#' |
|
| 111 |
#' @export |
|
| 112 |
s_cond_proportion_j <- function( |
|
| 113 |
df, |
|
| 114 |
.var, |
|
| 115 |
conf_level = 0.95, |
|
| 116 |
long = FALSE, |
|
| 117 |
na.rm = FALSE, |
|
| 118 |
num_limit = 0, |
|
| 119 |
denom_limit = 10, |
|
| 120 |
denom = c("n", "N_row"),
|
|
| 121 |
method_scope = c("cell", "row"),
|
|
| 122 |
.df_row = NULL |
|
| 123 |
) {
|
|
| 124 | 38x |
checkmate::assert_flag(long) |
| 125 | 38x |
checkmate::assert_flag(na.rm) |
| 126 | 38x |
assert_proportion_value(conf_level) |
| 127 | 38x |
checkmate::assert_int(num_limit, lower = 0) |
| 128 | 38x |
checkmate::assert_int(denom_limit, lower = 0) |
| 129 | 38x |
denom <- match.arg(denom) |
| 130 | 38x |
method_scope <- match.arg(method_scope) |
| 131 | ||
| 132 | 38x |
rsp_counts <- h_get_rsp_counts(df, .var, na.rm = na.rm) |
| 133 | 36x |
rsp <- rsp_counts[["rsp"]] |
| 134 | 36x |
n_obs <- rsp_counts[["len_rsp"]] |
| 135 | 36x |
n_rsp <- rsp_counts[["n_rsp"]] |
| 136 | ||
| 137 | 36x |
if (method_scope == "row" || denom == "N_row") {
|
| 138 | 15x |
row_rsp_counts <- h_get_rsp_counts(.df_row, .var, na.rm = na.rm) |
| 139 |
} |
|
| 140 | 34x |
denom_val <- switch( |
| 141 | 34x |
denom, |
| 142 | 34x |
n = n_obs, |
| 143 | 34x |
N_row = row_rsp_counts[["len_rsp"]] |
| 144 |
) |
|
| 145 | 34x |
assert_int(denom_val, lower = n_obs) |
| 146 | 34x |
p_hat <- ifelse(denom_val > 0, n_rsp / denom_val, 0) |
| 147 | ||
| 148 |
# Define the numerator and denominator used for the CI method selection. |
|
| 149 | 34x |
if (method_scope == "row") {
|
| 150 | 9x |
method_denom <- row_rsp_counts[["len_rsp"]] |
| 151 | 9x |
method_rsp <- row_rsp_counts[["n_rsp"]] |
| 152 |
} else {
|
|
| 153 | 25x |
method_denom <- denom_val |
| 154 | 25x |
method_rsp <- n_rsp |
| 155 |
} |
|
| 156 | ||
| 157 | 34x |
use_exact <- (method_denom < denom_limit) || |
| 158 | 34x |
(method_rsp <= num_limit) || |
| 159 | 34x |
(method_rsp >= (method_denom - num_limit)) |
| 160 | 34x |
method <- if (use_exact) "clopper-pearson" else "wald" |
| 161 | ||
| 162 | 34x |
prop_ci <- switch( |
| 163 | 34x |
method, |
| 164 | 34x |
"clopper-pearson" = prop_clopper_pearson(rsp, n = denom_val, conf_level), |
| 165 | 34x |
"wald" = prop_wald(rsp, n = denom_val, conf_level) |
| 166 |
) |
|
| 167 | ||
| 168 | 32x |
label <- d_cond_proportion_j( |
| 169 | 32x |
conf_level, |
| 170 | 32x |
long = long, |
| 171 | 32x |
num_limit = num_limit, |
| 172 | 32x |
denom_limit = denom_limit, |
| 173 | 32x |
method = if (method_scope == "row") method else NULL, |
| 174 | 32x |
method_denom = if (method_scope == "row") method_denom else NULL, |
| 175 | 32x |
method_rsp = if (method_scope == "row") method_rsp else NULL |
| 176 |
) |
|
| 177 | ||
| 178 | 32x |
list( |
| 179 | 32x |
"n_prop" = formatters::with_label(c(n_rsp, p_hat), "Responders"), |
| 180 | 32x |
"prop_ci" = formatters::with_label( |
| 181 | 32x |
x = 100 * prop_ci, |
| 182 | 32x |
label = label |
| 183 |
) |
|
| 184 |
) |
|
| 185 |
} |
|
| 186 | ||
| 187 |
#' @describeIn cond_proportion_j Description helper function for the conditional |
|
| 188 |
#' proportion summary label. |
|
| 189 |
#' |
|
| 190 |
#' @return |
|
| 191 |
#' * `d_cond_proportion_j()` returns a string describing the analysis label. |
|
| 192 |
#' |
|
| 193 |
#' @examples |
|
| 194 |
#' d_cond_proportion_j(conf_level = 0.90, long = FALSE, num_limit = 0, denom_limit = 10) |
|
| 195 |
#' |
|
| 196 |
#' # With dedicated row-wise method: |
|
| 197 |
#' d_cond_proportion_j( |
|
| 198 |
#' conf_level = 0.90, long = TRUE, num_limit = 0, denom_limit = 10, |
|
| 199 |
#' method = "wald", method_denom = 10, method_rsp = 8) |
|
| 200 |
#' |
|
| 201 |
#' @export |
|
| 202 |
d_cond_proportion_j <- function( |
|
| 203 |
conf_level, |
|
| 204 |
long = FALSE, |
|
| 205 |
num_limit = NULL, |
|
| 206 |
denom_limit = NULL, |
|
| 207 |
method = NULL, |
|
| 208 |
method_denom = NULL, |
|
| 209 |
method_rsp = NULL |
|
| 210 |
) {
|
|
| 211 | 40x |
assert_proportion_value(conf_level) |
| 212 | 40x |
assert_flag(long) |
| 213 | 40x |
assert_string(method, null.ok = TRUE) |
| 214 | ||
| 215 | 40x |
label <- paste0(conf_level * 100, "% CI") |
| 216 | ||
| 217 | 40x |
if (long) {
|
| 218 | 15x |
label <- paste(label, "for Response Rates") |
| 219 |
} |
|
| 220 | ||
| 221 | 40x |
method_part <- if (!is.null(method)) {
|
| 222 | 14x |
assert_choice(method, choices = c("wald", "clopper-pearson"))
|
| 223 | 14x |
assert_count(num_limit) |
| 224 | 14x |
assert_count(denom_limit) |
| 225 | 14x |
assert_count(method_denom) |
| 226 | 14x |
assert_count(method_rsp) |
| 227 | 14x |
reason <- if (method == "wald") {
|
| 228 | 5x |
paste0("n >= ", denom_limit, ", x = ", method_rsp)
|
| 229 | 14x |
} else if (method_denom < denom_limit) {
|
| 230 | 2x |
paste0("n < ", denom_limit)
|
| 231 | 14x |
} else if (method_rsp <= num_limit) {
|
| 232 | 2x |
if (num_limit == 0) "x = 0" else paste0("x <= ", num_limit)
|
| 233 | 14x |
} else if (method_rsp >= (method_denom - num_limit)) {
|
| 234 | 2x |
if (num_limit == 0) "x = n" else paste0("x >= n - ", num_limit)
|
| 235 |
} else {
|
|
| 236 | ! |
stop("Selected Clopper-Pearson method has no matching selection criterion.")
|
| 237 |
} |
|
| 238 | 14x |
if (long) {
|
| 239 | 40x |
switch( |
| 240 | 12x |
method, |
| 241 | 4x |
"wald" = paste0( |
| 242 | 4x |
"Wald because ", |
| 243 | 4x |
reason |
| 244 |
), |
|
| 245 | 8x |
"clopper-pearson" = paste0( |
| 246 | 8x |
"Clopper-Pearson because ", |
| 247 | 8x |
reason |
| 248 |
) |
|
| 249 |
) |
|
| 250 |
} else {
|
|
| 251 | 40x |
switch( |
| 252 | 2x |
method, |
| 253 | 1x |
"wald" = "Wald", |
| 254 | 1x |
"clopper-pearson" = "Clopper-Pearson" |
| 255 |
) |
|
| 256 |
} |
|
| 257 | 40x |
} else if (long) {
|
| 258 | 3x |
assert_count(num_limit) |
| 259 | 3x |
assert_count(denom_limit, positive = TRUE) |
| 260 | 3x |
paste0( |
| 261 | 3x |
"Wald if n >= ", |
| 262 | 3x |
denom_limit, |
| 263 | 3x |
", x > ", |
| 264 | 3x |
num_limit, |
| 265 | 3x |
", x < n - ", |
| 266 | 3x |
num_limit, |
| 267 | 3x |
"; else Clopper-Pearson" |
| 268 |
) |
|
| 269 |
} else {
|
|
| 270 | 23x |
"Wald / Clopper-Pearson" |
| 271 |
} |
|
| 272 | ||
| 273 | 40x |
paste0(label, " (", method_part, ")")
|
| 274 |
} |
|
| 275 | ||
| 276 |
#' @describeIn cond_proportion_j Formatted analysis function which is used as |
|
| 277 |
#' `afun` for conditional proportion with adaptive CI selection (exact vs. Wald). |
|
| 278 |
#' |
|
| 279 |
#' @return |
|
| 280 |
#' * `a_cond_proportion_j()` returns the corresponding list with formatted [rtables::CellValue()]. |
|
| 281 |
#' |
|
| 282 |
#' @examples |
|
| 283 |
#' nex <- 100 |
|
| 284 |
#' dta <- data.frame( |
|
| 285 |
#' "rsp" = sample(c(TRUE, FALSE), nex, TRUE), |
|
| 286 |
#' "grp" = sample(c("A", "B"), nex, TRUE),
|
|
| 287 |
#' "f1" = sample(c("a1", "a2"), nex, TRUE),
|
|
| 288 |
#' stringsAsFactors = TRUE |
|
| 289 |
#' ) |
|
| 290 |
#' |
|
| 291 |
#' l <- basic_table() |> |
|
| 292 |
#' split_cols_by(var = "grp") |> |
|
| 293 |
#' analyze( |
|
| 294 |
#' vars = "rsp", |
|
| 295 |
#' afun = a_cond_proportion_j, |
|
| 296 |
#' extra_args = list( |
|
| 297 |
#' conf_level = 0.90, |
|
| 298 |
#' num_limit = 0, |
|
| 299 |
#' denom_limit = 10 |
|
| 300 |
#' ) |
|
| 301 |
#' ) |
|
| 302 |
#' |
|
| 303 |
#' build_table(l, df = dta) |
|
| 304 |
#' |
|
| 305 |
#' @export |
|
| 306 |
#' @order 2 |
|
| 307 |
a_cond_proportion_j <- function( |
|
| 308 |
df, |
|
| 309 |
.var, |
|
| 310 |
..., |
|
| 311 |
.stats = NULL, |
|
| 312 |
.formats = NULL, |
|
| 313 |
.labels = NULL, |
|
| 314 |
.indent_mods = NULL, |
|
| 315 |
.df_row = NULL |
|
| 316 |
) {
|
|
| 317 | 10x |
dots_extra_args <- list(...) |
| 318 | ||
| 319 |
# Only support default stats, not custom stats |
|
| 320 | 10x |
.stats <- .split_std_from_custom_stats(.stats)$default_stats |
| 321 | ||
| 322 | 10x |
x_stats <- .apply_stat_functions( |
| 323 | 10x |
default_stat_fnc = s_cond_proportion_j, |
| 324 | 10x |
custom_stat_fnc_list = NULL, |
| 325 | 10x |
args_list = c( |
| 326 | 10x |
df = list(df), |
| 327 | 10x |
.var = .var, |
| 328 | 10x |
.df_row = list(.df_row), |
| 329 | 10x |
dots_extra_args |
| 330 |
) |
|
| 331 |
) |
|
| 332 | ||
| 333 | 10x |
format_stats( |
| 334 | 10x |
x_stats, |
| 335 | 10x |
method_groups = "estimate_proportion", |
| 336 | 10x |
stats_in = .stats, |
| 337 | 10x |
formats_in = .formats, |
| 338 | 10x |
labels_in = .labels, |
| 339 | 10x |
indents_in = .indent_mods |
| 340 |
) |
|
| 341 |
} |
| 1 |
## contribute to formatters |
|
| 2 |
#' @name inches_to_spaces |
|
| 3 |
#' @title Conversion of inches to spaces. |
|
| 4 |
#' @param ins (`numeric`)\cr Vector of widths in inches. |
|
| 5 |
#' @param fontspec (`font_spec`)\cr The font specification to use. |
|
| 6 |
#' @param raw (`logical(1)`)\cr Should the answer be returned unrounded |
|
| 7 |
#' (`TRUE`), or rounded to the nearest reasonable value (`FALSE`, |
|
| 8 |
#' the default). |
|
| 9 |
#' @param tol (`numeric(1)`)\cr The numeric tolerance. Values |
|
| 10 |
#' between an integer `n`, and `n+tol` will be returned |
|
| 11 |
#' as `n`, rather than `n+1`, if `raw == FALSE`. Ignored |
|
| 12 |
#' when `raw` is `TRUE`. |
|
| 13 |
#' @return The number of either fractional (`raw = TRUE`) or whole (`raw = FALSE`) |
|
| 14 |
#' spaces that will fit within `ins` inches in the specified font. |
|
| 15 |
#' @export |
|
| 16 |
inches_to_spaces <- function(ins, fontspec, raw = FALSE, tol = sqrt(.Machine$double.eps)) {
|
|
| 17 | 110x |
newdev <- open_font_dev(fontspec) |
| 18 | 110x |
if (newdev) {
|
| 19 | 8x |
on.exit(close_font_dev()) |
| 20 |
} |
|
| 21 | 110x |
spcw <- grid::convertWidth(grid::unit(1, "strwidth", " "), "inches", valueOnly = TRUE) |
| 22 | 110x |
ans <- ins / spcw |
| 23 | 110x |
if (!raw) {
|
| 24 | 106x |
win_tol <- ans - floor(ans) < tol |
| 25 | 106x |
ans[win_tol] <- floor(ans) |
| 26 | 106x |
ans[!win_tol] <- ceiling(ans) |
| 27 |
} |
|
| 28 | 110x |
ans |
| 29 |
} |
|
| 30 | ||
| 31 |
#' @importFrom formatters wrap_string_ttype |
|
| 32 |
ttype_wrap_vec <- function(vec, fontspec, width, wordbreak_ok = TRUE, ...) {
|
|
| 33 | 11x |
lapply( |
| 34 | 11x |
vec, |
| 35 | 11x |
wrap_string_ttype, |
| 36 | 11x |
width = width, |
| 37 | 11x |
fontspec = fontspec, |
| 38 | 11x |
wordbreak_ok = wordbreak_ok, |
| 39 |
... |
|
| 40 |
) |
|
| 41 |
} |
|
| 42 | ||
| 43 |
#' @name check_wrap_nobreak |
|
| 44 |
#' @title Check Word Wrapping |
|
| 45 |
#' @description Check a set of column widths for word-breaking wrap behavior. |
|
| 46 |
#' @param tt (`TableTree`)\cr TableTree object |
|
| 47 |
#' @param colwidths (`numeric`)\cr Column widths (in numbers of spaces under `fontspec`) |
|
| 48 |
#' @param fontspec (`font_spec`)\cr Font specification object |
|
| 49 |
#' |
|
| 50 |
#' @return `TRUE` if the wrap is able to be done without breaking words, |
|
| 51 |
#' `FALSE` if wordbreaking is required to apply `colwidths`. |
|
| 52 |
#' @rdname check_wrap_nobreak |
|
| 53 |
#' @export |
|
| 54 |
check_wrap_nobreak <- function(tt, colwidths, fontspec) {
|
|
| 55 | 2x |
newdev <- open_font_dev(fontspec) |
| 56 | 2x |
if (newdev) {
|
| 57 | 2x |
on.exit(close_font_dev()) |
| 58 |
} |
|
| 59 | 2x |
mpf <- matrix_form(tt, fontspec = fontspec) |
| 60 | 2x |
strs <- mf_strings(mpf) |
| 61 | 2x |
colok <- vapply( |
| 62 | 2x |
seq_len(ncol(strs)), |
| 63 | 2x |
function(i) {
|
| 64 | 10x |
res <- tryCatch( |
| 65 | 10x |
ttype_wrap_vec(strs[, i, drop = TRUE], colwidths[i], fontspec = fontspec, wordbreak_ok = FALSE), |
| 66 | 10x |
error = function(e) e |
| 67 |
) |
|
| 68 | 10x |
!methods::is(res, "error") |
| 69 |
}, |
|
| 70 | 2x |
TRUE |
| 71 |
) |
|
| 72 | 2x |
all(colok) |
| 73 |
} |
|
| 74 | ||
| 75 |
#' Colwidths for all columns to be forced on one page. |
|
| 76 |
#' |
|
| 77 |
#' @param tt (`TableTree`)\cr TableTree object to calculate column widths for |
|
| 78 |
#' @param fontspec (`font_spec`)\cr Font specification object |
|
| 79 |
#' @param col_gap (`numeric`)\cr Column gap in spaces |
|
| 80 |
#' @param rowlabel_width (`numeric`)\cr Width of row labels in spaces |
|
| 81 |
#' @param print_width_ins (`numeric`)\cr Print width in inches |
|
| 82 |
#' @param landscape (`logical`)\cr Whether the output is in landscape orientation |
|
| 83 |
#' @param lastcol_gap (`logical`)\cr Whether to include a gap after the last column |
|
| 84 |
#' @keywords internal |
|
| 85 |
smart_colwidths_1page <- function( |
|
| 86 |
tt, |
|
| 87 |
fontspec, |
|
| 88 |
col_gap = 6L, |
|
| 89 |
rowlabel_width = inches_to_spaces(2, fontspec), |
|
| 90 |
print_width_ins = ifelse(landscape, 11, 8.5) - 2.12, |
|
| 91 |
landscape = FALSE, |
|
| 92 |
lastcol_gap = TRUE) {
|
|
| 93 | 3x |
total_cpp <- floor(inches_to_spaces(print_width_ins, fontspec = fontspec, raw = TRUE)) |
| 94 | ||
| 95 | 3x |
nc <- ncol(tt) |
| 96 | 3x |
remain <- total_cpp - rowlabel_width - col_gap * (nc - !lastcol_gap) |
| 97 | ||
| 98 | 3x |
c(rowlabel_width - col_gap, spread_integer(remain, nc)) |
| 99 |
} |
|
| 100 | ||
| 101 |
spaces_to_inches <- function(spcs, fontspec) {
|
|
| 102 | 1x |
nchar_ttype(" ", fontspec, raw = TRUE) * spcs
|
| 103 |
} |
|
| 104 | ||
| 105 |
no_cellwrap_colwidths <- function(tt, fontspec, col_gap = 4L, label_width_ins = 2) {
|
|
| 106 | 26x |
if (is.null(col_gap)) {
|
| 107 | ! |
col_gap <- 4L |
| 108 |
} |
|
| 109 | 26x |
mpf <- matrix_form(tt, TRUE, FALSE, fontspec = fontspec, col_gap = col_gap) |
| 110 | 26x |
strmat <- mf_strings(mpf) |
| 111 | 26x |
label_width_max <- inches_to_spaces(label_width_ins, fontspec) |
| 112 | ||
| 113 | 26x |
nchar_mat <- nchar_ttype(strmat[-seq_len(mf_nlheader(mpf)), , drop = FALSE], |
| 114 | 26x |
fontspec = fontspec |
| 115 |
) |
|
| 116 | ||
| 117 | 26x |
label_width <- min( |
| 118 | 26x |
label_width_max, |
| 119 | 26x |
max(nchar_mat[, 1, drop = TRUE]) |
| 120 |
) |
|
| 121 | 26x |
col_maxes <- apply(nchar_mat[, -1, drop = FALSE], 2, max) |
| 122 | 26x |
c(label_width, col_maxes + col_gap) |
| 123 |
} |
|
| 124 | ||
| 125 |
pack_into_lines2 <- function(strs, wrdwidths = nchar_ttype(strs), colwidth, fontspec) {
|
|
| 126 | 17526x |
if (length(wrdwidths) == 0 || (length(wrdwidths) == 1 && wrdwidths <= colwidth)) {
|
| 127 | 5585x |
return(1) |
| 128 |
} |
|
| 129 | 11941x |
newdev <- open_font_dev(fontspec) |
| 130 | 11941x |
if (newdev) {
|
| 131 | ! |
on.exit(close_font_dev()) |
| 132 |
} |
|
| 133 | ||
| 134 | 11941x |
csums <- cumsum(wrdwidths + c(0, rep(1, length(wrdwidths) - 1))) |
| 135 | 11941x |
oneline_wdth <- sum(wrdwidths) + length(wrdwidths) - 1 |
| 136 | 11941x |
if (colwidth >= ceiling(oneline_wdth)) {
|
| 137 | 8818x |
return(1) |
| 138 |
} |
|
| 139 | 3123x |
lines <- 0 |
| 140 | 3123x |
wrdsused <- 0 |
| 141 | 3123x |
widthused <- 0 |
| 142 | 3123x |
index_seq <- seq_along(wrdwidths) |
| 143 | 3123x |
totwrds <- length(wrdwidths) |
| 144 | 3123x |
adj <- 0 |
| 145 |
## finite precision arithmetic is a dreamscape of infinite wonder |
|
| 146 | 3123x |
tol <- sqrt(.Machine$double.eps) |
| 147 | 3123x |
while (wrdsused < length(wrdwidths)) {
|
| 148 | 7428x |
csums_i <- csums - widthused |
| 149 | ||
| 150 | 7428x |
fit <- which(index_seq > wrdsused & csums_i <= colwidth + tol) |
| 151 | 7428x |
widthused <- sum(widthused, wrdwidths[fit], length(fit)) |
| 152 | 7428x |
wrdsused <- wrdsused + length(fit) |
| 153 | 7428x |
lines <- lines + 1 |
| 154 |
} |
|
| 155 | 3123x |
lines |
| 156 |
} |
|
| 157 | ||
| 158 |
recursive_add_poss <- function( |
|
| 159 |
wlst, |
|
| 160 |
cur_lst, |
|
| 161 |
ubnd_width, |
|
| 162 |
lbnd_width, |
|
| 163 |
ubnd_lines = calc_total_lns(wlst, fontspec = fontspec, colwidth = ubnd_width)$lines, |
|
| 164 |
lbnd_lines = calc_total_lns(wlst, fontspec = fontspec, colwidth = lbnd_width)$lines, |
|
| 165 |
fontspec) {
|
|
| 166 | 459x |
if (ubnd_width <= lbnd_width + 1 + sqrt(.Machine$double.eps)) {
|
| 167 | 109x |
return(cur_lst) |
| 168 |
} |
|
| 169 | 350x |
curw <- floor((ubnd_width + lbnd_width) / 2) |
| 170 | 350x |
curlnsdf <- calc_total_lns(wlst, fontspec = fontspec, colwidth = curw) |
| 171 | 350x |
to_add <- list(curlnsdf) |
| 172 | ||
| 173 | 350x |
if (curlnsdf$lines != lbnd_lines) {
|
| 174 | 227x |
to_add <- c( |
| 175 | 227x |
to_add, |
| 176 | 227x |
recursive_add_poss( |
| 177 | 227x |
wlst = wlst, |
| 178 | 227x |
fontspec = fontspec, |
| 179 | 227x |
ubnd_width = curw, |
| 180 | 227x |
ubnd_lines = curlnsdf$lines, |
| 181 | 227x |
lbnd_width = lbnd_width, |
| 182 | 227x |
lbnd_lines = lbnd_lines, |
| 183 | 227x |
cur_lst = list() |
| 184 |
) |
|
| 185 |
) |
|
| 186 |
} |
|
| 187 | 350x |
if (curlnsdf$lines != ubnd_lines) {
|
| 188 | 186x |
to_add <- c( |
| 189 | 186x |
to_add, |
| 190 | 186x |
recursive_add_poss( |
| 191 | 186x |
wlst = wlst, |
| 192 | 186x |
fontspec = fontspec, |
| 193 | 186x |
ubnd_width = ubnd_width, |
| 194 | 186x |
ubnd_lines = ubnd_lines, |
| 195 | 186x |
lbnd_width = curw, |
| 196 | 186x |
lbnd_lines = curlnsdf$lines, |
| 197 | 186x |
cur_lst = list() |
| 198 |
) |
|
| 199 |
) |
|
| 200 |
} |
|
| 201 | ||
| 202 | 350x |
c(cur_lst, to_add) |
| 203 |
} |
|
| 204 | ||
| 205 |
calc_total_lns <- function(wlst, colwidth, fontspec, lns_per_pg = 50) {
|
|
| 206 | 534x |
lns <- vapply( |
| 207 | 534x |
seq_along(wlst), |
| 208 | 534x |
function(i) {
|
| 209 | 17526x |
pack_into_lines2(wrdwidths = wlst[[i]], colwidth = colwidth, fontspec = fontspec) |
| 210 |
}, |
|
| 211 | 534x |
1 |
| 212 |
) |
|
| 213 | 534x |
nonblnks <- !vapply(wlst, function(x) length(x) == 0, TRUE) |
| 214 | 534x |
celllns <- lns[-1] |
| 215 | 534x |
cllsum <- sum(celllns) |
| 216 | 534x |
hdr <- lns[1] |
| 217 | 534x |
data.frame( |
| 218 | 534x |
colwidth = colwidth, |
| 219 | 534x |
lines = cllsum + ceiling(cllsum / lns_per_pg) * hdr, |
| 220 | 534x |
cell_lines = cllsum, |
| 221 | 534x |
lbl_lines = hdr, |
| 222 | 534x |
min_cell_lines = min(1, celllns[nonblnks], na.rm = TRUE), |
| 223 | 534x |
max_cell_lines = max(celllns) |
| 224 |
) |
|
| 225 |
} |
|
| 226 | ||
| 227 | ||
| 228 |
calc_poss_lines <- function(wlst, lbound, avail_spc, fontspec) {
|
|
| 229 | 46x |
minposs <- calc_total_lns(wlst, lbound + avail_spc, fontspec) |
| 230 | 46x |
maxposs <- calc_total_lns(wlst, lbound, fontspec) |
| 231 | ||
| 232 | 46x |
retlst <- list(minposs, maxposs) |
| 233 | ||
| 234 | 46x |
retlst <- recursive_add_poss( |
| 235 | 46x |
wlst = wlst, |
| 236 | 46x |
ubnd_width = lbound + avail_spc, |
| 237 | 46x |
lbnd_width = lbound, |
| 238 | 46x |
cur_lst = retlst, |
| 239 | 46x |
fontspec = fontspec |
| 240 |
) |
|
| 241 | 46x |
do.call(rbind, retlst) |
| 242 |
} |
|
| 243 | ||
| 244 | ||
| 245 |
make_poss_wdf <- function( |
|
| 246 |
mpf, |
|
| 247 |
incl_header = FALSE, |
|
| 248 |
col_gap = 3, |
|
| 249 |
pg_width_ins = 8.88, |
|
| 250 |
fontspec = font_spec("Times", 9, 1.2)) {
|
|
| 251 | 8x |
newdev <- open_font_dev(fontspec) |
| 252 | 8x |
if (newdev) {
|
| 253 | 1x |
on.exit(close_font_dev()) |
| 254 |
} |
|
| 255 | 8x |
if (!methods::is(mpf, "MatrixPrintForm")) {
|
| 256 | 6x |
mpf <- matrix_form(mpf, fontspec = fontspec, col_gap = col_gap) |
| 257 |
} |
|
| 258 | 8x |
strs <- mf_strings(mpf) |
| 259 | 8x |
if (!incl_header) {
|
| 260 | 1x |
strs <- strs[-seq_len(mf_nlheader(mpf)), ] |
| 261 |
} |
|
| 262 | 8x |
nc <- ncol(strs) |
| 263 | 8x |
nr <- nrow(strs) |
| 264 |
## strip out markup so we are not over countin (by as much) |
|
| 265 | 8x |
strs <- matrix(gsub("~[{[][[:alpha:]]+ ([^]}]+)[]}]", "\\1", strs), ncol = nc, nrow = nr)
|
| 266 | ||
| 267 | 8x |
possrows <- lapply(seq_len(ncol(strs)), function(ii) {
|
| 268 | 46x |
res <- one_col_strs(strs[, ii, drop = TRUE], fontspec = fontspec, col_gap = col_gap) |
| 269 | 46x |
res$col_num <- ii |
| 270 | 46x |
res |
| 271 |
}) |
|
| 272 | ||
| 273 | 8x |
possdf <- do.call(rbind, possrows) |
| 274 | 8x |
o <- order(possdf$col_num, possdf$colwidth) |
| 275 | 8x |
possdf <- possdf[o, ] |
| 276 | 8x |
possdf |
| 277 |
} |
|
| 278 | ||
| 279 |
#' |
|
| 280 |
#' @param mpf (`listing_df` or `MatrixPrintForm` derived thereof)\cr The listing |
|
| 281 |
#' calculate column widths for. |
|
| 282 |
#' @param incl_header (`logical(1)`)\cr Should the constraint to not break up |
|
| 283 |
#' individual words be extended to words in the column labels? Defaults to `TRUE` |
|
| 284 |
#' @param col_gap (`numeric(1)`)\cr Amount of extra space (in spaces) to |
|
| 285 |
#' assume between columns. Defaults to `0.5` |
|
| 286 |
#' @param pg_width_ins (`numeric(1)`)\cr Number of inches in width for |
|
| 287 |
#' *the portion of the page the listing will be printed to*. Defaults to `8.88` |
|
| 288 |
#' which corresponds to landscape orientation on a standard page after margins. |
|
| 289 |
#' @param fontspec (`font_spec`)\cr Defaults to Times New Roman 8pt font with 1.2 line |
|
| 290 |
#' height. |
|
| 291 |
#' @param verbose (`logical(1)`)\cr Should additional information messages be |
|
| 292 |
#' displayed during the calculation of the column widths? Defaults to `FALSE`. |
|
| 293 |
#' @return |
|
| 294 |
#' * `listing_column_widths`: a vector of column widths suitable to use in `tt_to_tlgrtf` and |
|
| 295 |
#' other exporters. |
|
| 296 |
#' @rdname def_colwidths |
|
| 297 |
#' @export |
|
| 298 |
listing_column_widths <- function( |
|
| 299 |
mpf, |
|
| 300 |
incl_header = TRUE, |
|
| 301 |
col_gap = 0.5, |
|
| 302 |
pg_width_ins = 8.88, |
|
| 303 |
fontspec = font_spec("Times", 8, 1.2),
|
|
| 304 |
verbose = FALSE) {
|
|
| 305 | 7x |
newdev <- open_font_dev(fontspec) |
| 306 | 7x |
if (newdev) {
|
| 307 | 3x |
on.exit(close_font_dev()) |
| 308 |
} |
|
| 309 | 7x |
possdf <- make_poss_wdf( |
| 310 | 7x |
mpf = mpf, |
| 311 | 7x |
incl_header = incl_header, |
| 312 | 7x |
col_gap = col_gap, |
| 313 | 7x |
fontspec = fontspec, |
| 314 | 7x |
pg_width_ins = pg_width_ins |
| 315 |
) |
|
| 316 | 7x |
optimal <- optimal_widths( |
| 317 | 7x |
possdf = possdf, |
| 318 | 7x |
tot_spaces = inches_to_spaces(pg_width_ins, fontspec = fontspec), |
| 319 | 7x |
verbose = verbose |
| 320 |
) |
|
| 321 | 7x |
optimal$colwidth |
| 322 |
} |
|
| 323 | ||
| 324 |
find_free_colspc <- function(curposs, fullposs, thresh = 0.99, skip = integer(), verbose = FALSE) {
|
|
| 325 | 1x |
orig_curposs <- curposs |
| 326 | 1x |
maxlns_ind <- which.max(curposs$cell_lines) |
| 327 | 1x |
longestcol <- curposs$col_num[maxlns_ind] |
| 328 | 1x |
maxlns <- curposs$cell_lines[maxlns_ind] |
| 329 | 1x |
adjrow_inds <- setdiff(which(curposs$cell_lines < thresh * maxlns), skip) |
| 330 | 1x |
for (arowi in adjrow_inds) {
|
| 331 | 2x |
col <- curposs$col_num[arowi] |
| 332 | 2x |
colwidthii <- curposs$colwidth[arowi] |
| 333 | 2x |
fp_ind <- with(fullposs, min(which(col_num == col & cell_lines <= maxlns & colwidth < colwidthii))) |
| 334 | 2x |
if (is.finite(fp_ind)) {
|
| 335 | ! |
if (verbose) {
|
| 336 | ! |
oldwdth <- curposs$colwidth[arowi] |
| 337 | ! |
newwdth <- fullposs$colwidth[fp_ind] |
| 338 | ! |
msg <- sprintf("adjusting column %d width from %d to %d", col, oldwdth, newwdth)
|
| 339 | ! |
message(msg) |
| 340 |
} |
|
| 341 | ! |
curposs[arowi, ] <- fullposs[fp_ind, ] |
| 342 |
} |
|
| 343 |
} |
|
| 344 | 1x |
curposs |
| 345 |
} |
|
| 346 | ||
| 347 |
constrict_lbl_lns <- function(curdf, possdf, avail_spc = 0, verbose = TRUE) {
|
|
| 348 | 7x |
old_lbl_lns <- max(curdf$lbl_lines) |
| 349 | 7x |
cols_to_pack <- which(curdf$lbl_lines == old_lbl_lns) |
| 350 | 7x |
olddf <- curdf |
| 351 | 7x |
success <- TRUE |
| 352 | 7x |
for (ii in cols_to_pack) {
|
| 353 | 7x |
colii <- curdf$col_num[ii] |
| 354 | 7x |
cwidthii <- curdf$colwidth[ii] |
| 355 | 7x |
possdfii <- possdf[possdf$col_num == colii & possdf$lbl_lines < old_lbl_lns, ] |
| 356 | 7x |
if (nrow(possdfii) == 0) {
|
| 357 | 2x |
success <- FALSE |
| 358 | 2x |
break |
| 359 |
} |
|
| 360 | 5x |
newrow <- possdfii[ii, ] |
| 361 | 5x |
if (ii > nrow(possdfii)) {
|
| 362 | 1x |
success <- FALSE |
| 363 | 1x |
break |
| 364 |
} |
|
| 365 | 4x |
if (newrow$colwidth - cwidthii > avail_spc) {
|
| 366 | 4x |
success <- FALSE |
| 367 | 4x |
break |
| 368 |
} |
|
| 369 |
## assuming sorted |
|
| 370 | ! |
curdf[ii, ] <- newrow |
| 371 | ! |
avail_spc <- avail_spc + cwidthii - newrow$colwidth |
| 372 |
} |
|
| 373 | ||
| 374 | 7x |
if (verbose) {
|
| 375 | 1x |
if (success) {
|
| 376 | ! |
msg <- paste( |
| 377 | ! |
"overall number of label rows successfully reduced. Cols affected: ", |
| 378 | ! |
paste(curdf$col_num[cols_to_pack], collapse = ", ") |
| 379 |
) |
|
| 380 |
} else {
|
|
| 381 | 1x |
msg <- paste("Unable to reduce label rows required.")
|
| 382 |
} |
|
| 383 |
} |
|
| 384 | ||
| 385 | 7x |
if (!success) {
|
| 386 | 7x |
curdf <- olddf |
| 387 |
} |
|
| 388 | 7x |
curdf |
| 389 |
} |
|
| 390 | ||
| 391 |
optimal_widths <- function(possdf, tot_spaces = 320, max_lbl_lines = 3, verbose = FALSE) {
|
|
| 392 | 7x |
odf <- order(possdf$col_num, possdf$colwidth) |
| 393 | 7x |
possdf <- possdf[odf, ] |
| 394 | 7x |
badlbl <- which(possdf$lbl_lines > max_lbl_lines) |
| 395 | 7x |
if (length(badlbl > 0)) {
|
| 396 | 2x |
if (verbose) {
|
| 397 | ! |
message("Excluding ", length(badlbl), " column widths for labels taking over ", max_lbl_lines, " lines.")
|
| 398 |
} |
|
| 399 | 2x |
possdf <- possdf[-badlbl, ] |
| 400 | 2x |
possdf <- possdf[possdf$lbl_lines <= max_lbl_lines, , drop = FALSE] |
| 401 |
} |
|
| 402 | 7x |
full_possdf <- possdf |
| 403 |
## already ordered by colnum then width so this the first of each colwidth is the min width for that col |
|
| 404 | 7x |
dups <- duplicated(possdf$col_num) |
| 405 | 7x |
curdf <- possdf[!dups, ] |
| 406 | 7x |
possdf <- possdf[dups, ] ## without rows for ones in curdf |
| 407 | 7x |
spcleft <- tot_spaces - sum(curdf$colwidth) |
| 408 | 7x |
if (verbose) {
|
| 409 | 1x |
message( |
| 410 | 1x |
"Optimizng Column Widths\n", |
| 411 | 1x |
"Initial lines required: ", |
| 412 | 1x |
max(curdf$lines), |
| 413 | 1x |
"\n", |
| 414 | 1x |
"Available adjustment: ", |
| 415 | 1x |
spcleft, |
| 416 | 1x |
" spaces\n" |
| 417 |
) |
|
| 418 |
} |
|
| 419 | 7x |
done <- FALSE |
| 420 | 7x |
while (!done) {
|
| 421 | 7x |
oldwdths <- curdf$colwidth |
| 422 | 7x |
curdf <- constrict_lbl_lns(curdf, possdf, verbose = verbose) |
| 423 | 7x |
if (all.equal(curdf$colwidth, oldwdths)) {
|
| 424 | 7x |
done <- TRUE |
| 425 |
} |
|
| 426 |
} |
|
| 427 | 7x |
change <- TRUE |
| 428 | 7x |
while (spcleft > 0 && change && nrow(possdf) > 0) {
|
| 429 | 24x |
change <- FALSE |
| 430 | 24x |
ii <- which.max(curdf$cell_lines) |
| 431 | 24x |
spcleft <- tot_spaces - sum(curdf$colwidth) |
| 432 | 24x |
colii <- curdf$col_num[ii] |
| 433 | 24x |
bef_lns <- curdf$cell_lines[ii] |
| 434 | 24x |
bef_width <- curdf$colwidth[ii] |
| 435 | 24x |
nextlns <- max(curdf$cell_lines[-ii]) |
| 436 | 24x |
cand_row_cond <- possdf$col_num == colii & possdf$cell_lines < bef_lns & possdf$colwidth - bef_width <= spcleft |
| 437 | 24x |
canddf <- possdf[cand_row_cond, , drop = FALSE] |
| 438 | 24x |
if (nrow(canddf) > 0) {
|
| 439 | 18x |
more_than_next <- canddf$cell_lines >= nextlns |
| 440 | 18x |
if (any(more_than_next)) {
|
| 441 | 9x |
candrow <- canddf[max(which(more_than_next)), ] |
| 442 |
} else {
|
|
| 443 | 9x |
candrow <- canddf[nrow(canddf), ] |
| 444 |
} |
|
| 445 | ||
| 446 | 18x |
if (verbose) {
|
| 447 | 4x |
message( |
| 448 | 4x |
"COL ", |
| 449 | 4x |
colii, |
| 450 | 4x |
" width: ", |
| 451 | 4x |
bef_width, |
| 452 |
"->", |
|
| 453 | 4x |
candrow$colwidth, |
| 454 | 4x |
" lines req: ", |
| 455 | 4x |
bef_lns, |
| 456 |
"->", |
|
| 457 | 4x |
candrow$cell_lines |
| 458 |
) |
|
| 459 |
} |
|
| 460 | 18x |
change <- TRUE |
| 461 | 18x |
curdf[ii, ] <- candrow |
| 462 |
} |
|
| 463 |
} |
|
| 464 | 7x |
curdf |
| 465 |
} |
|
| 466 | ||
| 467 | ||
| 468 |
one_col_strs <- function(strcol, col_gap = 2, fontspec) {
|
|
| 469 | 46x |
strspls <- strsplit(strcol, split = "(-| (?=[^/]))", perl = TRUE) |
| 470 | 46x |
strspl_widths <- lapply(strspls, nchar_ttype, fontspec = fontspec, raw = TRUE) |
| 471 | 46x |
lbound_raw <- max(unlist(strspl_widths)) |
| 472 | 46x |
lbound <- ceiling(lbound_raw + 2 * col_gap) |
| 473 | 46x |
ret <- calc_poss_lines(strspl_widths, lbound, 50, fontspec = fontspec) |
| 474 | 46x |
ret |
| 475 |
} |
|
| 476 | ||
| 477 | ||
| 478 | ||
| 479 |
#' @name def_colwidths |
|
| 480 |
#' |
|
| 481 |
#' @title Define Column Widths |
|
| 482 |
#' |
|
| 483 |
#' @description |
|
| 484 |
#' `def_colwidths` uses heuristics to determine suitable column widths given a |
|
| 485 |
#' table or listing, and a font. |
|
| 486 |
#' |
|
| 487 |
#' @param tt input TableTree |
|
| 488 |
#' @param fontspec Font specification |
|
| 489 |
#' @param label_width_ins Label Width in Inches. |
|
| 490 |
#' @param col_gap Column gap in spaces. Defaults to `.5` for listings and `3` |
|
| 491 |
#' for tables. |
|
| 492 |
#' @param type Type of the TableTree, used to determine column width calculation method. |
|
| 493 |
#' |
|
| 494 |
#' @details Listings are assumed to be rendered landscape on standard A1 paper, |
|
| 495 |
#' such that all columns are rendered on one page. Tables are allowed to |
|
| 496 |
#' be horizontally paginated, and column widths are determined based only on |
|
| 497 |
#' required word wrapping. See the `Automatic Column Widths` vignette for |
|
| 498 |
#' a detailed discussion of the algorithms used. |
|
| 499 |
#' @return |
|
| 500 |
#' * `def_colwidths`: a vector of column widths (including the label row pseudo-column in the table |
|
| 501 |
#' case) suitable for use rendering `tt` in the specified font. |
|
| 502 |
#' @export |
|
| 503 |
def_colwidths <- function(tt, |
|
| 504 |
fontspec, |
|
| 505 |
label_width_ins = 2, |
|
| 506 |
col_gap = ifelse(type == "Listing", .5, 3), |
|
| 507 |
type = tlg_type(tt)) {
|
|
| 508 | 32x |
if (type == "Figure") {
|
| 509 | ! |
ret <- NULL |
| 510 | 32x |
} else if (type == "Table") {
|
| 511 |
if ( |
|
| 512 | 26x |
is.list(tt) && |
| 513 | 26x |
!methods::is(tt, "MatrixPrintForm") && |
| 514 | 26x |
!is.null(formatters::mf_col_widths(tt[[1]])) |
| 515 |
) {
|
|
| 516 | ! |
ret <- formatters::mf_col_widths(tt[[1]]) |
| 517 |
} else {
|
|
| 518 | 26x |
ret <- no_cellwrap_colwidths(tt, fontspec, col_gap = col_gap, label_width_ins = label_width_ins) |
| 519 |
} |
|
| 520 |
} else {
|
|
| 521 | 6x |
ret <- listing_column_widths(tt, fontspec = fontspec, col_gap = col_gap) |
| 522 |
} |
|
| 523 | 32x |
ret |
| 524 |
} |
| 1 |
.h_s_eair_diff_scalar <- function(x1, |
|
| 2 |
n1, |
|
| 3 |
x2, |
|
| 4 |
n2, |
|
| 5 |
conf_type = c("wald", "waldcc", "mn", "scas"),
|
|
| 6 |
conf_level = 0.95) {
|
|
| 7 | 272x |
conf_type <- match.arg(conf_type) |
| 8 | 272x |
diff_est <- switch(conf_type, |
| 9 | 272x |
waldcc = h_s_eair_diff_wald(x1, n1, x2, n2, conf_level, cc = TRUE), |
| 10 | 272x |
wald = h_s_eair_diff_wald(x1, n1, x2, n2, conf_level, cc = FALSE), |
| 11 | 272x |
mn = h_s_eair_diff_mn(x1, n1, x2, n2, conf_level, skew = FALSE), |
| 12 | 272x |
scas = h_s_eair_diff_mn(x1, n1, x2, n2, conf_level, skew = TRUE) |
| 13 |
) |
|
| 14 | 272x |
list(diff_est = diff_est$est, diff_lcl = diff_est$ci[1], diff_ucl = diff_est$ci[2]) |
| 15 |
} |
|
| 16 | ||
| 17 |
h_s_eair_diff <- function(x1, |
|
| 18 |
n1, |
|
| 19 |
x2, |
|
| 20 |
n2, |
|
| 21 |
conf_type, |
|
| 22 |
conf_level = 0.95) {
|
|
| 23 | 146x |
results <- mapply( |
| 24 | 146x |
.h_s_eair_diff_scalar, |
| 25 | 146x |
x1, n1, x2, n2, |
| 26 | 146x |
MoreArgs = list(conf_type = conf_type, conf_level = conf_level), |
| 27 | 146x |
SIMPLIFY = FALSE |
| 28 |
) |
|
| 29 | 146x |
data.frame( |
| 30 | 146x |
diff_est = sapply(results, `[[`, "diff_est"), |
| 31 | 146x |
diff_lcl = sapply(results, `[[`, "diff_lcl"), |
| 32 | 146x |
diff_ucl = sapply(results, `[[`, "diff_ucl") |
| 33 |
) |
|
| 34 |
} |
|
| 35 | ||
| 36 |
h_s_eair_diff_wald <- function(x1, n1, x2, n2, conf_level, cc = TRUE) {
|
|
| 37 | 197x |
alpha <- 1 - conf_level |
| 38 | 197x |
est <- x1 / n1 - x2 / n2 |
| 39 | 197x |
se <- sqrt(x1 / n1^2 + x2 / n2^2) |
| 40 | ||
| 41 | 197x |
coeff <- stats::qnorm(1 - alpha / 2) |
| 42 | 197x |
term2 <- coeff * se |
| 43 | 197x |
if (cc) {
|
| 44 | 30x |
term2 <- term2 + 0.5 * (1 / n1 + 1 / n2) |
| 45 |
} |
|
| 46 | 197x |
lcl <- est - term2 |
| 47 | 197x |
ucl <- est + term2 |
| 48 | ||
| 49 | 197x |
eair_diff <- list() |
| 50 | 197x |
eair_diff$est <- est |
| 51 | 197x |
eair_diff$ci <- c(lcl, ucl) |
| 52 | 197x |
eair_diff |
| 53 |
} |
|
| 54 | ||
| 55 |
h_s_eair_diff_mn <- function(x1, n1, x2, n2, conf_level, skew = FALSE) {
|
|
| 56 | 75x |
alpha <- 1 - conf_level |
| 57 | 75x |
z <- stats::qnorm(1 - alpha / 2) |
| 58 | ||
| 59 | 75x |
est <- x1 / n1 - x2 / n2 |
| 60 | ||
| 61 | 75x |
lcl <- .conf_poi_rd(x1, n1, x2, n2, z, lower = TRUE, skew = skew, level = conf_level) |
| 62 | 75x |
ucl <- .conf_poi_rd(x1, n1, x2, n2, z, lower = FALSE, skew = skew, level = conf_level) |
| 63 | ||
| 64 | 75x |
eair_diff <- list() |
| 65 | 75x |
eair_diff$est <- est |
| 66 | 75x |
eair_diff$ci <- c(lcl, ucl) |
| 67 | 75x |
eair_diff |
| 68 |
} |
|
| 69 | ||
| 70 |
.score_poi_rd <- function(x1, n1, x2, n2, theta, skew = FALSE, level = 0.95) {
|
|
| 71 | 3512x |
alpha <- 1 - level |
| 72 | 3512x |
p1hat <- x1 / n1 |
| 73 | 3512x |
p2hat <- x2 / n2 |
| 74 | 3512x |
Stheta <- (p1hat - p2hat) - theta |
| 75 | 3512x |
if (isTRUE(abs(Stheta) == 0)) {
|
| 76 | ! |
return(0) |
| 77 |
} |
|
| 78 | 3512x |
x <- x1 + x2 |
| 79 | 3512x |
N <- n1 + n2 |
| 80 | 3512x |
A <- N |
| 81 | 3512x |
B <- N * theta - x |
| 82 | 3512x |
C_ <- -x2 * theta |
| 83 | 3512x |
num <- (-B + sqrt(B^2 - 4 * A * C_)) |
| 84 | 3512x |
p2d <- ifelse(isTRUE(num == 0), 0, num / (2 * A)) |
| 85 | 3512x |
p1d <- p2d + theta |
| 86 | 3512x |
V <- p1d / n1 + p2d / n2 |
| 87 | 3512x |
score1 <- Stheta / sqrt(V) |
| 88 | ||
| 89 | 3512x |
if (!skew) {
|
| 90 | 1400x |
return(score1) |
| 91 |
} |
|
| 92 | ||
| 93 |
# Skewness correction (SCAS) |
|
| 94 | 2112x |
mu3 <- p1d / (n1^2) - p2d / (n2^2) |
| 95 | 2112x |
scterm <- mu3 / (6 * V^(3 / 2)) |
| 96 | 2112x |
if (isTRUE(scterm == 0)) {
|
| 97 | ! |
return(score1) |
| 98 |
} |
|
| 99 | ||
| 100 | 2112x |
A_q <- scterm |
| 101 | 2112x |
B_q <- 1 |
| 102 | 2112x |
C_q <- -(score1 + scterm) |
| 103 | 2112x |
dsct <- B_q^2 - 4 * A_q * C_q |
| 104 | 2112x |
if (dsct < 0) {
|
| 105 |
# Fallback to simplified skewness correction |
|
| 106 | ! |
qtnorm <- stats::qnorm(1 - alpha / 2) |
| 107 | ! |
score1 <- score1 - (qtnorm^2 - 1) * scterm |
| 108 |
} else {
|
|
| 109 | 2112x |
score1 <- (-B_q + sqrt(dsct)) / (2 * A_q) |
| 110 |
} |
|
| 111 | ||
| 112 | 2112x |
return(score1) |
| 113 |
} |
|
| 114 | ||
| 115 |
.conf_poi_rd <- function(x1, n1, x2, n2, z, lower = FALSE, |
|
| 116 |
skew = FALSE, level = 0.95) {
|
|
| 117 | 150x |
p1 <- x1 / n1 |
| 118 | 150x |
p2 <- x2 / n2 |
| 119 | 150x |
p_hat <- p1 - p2 |
| 120 | 150x |
dp <- 1 + ifelse(lower, 1, -1) * p_hat |
| 121 | 150x |
i <- 1 |
| 122 | 150x |
while (i <= 50) {
|
| 123 | 3512x |
dp <- 0.5 * dp |
| 124 | 3512x |
y <- p_hat + ifelse(lower, -1, 1) * dp |
| 125 | 3512x |
score <- .score_poi_rd(x1, n1, x2, n2, y, skew = skew, level = level) |
| 126 |
# For lower bound: score decreases as y moves left, find where score = +z |
|
| 127 |
# -> move y further left if score > z (still too close to estimate) |
|
| 128 |
# For upper bound: score decreases as y moves right, find where score = -z |
|
| 129 |
# -> move y further right if score > -z (still too close to estimate) |
|
| 130 | 3512x |
if (lower) {
|
| 131 | 755x |
if (score < z) p_hat <- y |
| 132 |
} else {
|
|
| 133 | 700x |
if (score > -z) p_hat <- y |
| 134 |
} |
|
| 135 | 3512x |
if ((dp < 1e-07) || (abs(score - ifelse(lower, z, -z)) < 1e-06)) {
|
| 136 | 150x |
break |
| 137 |
} else {
|
|
| 138 | 3362x |
i <- i + 1 |
| 139 |
} |
|
| 140 |
} |
|
| 141 | 150x |
return(y) |
| 142 |
} |
| 1 |
#' Get Control Subset |
|
| 2 |
#' |
|
| 3 |
#' Retrieves a subset of the DataFrame based on treatment variable and control group. |
|
| 4 |
#' |
|
| 5 |
#' @param df (`data.frame`)\cr Data frame to subset. |
|
| 6 |
#' @param trt_var (`character`)\cr Treatment variable name. |
|
| 7 |
#' @param ctrl_grp (`character`)\cr Control group value. |
|
| 8 |
#' @return Subset of the data frame. |
|
| 9 |
#' @keywords internal |
|
| 10 |
get_ctrl_subset <- function(df, trt_var, ctrl_grp) {
|
|
| 11 | 207x |
df[df[[trt_var]] == ctrl_grp, ] |
| 12 |
} |
|
| 13 | ||
| 14 | ||
| 15 |
#' Create Alternative Data Frame |
|
| 16 |
#' |
|
| 17 |
#' Creates an alternative data frame based on the current split context. |
|
| 18 |
#' |
|
| 19 |
#' @param .spl_context Current split context. |
|
| 20 |
#' @param .df_row Current data frame row. |
|
| 21 |
#' @param denomdf Denominator data frame. |
|
| 22 |
#' @param denom_by Denominator grouping variable. |
|
| 23 |
#' @param id Identifier variable. |
|
| 24 |
#' @param variables Variables to include in the analysis. |
|
| 25 |
#' @param denom Denominator type. |
|
| 26 |
#' @return Grand parent dataset. |
|
| 27 |
#' @noRd |
|
| 28 |
#' @keywords Internal |
|
| 29 |
h_create_altdf <- function(.spl_context, .df_row, denomdf, denom_by = NULL, id, variables, denom) {
|
|
| 30 | 662x |
colid <- .spl_context$cur_col_id[[1]] |
| 31 | 662x |
inriskdiffcol <- grepl("difference", tolower(colid), fixed = TRUE)
|
| 32 | 662x |
if (is.null(denomdf) && denom %in% c("N_col") && length(.spl_context$split) > 1 && inriskdiffcol) {
|
| 33 | ! |
stop("In order to get correct numbers in relative risk column,
|
| 34 | ! |
the alt_counts_df dataset should be passed to build_table") |
| 35 |
} |
|
| 36 | ||
| 37 |
### parent df in the current row-split (all col splits are still in) |
|
| 38 | 662x |
pardf <- .spl_context$full_parent_df[[NROW(.spl_context)]] |
| 39 | ||
| 40 |
## if no denomdf defined, use input if you want to use alt_source df, you better pass on the same dataframe as |
|
| 41 |
## denomdf in the function call |
|
| 42 | ||
| 43 | 662x |
colsplit <- .spl_context$cur_col_split[[1]] |
| 44 | 662x |
if (length(colsplit) == 1 && tolower(colsplit) == "total") {
|
| 45 | 7x |
colsplit <- NULL |
| 46 |
} |
|
| 47 | ||
| 48 | 662x |
if (is.null(denomdf) || denom %in% c("n_df", "n_rowdf")) {
|
| 49 |
#### once we have the rtables version >= 0.6.12 -- is.null(denomdf) only will happen when build_table has been |
|
| 50 |
#### used without alt_counts_df argument denom options c('N_col', 'n_df', 'n_altdf', 'N_colgroup', 'n_rowdf',
|
|
| 51 |
#### 'n_parentdf') note that n_altdf always has a non-null denomdf N_colgroup and n_parentdf will be handled |
|
| 52 |
#### separately n_parentdf in h_denom_parentdf N_colgroup with h_colexpr_substr |
|
| 53 | ||
| 54 | 139x |
denomdf <- unique(.df_row[, c(id, variables$strata, colsplit, denom_by), drop = FALSE]) |
| 55 |
} |
|
| 56 | ||
| 57 |
# grand parent dataset starts of from denomdf -- not yet rowsplits this is for the risk diff columns to include all |
|
| 58 |
# subjects to start from |
|
| 59 | 662x |
gpardf <- denomdf |
| 60 | ||
| 61 | 662x |
nm_gpardf <- names(gpardf) |
| 62 | ||
| 63 | 662x |
sbgrpvar <- intersect(.spl_context$split, nm_gpardf) |
| 64 | ||
| 65 | 662x |
cursbgrp_value <- NULL |
| 66 | ||
| 67 | 662x |
if (!is.null(denom_by) && length(sbgrpvar) > 0) {
|
| 68 |
### assumption: the subgroup is the first variable in the row-split if this assumption is not valid --- |
|
| 69 |
### re-design and let user pass in the subgroup variable |
|
| 70 | 99x |
sbgrpvar <- denom_by[1] |
| 71 | 99x |
cur_index <- which(.spl_context$split == sbgrpvar) |
| 72 | 99x |
if (length(cur_index) > 0) {
|
| 73 | 99x |
cursbgrp_value <- .spl_context$value[[cur_index]] |
| 74 | ||
| 75 | 99x |
gpardf <- subset(denomdf, eval(denomdf[[sbgrpvar]] == cursbgrp_value)) |
| 76 |
} |
|
| 77 | 99x |
if (length(cur_index) == 0) {
|
| 78 |
# cat(paste('sbgrpvar', sbgrpvar)) cat(paste( unique(.spl_context$value), sep = ', '))
|
|
| 79 |
} |
|
| 80 |
} |
|
| 81 | ||
| 82 |
## note this is a rowsplit only, not yet column split |
|
| 83 | 662x |
return(gpardf) |
| 84 |
} |
|
| 85 | ||
| 86 |
#' No Data to Report String |
|
| 87 |
#' |
|
| 88 |
#' A constant string used when there is no data to display in a table. |
|
| 89 |
#' This is used as a placeholder in tables when no data is available for a particular category. |
|
| 90 |
#' |
|
| 91 |
#' @return A character string with the value "No data to report". |
|
| 92 |
#' |
|
| 93 |
#' @export |
|
| 94 |
#' @keywords internal |
|
| 95 |
no_data_to_report_str <- "No data to report" |
|
| 96 | ||
| 97 | ||
| 98 |
#' Update Factor |
|
| 99 |
#' |
|
| 100 |
#' Updates a factor variable in a data frame based on specified values. |
|
| 101 |
#' |
|
| 102 |
#' @param df Data frame containing the variable to update. |
|
| 103 |
#' @param .var Variable name to update. |
|
| 104 |
#' @param val Values to keep. |
|
| 105 |
#' @param excl_levels Levels to exclude from the factor. |
|
| 106 |
#' @return Updated data frame. |
|
| 107 |
#' @noRd |
|
| 108 |
h_update_factor <- function(df, .var, val = NULL, excl_levels = NULL) {
|
|
| 109 | 583x |
if (!is.factor(df[[.var]]) || (is.null(val) && is.null(excl_levels))) {
|
| 110 | 71x |
return(df) |
| 111 |
} |
|
| 112 | ||
| 113 | 512x |
if ((!is.null(val) && !is.null(excl_levels))) {
|
| 114 | ! |
stop("update_factor cannot be used with both val and excl_levels specified.")
|
| 115 |
} |
|
| 116 | ||
| 117 | 512x |
all_levels <- levels(df[[.var]]) |
| 118 | ||
| 119 | 512x |
if (!is.null(val)) {
|
| 120 | 511x |
exclude_levels <- all_levels[!(all_levels %in% val)] |
| 121 | 511x |
remaining_levels <- setdiff(all_levels, exclude_levels) |
| 122 |
} |
|
| 123 | 512x |
if (!is.null(excl_levels)) {
|
| 124 | 1x |
exclude_levels <- all_levels[all_levels %in% excl_levels] |
| 125 | 1x |
remaining_levels <- setdiff(all_levels, exclude_levels) |
| 126 |
} |
|
| 127 | ||
| 128 |
## introduce level No data to report |
|
| 129 | 512x |
if (length(remaining_levels) == 0) {
|
| 130 | 15x |
remaining_levels <- no_data_to_report_str |
| 131 |
} |
|
| 132 | 512x |
df[[.var]] <- factor(as.character(df[[.var]]), levels = remaining_levels) |
| 133 | ||
| 134 | 512x |
return(df) |
| 135 |
} |
|
| 136 | ||
| 137 | ||
| 138 |
#' Extract Substring from Column Expression |
|
| 139 |
#' |
|
| 140 |
#' Retrieves the substring from a column expression related to a variable component. |
|
| 141 |
#' |
|
| 142 |
#' get substring from col_expr related to var component |
|
| 143 |
#' intended usage is on strings coming from .spl_context$cur_col_expr |
|
| 144 |
#' these strings are of type '!(is.na(var) & var %in% 'xxx') & !(is.na(var2) & var2 %in% 'xxx')' |
|
| 145 |
#' |
|
| 146 |
#' @param var Variable to extract from the expression. |
|
| 147 |
#' @param col_expr Column expression string. |
|
| 148 |
#' @return Substring corresponding to the variable. |
|
| 149 |
#' @noRd |
|
| 150 |
#' @keywords internal |
|
| 151 |
h_colexpr_substr <- function(var, col_expr) {
|
|
| 152 |
# reconstructing the strings is not an option as doesn't work for combined columns facets |
|
| 153 | 94x |
cur_col_expr <- as.character(col_expr) |
| 154 | ||
| 155 | 94x |
if (!grepl(var, cur_col_expr, fixed = TRUE)) {
|
| 156 | 1x |
return(NULL) |
| 157 |
} |
|
| 158 | ||
| 159 | 93x |
z2 <- paste0("(!is.na(", var, ") & ", var, " %in%")
|
| 160 | 93x |
start <- regexpr(z2, cur_col_expr, fixed = TRUE) |
| 161 | 93x |
end <- start + attr(start, "match.length") - 1 |
| 162 | 93x |
z1 <- cbind(start, end) |
| 163 | ||
| 164 |
### start of the string |
|
| 165 | 93x |
z1_start <- z1[1, 1] |
| 166 | ||
| 167 |
### figure out what is the appropriate end expression has several & in string |
|
| 168 | ||
| 169 | 93x |
positions <- gregexpr("&", cur_col_expr, fixed = TRUE)
|
| 170 | 93x |
start <- unlist(positions) |
| 171 | 93x |
end <- start + attr(positions[[1]], "match.length") - 1 |
| 172 | 93x |
z3 <- cbind(start, end)[, 1] |
| 173 | ||
| 174 |
### get the first & after the end of z3 ((!is.na(TRT01A) & TRT01A %in%) |
|
| 175 | 93x |
h <- z3 > z1[, 2] |
| 176 | 93x |
if (any(h)) {
|
| 177 | 85x |
z4 <- which.max(h) |
| 178 | 85x |
z1_end <- z3[z4] - 3 |
| 179 |
} else {
|
|
| 180 | 8x |
z1_end <- nchar(cur_col_expr) |
| 181 |
} |
|
| 182 | ||
| 183 | 93x |
col_expr_substr <- substr(cur_col_expr, z1_start, z1_end) |
| 184 | ||
| 185 | 93x |
return(col_expr_substr) |
| 186 |
} |
|
| 187 | ||
| 188 |
#' Get Denominator Parent Data Frame |
|
| 189 |
#' |
|
| 190 |
#' Retrieves the parent data frame based on denominator. |
|
| 191 |
#' |
|
| 192 |
#' @param .spl_context Current split context. |
|
| 193 |
#' @param denom Denominator type. |
|
| 194 |
#' @param denom_by Denominator grouping variable. |
|
| 195 |
#' @return Parent data frame. |
|
| 196 |
#' @noRd |
|
| 197 |
#' @keywords internal |
|
| 198 |
h_denom_parentdf <- function(.spl_context, denom, denom_by) {
|
|
| 199 | 566x |
if (denom != "n_parentdf") {
|
| 200 | 566x |
return(NULL) |
| 201 |
} |
|
| 202 | ! |
if (is.null(denom_by)) {
|
| 203 | ! |
stop("denom_by must be specified when using denom = 'n_parentdf'.")
|
| 204 |
} |
|
| 205 | ! |
split <- .spl_context$split |
| 206 | ! |
if (split[1] == "root" && !is.null(denom_by)) {
|
| 207 | ! |
denom_by <- c("root", denom_by)
|
| 208 |
} |
|
| 209 | ! |
split <- intersect(denom_by, split) |
| 210 | ! |
parentdf <- .spl_context$full_parent_df[[length(split)]] |
| 211 | ! |
return(parentdf) |
| 212 |
} |
|
| 213 | ||
| 214 |
#' Add New Levels to Data Frame |
|
| 215 |
#' |
|
| 216 |
#' Adds new factor levels to a specified variable in the data frame. |
|
| 217 |
#' |
|
| 218 |
#' @param df Data frame to update. |
|
| 219 |
#' @param .var Variable to which new levels will be added. |
|
| 220 |
#' @param new_levels List of new levels to add. |
|
| 221 |
#' @param addstr2levs String to add to new levels. |
|
| 222 |
#' @param new_levels_after Boolean, indicating if new levels should be added after existing levels. |
|
| 223 |
#' @return Updated data frame. |
|
| 224 |
#' @noRd |
|
| 225 |
#' @keywords internal |
|
| 226 |
h_df_add_newlevels <- function(df, .var, new_levels, addstr2levs = NULL, new_levels_after) {
|
|
| 227 | 21x |
varvec <- df[[.var]] |
| 228 | ||
| 229 | 21x |
levs <- if (is.factor(varvec)) levels(varvec) else sort(unique(varvec)) |
| 230 | ||
| 231 | 21x |
if (!is.null(new_levels)) {
|
| 232 |
## assumption: new_levels[[1]] : names of the new levels new_levels[[2]] : values of the new levels |
|
| 233 | 21x |
if (length(new_levels[[1]]) != length(new_levels[[2]])) {
|
| 234 | ! |
stop( |
| 235 | ! |
"new_levels must be a list of length 2, second element must be a ", |
| 236 | ! |
"list of same length as first element." |
| 237 |
) |
|
| 238 |
} |
|
| 239 | 21x |
if (any(duplicated(unlist(new_levels[[2]])))) {
|
| 240 | ! |
stop( |
| 241 | ! |
"unlist(new_levels[[2]]) contains duplicates: duplicate assignment ", |
| 242 | ! |
"of a level to a new level is not allowed." |
| 243 |
) |
|
| 244 |
} |
|
| 245 | ||
| 246 | 21x |
sortnewlevs <- unlist(lapply(X = new_levels[[2]], FUN = function(x) {
|
| 247 | 21x |
min(which(levs %in% x)) - 0.1 |
| 248 |
})) |
|
| 249 | ||
| 250 | 21x |
if (new_levels_after) {
|
| 251 | 17x |
sortnewlevs <- unlist(lapply(X = new_levels[[2]], FUN = function(x) {
|
| 252 | 17x |
max(which(levs %in% x)) + 0.1 |
| 253 |
})) |
|
| 254 |
} |
|
| 255 | ||
| 256 | 21x |
newlevs <- c(levs, new_levels[[1]]) |
| 257 | 21x |
sortx <- c(seq_along(levs), sortnewlevs) |
| 258 | ||
| 259 | 21x |
newlevsx <- newlevs[order(sortx)] |
| 260 | ||
| 261 | 21x |
levs <- newlevsx |
| 262 | ||
| 263 |
### add newlevels to df |
|
| 264 | 21x |
for (i in seq_along(new_levels[[1]])) {
|
| 265 | 21x |
levii <- unlist(new_levels[[2]][i]) |
| 266 | ||
| 267 | 21x |
addi <- df[df[[.var]] %in% levii, ] |
| 268 | 21x |
addi[[.var]] <- new_levels[[1]][i] |
| 269 | ||
| 270 | 21x |
df <- rbind(df, addi) |
| 271 |
} |
|
| 272 |
} |
|
| 273 | ||
| 274 | 21x |
levlabels <- levs |
| 275 | 21x |
if (!is.null(addstr2levs)) {
|
| 276 | 4x |
levlabels <- paste0(levs, addstr2levs) |
| 277 |
} |
|
| 278 | ||
| 279 | 21x |
df[[.var]] <- factor(as.character(df[[.var]]), levels = levs, labels = levlabels) |
| 280 | ||
| 281 | 21x |
return(df) |
| 282 |
} |
|
| 283 | ||
| 284 |
#' @title Get Current Treatment Group |
|
| 285 |
#' |
|
| 286 |
#' @description `r lifecycle::badge("stable")`
|
|
| 287 |
#' |
|
| 288 |
#' Retrieves the current treatment group from the current column split-path, |
|
| 289 |
#' given the treatment variable name. |
|
| 290 |
#' |
|
| 291 |
#' @param trt_var (`character(1)`)\cr The treatment variable name. |
|
| 292 |
#' @param .spl_context (`data.frame`)\cr The current split context. |
|
| 293 |
#' @return A character string containing the treatment group name. |
|
| 294 |
#' |
|
| 295 |
#' @keywords internal |
|
| 296 |
#' @author WW |
|
| 297 |
#' @seealso [cur_col_split_path()] |
|
| 298 |
#' @examples |
|
| 299 |
#' \dontrun{
|
|
| 300 |
#' .spl_context <- data.frame( |
|
| 301 |
#' cur_col_split = I(list(c("ARM"))),
|
|
| 302 |
#' cur_col_split_val = I(list(c("Placebo")))
|
|
| 303 |
#' ) |
|
| 304 |
#' |
|
| 305 |
#' h_get_cur_trt_grp("ARM", .spl_context)
|
|
| 306 |
#' h_get_cur_trt_grp("TRT", .spl_context) # errors: TRT not found
|
|
| 307 |
#' } |
|
| 308 |
#' |
|
| 309 |
h_get_cur_trt_grp <- function(trt_var, .spl_context) {
|
|
| 310 | 72x |
checkmate::assert_string(trt_var) |
| 311 | 72x |
checkmate::assert_data_frame(.spl_context) |
| 312 | ||
| 313 | 72x |
cur_col_path <- cur_col_split_path(.spl_context) |
| 314 | 72x |
checkmate::assert_true(length(cur_col_path) %% 2L == 0L) |
| 315 | ||
| 316 | 72x |
trt_var_pos <- strict_match(trt_var, cur_col_path, odd = TRUE) |
| 317 | 71x |
cur_col_path[trt_var_pos + 1L] |
| 318 |
} |
|
| 319 | ||
| 320 |
# helper function to define expression for retrieving ref_group type of datasets |
|
| 321 |
h_get_ref_col_expr <- function(ref_path = NULL) {
|
|
| 322 | 153x |
if (is.null(ref_path)) {
|
| 323 | ! |
stop("h_get_ref_col_expr: ref_path cannot be NULL")
|
| 324 |
} |
|
| 325 | 153x |
checkmate::assert_character(ref_path, min.len = 2L, names = "unnamed") |
| 326 | 153x |
checkmate::assert_true(length(ref_path) %% 2 == 0) |
| 327 | ||
| 328 | 153x |
vars <- ref_path[seq(from = 1L, to = length(ref_path) - 1L, by = 2L)] |
| 329 | 153x |
levels <- ref_path[seq(from = 2L, to = length(ref_path), by = 2L)] |
| 330 | ||
| 331 | 153x |
parts <- paste0( |
| 332 | 153x |
"!is.na(", vars, ") & (", vars, " %in% c(\"", levels, "\"))"
|
| 333 |
) |
|
| 334 | ||
| 335 | 153x |
res <- paste(parts, collapse = " & ") |
| 336 | 153x |
call_expr <- parse(text = res)[[1]] |
| 337 | ||
| 338 | 153x |
as.expression(call_expr) |
| 339 |
} |
|
| 340 | ||
| 341 |
get_ref_info_expanded <- function(df, |
|
| 342 |
.var, |
|
| 343 |
.df_row, |
|
| 344 |
.spl_context, |
|
| 345 |
ref_path, |
|
| 346 |
riskdiff = TRUE, |
|
| 347 |
riskdiff_setup = c("horizontal", "vertical")) {
|
|
| 348 | 155x |
riskdiff_setup <- match.arg(riskdiff_setup) |
| 349 | ||
| 350 | 155x |
if (riskdiff && is.null(ref_path)) {
|
| 351 | ! |
stop("ref_path cannot be NULL when riskdiff = TRUE, please specify it. See ?get_ref_info for details.")
|
| 352 |
} |
|
| 353 | ||
| 354 |
## prepare for column based split |
|
| 355 | 155x |
cur_col_expr <- .spl_context$cur_col_expr[[1]] |
| 356 |
## colid can be used to figure out if we're in the relative risk columns or not |
|
| 357 | 155x |
colid <- .spl_context$cur_col_id[[1]] |
| 358 | 155x |
inriskdiffcol <- grepl("difference", tolower(colid), fixed = TRUE)
|
| 359 | ||
| 360 | 155x |
if (!is.null(ref_path)) {
|
| 361 | 153x |
ref <- get_ref_info(ref_path, .spl_context) |
| 362 | 153x |
.in_ref_col <- ref$in_ref_col |
| 363 | 153x |
.ref_group <- ref$ref_group |
| 364 | 153x |
ref_col_expr <- h_get_ref_col_expr(ref_path) |
| 365 |
} else {
|
|
| 366 | 2x |
ref_col_expr <- NULL |
| 367 | 2x |
.in_ref_col <- NULL |
| 368 | 2x |
.ref_group <- NULL |
| 369 |
} |
|
| 370 | ||
| 371 |
# !perform_vs_ref_stats will be passed as .in_ref_col in the s_function s_eair100_levii_j |
|
| 372 |
# while it does not entirely reflect |
|
| 373 |
# whether in ref column or not |
|
| 374 |
# it is the behaviour of performing vs ref group calculations what is important |
|
| 375 | 155x |
if (riskdiff && riskdiff_setup == "vertical" && is.null(.in_ref_col)) {
|
| 376 | 10x |
perform_vs_ref_stats <- FALSE |
| 377 | 145x |
} else if (riskdiff && riskdiff_setup == "vertical" && is.logical(.in_ref_col)) {
|
| 378 | 37x |
perform_vs_ref_stats <- !.in_ref_col |
| 379 |
} else if ( |
|
| 380 | 108x |
riskdiff && |
| 381 | 108x |
riskdiff_setup == "horizontal" && |
| 382 | 108x |
inriskdiffcol && |
| 383 | 108x |
!identical(df, subset(.df_row, eval(ref_col_expr))) |
| 384 |
) {
|
|
| 385 | 39x |
perform_vs_ref_stats <- TRUE |
| 386 |
} else {
|
|
| 387 | 69x |
perform_vs_ref_stats <- FALSE |
| 388 |
} |
|
| 389 | 155x |
return(list( |
| 390 | 155x |
.in_ref_col = .in_ref_col, |
| 391 | 155x |
.ref_group = .ref_group, |
| 392 | 155x |
perform_vs_ref_stats = perform_vs_ref_stats, |
| 393 | 155x |
ref_col_expr = ref_col_expr |
| 394 |
)) |
|
| 395 |
} |
|
| 396 | ||
| 397 |
#' Update Data Frame Row |
|
| 398 |
#' |
|
| 399 |
#' Updates a row in the data frame based on various parameters. |
|
| 400 |
#' |
|
| 401 |
#' @param df_row Data frame row to update. |
|
| 402 |
#' @param .var Variable name to update. |
|
| 403 |
#' @param val Values to keep. |
|
| 404 |
#' @param excl_levels Levels to exclude from the factor. |
|
| 405 |
#' @param drop_levels Boolean, indicating if levels should be dropped. |
|
| 406 |
#' @param new_levels New levels to add. |
|
| 407 |
#' @param new_levels_after Boolean, indicating if new levels should be added after existing levels. |
|
| 408 |
#' @param addstr2levs String to add to new levels. |
|
| 409 |
#' @param label Label string. |
|
| 410 |
#' @param label_map Mapping for labels. |
|
| 411 |
#' @param labelstr Label string to replace. |
|
| 412 |
#' @param label_fstr Format string for labels. |
|
| 413 |
#' @param .spl_context Current split context. |
|
| 414 |
#' @return List containing updated data frames and values. |
|
| 415 |
#' @noRd |
|
| 416 |
#' @keywords internal |
|
| 417 |
h_upd_dfrow <- function( |
|
| 418 |
df_row, |
|
| 419 |
.var, |
|
| 420 |
val, |
|
| 421 |
excl_levels, |
|
| 422 |
drop_levels, |
|
| 423 |
new_levels, |
|
| 424 |
new_levels_after, |
|
| 425 |
addstr2levs, |
|
| 426 |
label, |
|
| 427 |
label_map, |
|
| 428 |
labelstr, |
|
| 429 |
label_fstr, |
|
| 430 |
.spl_context |
|
| 431 |
) {
|
|
| 432 | 682x |
if (!is.null(label) && !is.null(label_map)) {
|
| 433 | ! |
stop("a_freq_j: label and label_map cannot be used together.")
|
| 434 |
} |
|
| 435 | ||
| 436 | 682x |
if (!is.null(labelstr) && (!is.null(val) || !is.null(new_levels) || !is.null(excl_levels))) {
|
| 437 | ! |
stop("a_freq_j: val/excl_levels/new_levels cannot be used in a summarize_row_group call.")
|
| 438 |
} |
|
| 439 | ||
| 440 | 682x |
if (!is.null(new_levels) && (is.null(.var) || is.na(.var))) {
|
| 441 | ! |
stop("When using new_levels, var must be provided in analyze call with a_freq_j.")
|
| 442 |
} |
|
| 443 | ||
| 444 | 682x |
if (!is.null(val) && drop_levels == TRUE) {
|
| 445 | ! |
stop("argument val cannot be used together with drop_levels = TRUE.")
|
| 446 |
} |
|
| 447 | ||
| 448 | 682x |
if (!is.null(val) && !is.null(excl_levels)) {
|
| 449 | ! |
stop("argument val and excl_levels cannot be used together.")
|
| 450 |
} |
|
| 451 | ||
| 452 |
## start making updates to factor information on incoming data |
|
| 453 | ||
| 454 | 682x |
if (!is.null(new_levels)) {
|
| 455 | 18x |
df_row <- h_df_add_newlevels( |
| 456 | 18x |
df = df_row, |
| 457 | 18x |
.var = .var, |
| 458 | 18x |
new_levels = new_levels, |
| 459 | 18x |
new_levels_after = new_levels_after, |
| 460 | 18x |
addstr2levs = addstr2levs |
| 461 |
) |
|
| 462 |
} |
|
| 463 | ||
| 464 |
# if character var turn incoming data into factor, with levels from row-based df |
|
| 465 | 682x |
if (!is.na(.var) && is.character(df_row[[.var]]) && is.null(labelstr)) {
|
| 466 | 51x |
levels <- sort(unique(df_row[[.var]])) |
| 467 | 51x |
df_row[[.var]] <- factor(df_row[[.var]], levels = levels) |
| 468 | 51x |
drop_levels <- FALSE |
| 469 |
} |
|
| 470 | ||
| 471 | 682x |
if (!is.null(labelstr)) {
|
| 472 | 187x |
single_level <- labelstr |
| 473 | ||
| 474 | 187x |
if (!is.null(label_fstr) && grepl("%s", label_fstr, fixed = TRUE)) {
|
| 475 | 21x |
single_level <- sprintf(label_fstr, single_level) |
| 476 |
} |
|
| 477 | ||
| 478 | 187x |
df_row[[.var]] <- as.character(df_row[[.var]]) |
| 479 | 187x |
df_row[[.var]][!is.na(df_row[[.var]])] <- single_level |
| 480 | ||
| 481 | 187x |
df_row[[.var]] <- factor(as.character(df_row[[.var]]), levels = single_level) |
| 482 | 187x |
drop_levels <- FALSE |
| 483 |
} |
|
| 484 | ||
| 485 | 682x |
if (drop_levels) {
|
| 486 | 91x |
obs_levs <- unique(df_row[[.var]]) |
| 487 | 91x |
obs_levs <- intersect(levels(df_row[[.var]]), obs_levs) |
| 488 | ||
| 489 | ! |
if (!is.null(excl_levels)) obs_levs <- setdiff(obs_levs, excl_levels) |
| 490 | ||
| 491 | 91x |
val <- obs_levs |
| 492 | 91x |
excl_levels <- NULL |
| 493 |
} |
|
| 494 | ||
| 495 | 682x |
if (is.null(val) && !is.null(label_map)) {
|
| 496 | 23x |
split_info <- .spl_context[c("split", "value")]
|
| 497 | 23x |
val <- h_restrict_val(df_row, .var, label_map, split_info) |
| 498 |
} |
|
| 499 | ||
| 500 | 682x |
if (!is.null(val)) {
|
| 501 |
# do not yet restrict to val levels, only update factors to the requested levels df_row <- |
|
| 502 | 178x |
df_row <- h_update_factor(df_row, .var, val) |
| 503 |
} |
|
| 504 | 682x |
if (!is.null(excl_levels)) {
|
| 505 |
# do not yet exclude the level specified in excl_levels, only update factors to remove requested levels df_row |
|
| 506 | ||
| 507 | ! |
df_row <- h_update_factor(df_row, .var, excl_levels = excl_levels) |
| 508 | ! |
val <- levels(df_row[[.var]]) |
| 509 |
} |
|
| 510 | ||
| 511 |
## update data with level coming from label -- important that this is done after restriction to val levels!!! |
|
| 512 | 682x |
if (!is.null(label)) {
|
| 513 |
# similar to processing when labelstr |
|
| 514 | 49x |
single_level <- label |
| 515 | 49x |
df_row[[.var]] <- as.character(df_row[[.var]]) |
| 516 | 49x |
df_row[[.var]][!is.na(df_row[[.var]])] <- single_level |
| 517 | 49x |
val <- label |
| 518 | 49x |
df_row[[.var]] <- factor(df_row[[.var]], levels = single_level) |
| 519 |
} |
|
| 520 | ||
| 521 |
# now update labels coming from label_map |
|
| 522 | 682x |
if (!is.null(label_map)) {
|
| 523 | 28x |
split_info <- .spl_context[c("split", "value")]
|
| 524 | 28x |
new_labels <- h_get_label_map(levels(df_row[[.var]]), label_map, .var, split_info) |
| 525 | ||
| 526 | 27x |
df_row[[.var]] <- factor(as.character(df_row[[.var]]), levels = levels(df_row[[.var]]), labels = new_labels) |
| 527 | ||
| 528 | 27x |
val <- new_labels |
| 529 |
} |
|
| 530 | ||
| 531 |
# now apply the updated factors from df_row to df as well due to drop_levels = TRUE can have different results when |
|
| 532 |
# applying all of the above to df |
|
| 533 | 681x |
col_expr <- .spl_context$cur_col_expr[[1]] |
| 534 | 681x |
df <- subset(df_row, eval(col_expr)) |
| 535 | ||
| 536 | 681x |
return(list(df_row = df_row, df = df, val = val)) |
| 537 |
} |
|
| 538 | ||
| 539 | ||
| 540 |
#' Get Label Map |
|
| 541 |
#' |
|
| 542 |
#' Maps labels based on the provided label map and split context. |
|
| 543 |
#' |
|
| 544 |
#' @param .labels Current labels. |
|
| 545 |
#' @param label_map Mapping for labels. |
|
| 546 |
#' @param .var Variable name. |
|
| 547 |
#' @param split_info Current split information. |
|
| 548 |
#' @return Mapped labels. |
|
| 549 |
#' @noRd |
|
| 550 |
#' @keywords internal |
|
| 551 |
h_get_label_map <- function(.labels, label_map, .var, split_info) {
|
|
| 552 | 30x |
if (!is.null(label_map)) {
|
| 553 | 30x |
if (!all(c("split", "value") %in% names(split_info))) {
|
| 554 | ! |
stop("split_info does not contain required elements.")
|
| 555 |
} |
|
| 556 | ||
| 557 | 30x |
nodata <- FALSE |
| 558 | 30x |
if (length(.labels) == 1 && .labels == no_data_to_report_str) {
|
| 559 | 1x |
nodata <- TRUE |
| 560 |
} |
|
| 561 | ||
| 562 |
### if label_map has a variable from row split, apply current splits on label_map tibble as well |
|
| 563 | 30x |
rowsplits <- split_info$split |
| 564 | ||
| 565 | 30x |
label_map_split <- intersect(names(label_map), rowsplits) |
| 566 | ||
| 567 | 30x |
if (!(length(label_map_split) == 0)) {
|
| 568 | 12x |
for (i in seq_along(label_map_split)) {
|
| 569 | 12x |
cursplvar <- label_map_split[i] |
| 570 | 12x |
cid <- match(cursplvar, rowsplits) |
| 571 | 12x |
cursplval <- split_info$value[cid] |
| 572 | ||
| 573 | 12x |
label_map <- label_map[label_map[[cursplvar]] == cursplval, ] |
| 574 |
} |
|
| 575 |
} |
|
| 576 | ||
| 577 | 30x |
if ("var" %in% names(label_map)) {
|
| 578 | ! |
label_map <- label_map[label_map[["var"]] == .var, ] |
| 579 |
} |
|
| 580 | ||
| 581 | 30x |
.labels <- label_map$label[match(.labels, label_map$value)] |
| 582 | ||
| 583 | 30x |
if (nodata && anyNA(.labels)) {
|
| 584 | 1x |
stop(paste0( |
| 585 | 1x |
"got a label map that doesn't provide labels for all values.\n", |
| 586 | 1x |
"Perhaps convert analysis variable ", .var, " to a factor?" |
| 587 |
)) |
|
| 588 |
} |
|
| 589 | 29x |
if (anyNA(.labels)) {
|
| 590 | ! |
stop("got a label map that doesn't provide labels for all values.")
|
| 591 |
} |
|
| 592 |
} |
|
| 593 | ||
| 594 | 29x |
return(.labels) |
| 595 |
} |
|
| 596 | ||
| 597 | ||
| 598 |
#' A Frequency Data Preparation Function |
|
| 599 |
#' |
|
| 600 |
#' Prepares frequency data for analysis. |
|
| 601 |
#' @noRd |
|
| 602 |
#' @param df Data frame to prepare. |
|
| 603 |
#' @param labelstr Label string. |
|
| 604 |
#' @param .var Variable name. |
|
| 605 |
#' @param val Values for analysis. |
|
| 606 |
#' @param drop_levels Boolean, indicating if levels should be dropped. |
|
| 607 |
#' @param excl_levels Levels to exclude. |
|
| 608 |
#' @param new_levels New levels to add. |
|
| 609 |
#' @param new_levels_after Boolean for adding new levels after existing ones. |
|
| 610 |
#' @param addstr2levs String to add to new levels. |
|
| 611 |
#' @param .df_row Current data frame row. |
|
| 612 |
#' @param .spl_context Current split context. |
|
| 613 |
#' @param .N_col Number of columns. |
|
| 614 |
#' @param id Identifier variable. |
|
| 615 |
#' @param denom Denominator types. |
|
| 616 |
#' @param variables Variables to include in the analysis. |
|
| 617 |
#' @param label Label string. |
|
| 618 |
#' @param label_fstr Formatted label string. |
|
| 619 |
#' @param label_map Mapping for labels. |
|
| 620 |
#' @param .alt_df_full Alternative full data frame. |
|
| 621 |
#' @param denom_by Denominator grouping variable. |
|
| 622 |
#' @param .stats Statistics to compute. |
|
| 623 |
#' @return List containing prepared data frames and values. |
|
| 624 |
h_a_freq_dataprep <- function( |
|
| 625 |
df, |
|
| 626 |
labelstr = NULL, |
|
| 627 |
.var = NA, |
|
| 628 |
val = NULL, |
|
| 629 |
drop_levels = FALSE, |
|
| 630 |
excl_levels = NULL, |
|
| 631 |
new_levels = NULL, |
|
| 632 |
new_levels_after = FALSE, |
|
| 633 |
addstr2levs = NULL, |
|
| 634 |
.df_row, |
|
| 635 |
.spl_context, |
|
| 636 |
.N_col, |
|
| 637 |
id = "USUBJID", |
|
| 638 |
denom = c("N_col", "n_df", "n_altdf", "N_colgroup", "n_rowdf", "n_parentdf"),
|
|
| 639 |
variables, |
|
| 640 |
label = NULL, |
|
| 641 |
label_fstr = NULL, |
|
| 642 |
label_map = NULL, |
|
| 643 |
.alt_df_full = NULL, |
|
| 644 |
denom_by = NULL, |
|
| 645 |
.stats, |
|
| 646 |
countsource = c("df", "altdf", "altdf_subset")
|
|
| 647 |
) {
|
|
| 648 | 567x |
denom <- match.arg(denom) |
| 649 | ||
| 650 | 567x |
df <- df[!is.na(df[[.var]]), ] |
| 651 | 567x |
.df_row <- .df_row[!is.na(.df_row[[.var]]), ] |
| 652 | ||
| 653 |
# if no stats requested, get all stats |
|
| 654 | 567x |
.stats <- junco_get_stats("a_freq_j", stats_in = .stats, custom_stats_in = NULL)
|
| 655 | ||
| 656 |
### combine all preprocessing of incoming df/.df_row in one function do this outside stats derivation functions |
|
| 657 |
### (s_freq_j/) use all of val/excl_levels/drop_levels//new_levels/ label/label_map/labelstr/label_fstr |
|
| 658 | 567x |
upd_dfrow <- h_upd_dfrow( |
| 659 | 567x |
df_row = .df_row, |
| 660 | 567x |
.var = .var, |
| 661 | 567x |
val = val, |
| 662 | 567x |
excl_levels = excl_levels, |
| 663 | 567x |
drop_levels = drop_levels, |
| 664 | 567x |
new_levels = new_levels, |
| 665 | 567x |
new_levels_after = new_levels_after, |
| 666 | 567x |
addstr2levs = addstr2levs, |
| 667 | 567x |
label = label, |
| 668 | 567x |
label_map = label_map, |
| 669 | 567x |
labelstr = labelstr, |
| 670 | 567x |
label_fstr = label_fstr, |
| 671 | 567x |
.spl_context = .spl_context |
| 672 |
) |
|
| 673 | ||
| 674 | 566x |
.df_row <- upd_dfrow$df_row |
| 675 | 566x |
df <- upd_dfrow$df |
| 676 | ||
| 677 | 566x |
val <- upd_dfrow$val |
| 678 | ||
| 679 |
# from here onwards proceed with drop_levels = FALSE action has already been done in h_upd_dfrow, and proper |
|
| 680 |
# observed values will be passed to val for s_freq_j |
|
| 681 | 566x |
drop_levels <- FALSE |
| 682 | 566x |
excl_levels <- NULL |
| 683 | ||
| 684 |
### derive appropriate alt_df based upon .spl_context and .alt_df_full note that only row-based splits are done for |
|
| 685 |
### now only for variables from the first split_rows_by |
|
| 686 | 566x |
alt_df <- h_create_altdf( |
| 687 | 566x |
.spl_context, |
| 688 | 566x |
.df_row, |
| 689 | 566x |
.alt_df_full, |
| 690 | 566x |
denom_by = denom_by, |
| 691 | 566x |
id = id, |
| 692 | 566x |
variables = variables, |
| 693 | 566x |
denom = denom |
| 694 |
) |
|
| 695 | ||
| 696 | 566x |
if (identical(countsource, "altdf_subset")) {
|
| 697 |
# prefer explicit val, otherwise use the levels present in .df_row. |
|
| 698 | 4x |
if (!is.null(alt_df) && !is.na(.var) && (.var %in% names(alt_df))) {
|
| 699 | 4x |
keep_vals <- NULL |
| 700 | 4x |
if (!is.null(val)) {
|
| 701 | ! |
keep_vals <- val |
| 702 | 4x |
} else if (!is.null(.df_row) && !is.null(.df_row[[.var]])) {
|
| 703 | 4x |
keep_vals <- if (is.factor(.df_row[[.var]])) levels(.df_row[[.var]]) else unique(.df_row[[.var]]) |
| 704 |
} |
|
| 705 | ||
| 706 | 4x |
if (!is.null(keep_vals)) {
|
| 707 | 4x |
keep_vals <- intersect(keep_vals, unique(alt_df[[.var]])) |
| 708 | 4x |
if (length(keep_vals) > 0) {
|
| 709 | 4x |
alt_df <- alt_df[alt_df[[.var]] %in% keep_vals, , drop = FALSE] |
| 710 | 4x |
alt_df <- h_update_factor(alt_df, .var, keep_vals) |
| 711 |
} |
|
| 712 |
} |
|
| 713 |
} |
|
| 714 |
} |
|
| 715 | 566x |
new_denomdf <- alt_df |
| 716 | ||
| 717 | 566x |
parentdf <- h_denom_parentdf(.spl_context, denom, denom_by) |
| 718 | 566x |
if (denom == "n_parentdf") {
|
| 719 | ! |
new_denomdf <- parentdf |
| 720 |
} |
|
| 721 | ||
| 722 | 566x |
return(list( |
| 723 | 566x |
df = df, |
| 724 | 566x |
.df_row = .df_row, |
| 725 | 566x |
val = val, |
| 726 | 566x |
drop_levels = drop_levels, |
| 727 | 566x |
excl_levels = excl_levels, |
| 728 | 566x |
alt_df = alt_df, |
| 729 | 566x |
parentdf = parentdf, |
| 730 | 566x |
new_denomdf = new_denomdf, |
| 731 | 566x |
.stats = .stats |
| 732 |
)) |
|
| 733 |
} |
|
| 734 | ||
| 735 | ||
| 736 |
#' Frequency Preparation in Rows |
|
| 737 |
#' |
|
| 738 |
#' Prepares frequency data in rows based on provided parameters. |
|
| 739 |
#' @noRd |
|
| 740 |
#' @param x_stats Statistics data. |
|
| 741 |
#' @param .stats_adj Adjusted statistics. |
|
| 742 |
#' @param .formats Format settings. |
|
| 743 |
#' @param labelstr Label string. |
|
| 744 |
#' @param label_fstr Formatted label string. |
|
| 745 |
#' @param label Label string. |
|
| 746 |
#' @param .indent_mods Indentation settings. |
|
| 747 |
#' @param .labels_n Labels for statistics. |
|
| 748 |
#' @param na_str String for NA values. |
|
| 749 |
#' @return List containing prepared statistics, formats, labels, and indentation. |
|
| 750 |
#' @noRd |
|
| 751 |
#' @keywords internal |
|
| 752 |
h_a_freq_prepinrows <- function( |
|
| 753 |
x_stats, |
|
| 754 |
.stats_adj, |
|
| 755 |
.formats, |
|
| 756 |
labelstr, |
|
| 757 |
label_fstr, |
|
| 758 |
label, |
|
| 759 |
.indent_mods, |
|
| 760 |
.labels_n, |
|
| 761 |
na_str |
|
| 762 |
) {
|
|
| 763 |
# Fill in formatting defaults |
|
| 764 | ||
| 765 | 566x |
x_stats <- x_stats[.stats_adj] |
| 766 | ||
| 767 | 566x |
levels_per_stats <- lapply(x_stats, names) |
| 768 | ||
| 769 |
# Fill in formats/indents/labels with custom input and defaults |
|
| 770 | 566x |
.formats <- junco_get_formats_from_stats(.stats_adj, .formats, levels_per_stats) |
| 771 | ||
| 772 |
# lbls |
|
| 773 | 566x |
all_stats <- junco_get_stats("a_freq_j", stats_in = NULL, custom_stats_in = NULL)
|
| 774 | 566x |
count_rr_stats <- grep("(count|rr_ci)", all_stats, value = TRUE)
|
| 775 | 566x |
n_stats <- setdiff(all_stats, count_rr_stats) |
| 776 | 566x |
n_stats <- .stats_adj[.stats_adj %in% n_stats] |
| 777 | ||
| 778 | 566x |
if (length(.stats_adj) == 1 && length(n_stats) == 1) {
|
| 779 | 47x |
if (!is.null(labelstr) && (!is.null(label))) {
|
| 780 | ! |
lbls <- label |
| 781 | 47x |
} else if (!is.null(labelstr) && !is.null(.labels_n)) {
|
| 782 | ! |
lbls <- .labels_n |
| 783 | 47x |
} else if (!is.null(labelstr) && is.null(label_fstr)) {
|
| 784 | 42x |
lbls <- labelstr |
| 785 | 5x |
} else if (!is.null(labelstr) && !is.null(label_fstr) && grepl("%s", label_fstr, fixed = TRUE)) {
|
| 786 | ! |
lbls <- sprintf(label_fstr, labelstr) |
| 787 | 5x |
} else if (!is.null(label)) {
|
| 788 | 3x |
lbls <- label |
| 789 |
} else {
|
|
| 790 | 2x |
.labels <- .labels_n |
| 791 | 2x |
lbls <- junco_get_labels_from_stats(.stats_adj, .labels, levels_per_stats) |
| 792 |
} |
|
| 793 |
} else {
|
|
| 794 | 519x |
if (length(n_stats) > 1 && is.null(.labels_n)) {
|
| 795 | ! |
msg <- paste0( |
| 796 | ! |
"recommend to specify non-null .labels_n argument when multiple n_stats selected (",
|
| 797 | ! |
paste0(n_stats, collapse = ", "), |
| 798 |
")" |
|
| 799 |
) |
|
| 800 | ! |
message(msg) |
| 801 |
} |
|
| 802 |
### labels from the non n-stat statistics are present on levels_per_stats labels for the n-stat statistics come |
|
| 803 |
### from .labels_n (user) or from defaults |
|
| 804 | 519x |
.labels <- .labels_n |
| 805 | 519x |
lbls <- junco_get_labels_from_stats(.stats_adj, .labels, levels_per_stats) |
| 806 |
} |
|
| 807 | 566x |
.labels <- lbls |
| 808 | 566x |
.labels <- .unlist_keep_nulls(.labels) |
| 809 | ||
| 810 |
# indents |
|
| 811 | 566x |
.indent_mods_orig <- .indent_mods |
| 812 | 566x |
.indent_mods <- junco_get_indents_from_stats(.stats_adj, .indent_mods, levels_per_stats) |
| 813 | ||
| 814 |
# adjust indents when n_stat is included and no indents where passed by user |
|
| 815 | 566x |
if (length(n_stats) >= 1 && is.null(.indent_mods_orig)) {
|
| 816 | 47x |
.indent_mods_new <- sapply( |
| 817 | 47x |
names(.indent_mods), |
| 818 | 47x |
FUN = function(x) {
|
| 819 | 49x |
ret <- 0 |
| 820 | 4x |
if (!(x %in% n_stats)) ret <- 1 |
| 821 | 49x |
return(ret) |
| 822 |
}, |
|
| 823 | 47x |
simplify = FALSE, |
| 824 | 47x |
USE.NAMES = TRUE |
| 825 |
) |
|
| 826 | 47x |
.indent_mods <- .indent_mods_new |
| 827 |
} |
|
| 828 | ||
| 829 | 566x |
.indent_mods <- .unlist_keep_nulls(.indent_mods) |
| 830 | ||
| 831 |
# .format_na_strs processing if na_str = c(NA, NA, NA) this will ensure the ci (NA, NA, NA) will be represented as |
|
| 832 |
# NE (NE, NE) the value NE is defined as the default to replace NA in our jjcs format |
|
| 833 | ||
| 834 | 566x |
if (!is.null(na_str)) {
|
| 835 |
# Create a list of na_str values for each format in .formats |
|
| 836 | 566x |
.format_na_strs <- lapply(names(.formats), FUN = function(x) {
|
| 837 | 2474x |
na_str |
| 838 |
}) |
|
| 839 | 566x |
names(.format_na_strs) <- names(.formats) |
| 840 |
} else {
|
|
| 841 | ! |
.format_na_strs <- NULL |
| 842 |
} |
|
| 843 | ||
| 844 |
# Unlist stats + names |
|
| 845 | 566x |
x_stats <- .unlist_keep_nulls(x_stats) |
| 846 | 566x |
names(x_stats) <- names(.formats) |
| 847 | ||
| 848 | 566x |
return(list( |
| 849 | 566x |
x_stats = x_stats, |
| 850 | 566x |
.formats = .formats, |
| 851 | 566x |
.labels = .labels, |
| 852 | 566x |
.indent_mods = .indent_mods, |
| 853 | 566x |
.format_na_strs = .format_na_strs |
| 854 |
)) |
|
| 855 |
} |
|
| 856 | ||
| 857 |
#' Subset Combination |
|
| 858 |
#' |
|
| 859 |
#' Subsets a data frame based on specified combination criteria. |
|
| 860 |
#' @noRd |
|
| 861 |
#' @param df Data frame to subset. |
|
| 862 |
#' @param combosdf Data frame containing combinations. |
|
| 863 |
#' @param do_not_filter Variables to not filter. |
|
| 864 |
#' @param filter_var Variable used for filtering. |
|
| 865 |
#' @param flag_var Flag variable for filtering. |
|
| 866 |
#' @param colid Column ID for identification. |
|
| 867 |
#' @return Subsetted data frame. |
|
| 868 |
h_subset_combo <- function(df, combosdf, do_not_filter, filter_var, flag_var, colid) {
|
|
| 869 |
### this is the core code for subsetting to appropriate combo level |
|
| 870 | 15x |
if (!is.null(flag_var)) {
|
| 871 | 14x |
df <- df[df[[flag_var]] %in% "Y", ] |
| 872 |
} |
|
| 873 | ||
| 874 |
# get the string related to combosdf text from colid it is the last part of the column id after the . eg 'Active |
|
| 875 |
# Study Agent.Xanomeline High Dose.Thru 3 months' colid_str is 'Thru 3 months' |
|
| 876 |
# colid_str <- stringr::str_split_i(colid, |
|
| 877 |
# '\\.', i = -1) |
|
| 878 | 15x |
colid_str <- tail(unlist(strsplit(colid, "\\.")), 1) |
| 879 | ||
| 880 | 15x |
filter_val <- combosdf[combosdf$valname == colid_str, ]$label |
| 881 | ||
| 882 | 15x |
if (!(colid_str %in% do_not_filter)) {
|
| 883 | 11x |
df <- df[df[[filter_var]] %in% filter_val, ] |
| 884 |
} |
|
| 885 | ||
| 886 | 15x |
return(df) |
| 887 |
} |
|
| 888 | ||
| 889 |
#' @noRd |
|
| 890 |
#' @param df_row Data frame row to update. |
|
| 891 |
#' @param .var Variable name. |
|
| 892 |
#' @param label_map Mapping for labels. |
|
| 893 |
#' @param split_info Current split information. |
|
| 894 |
#' |
|
| 895 |
#' @return Restriction of val to appropriate levels from `label_map`. |
|
| 896 |
h_restrict_val <- function(df_row, .var, label_map, split_info) {
|
|
| 897 | 27x |
val <- NULL |
| 898 | 27x |
xval_row <- unique(df_row[[.var]]) |
| 899 | 27x |
xval_map <- label_map$value |
| 900 | ||
| 901 | 27x |
diff <- setdiff(xval_row, xval_map) |
| 902 | ||
| 903 | 27x |
if (length(diff) == 0) {
|
| 904 |
# If label_map has a variable from row split, apply |
|
| 905 |
# current splits on label_map tibble as well. |
|
| 906 | 26x |
rowsplits <- split_info$split |
| 907 | ||
| 908 | 26x |
label_map_split <- intersect(names(label_map), rowsplits) |
| 909 | ||
| 910 | 26x |
if (length(label_map_split) != 0) {
|
| 911 | 8x |
for (i in seq_along(label_map_split)) {
|
| 912 | 8x |
cursplvar <- label_map_split[i] |
| 913 | 8x |
cid <- match(cursplvar, rowsplits) |
| 914 | 8x |
cursplval <- split_info$value[cid] |
| 915 | ||
| 916 | 8x |
label_map <- label_map[label_map[[cursplvar]] == cursplval, ] |
| 917 |
} |
|
| 918 |
} |
|
| 919 | ||
| 920 | 26x |
if ("var" %in% names(label_map)) {
|
| 921 | ! |
label_map <- label_map[label_map[["var"]] == .var, ] |
| 922 |
} |
|
| 923 | ||
| 924 | 26x |
val <- label_map$value |
| 925 |
} |
|
| 926 | 27x |
val |
| 927 |
} |
|
| 928 | ||
| 929 |
#' @noRd |
|
| 930 |
#' @param denom_df denominator dataframe for relative risk calculation |
|
| 931 |
#' @param trt_var variable used as treatment variable |
|
| 932 |
#' @param cur_trt_grp current treatment group level |
|
| 933 |
#' @param .spl_context Current split context. |
|
| 934 |
#' |
|
| 935 |
#' @return Denominator dataframe with updated treatment variable to current treatment group level if this was |
|
| 936 |
#' coming from a combined facet |
|
| 937 |
#' |
|
| 938 |
upd_denom_df_combo <- function( |
|
| 939 |
denom_df, |
|
| 940 |
trt_var, |
|
| 941 |
cur_trt_grp, |
|
| 942 |
.spl_context |
|
| 943 |
) {
|
|
| 944 |
# check if we are in a combo facet, trt_var in that facet is not the same as in denom_df |
|
| 945 | 69x |
in_combo <- !(cur_trt_grp %in% unique(denom_df[[trt_var]])) |
| 946 | 69x |
if (in_combo) {
|
| 947 | 7x |
colexpr_substr_combo <- h_colexpr_substr(trt_var, .spl_context$cur_col_expr[[1]]) |
| 948 |
# adjust denom_df to update values of trt_var to reflect the combo value as in df |
|
| 949 | 7x |
y1 <- subset(denom_df, eval(parse(text = colexpr_substr_combo))) |
| 950 | 7x |
y1[[trt_var]] <- cur_trt_grp |
| 951 | 7x |
y2 <- subset(denom_df, eval(parse(text = paste0("!", colexpr_substr_combo))))
|
| 952 | 7x |
denom_df <- rbind(y1, y2) |
| 953 |
} |
|
| 954 | 69x |
denom_df |
| 955 |
} |
| 1 |
#' @title Analysis Function: Sum and Ratio of Sums |
|
| 2 |
#' |
|
| 3 |
#' @description `r lifecycle::badge("stable")`
|
|
| 4 |
#' |
|
| 5 |
#' Computes one or more of the following statistics for use with |
|
| 6 |
#' [rtables::analyze()]: |
|
| 7 |
#' - `"sum"`: `sum(.var)` |
|
| 8 |
#' - `"sum_unique"`: `sum(.var)` after keeping unique rows by `id_var` |
|
| 9 |
#' - `"ratio"`: `sum(.var) / sum(denom_by)` |
|
| 10 |
#' - `"ratio_unique"`: same ratio after keeping unique rows by `id_var` |
|
| 11 |
#' |
|
| 12 |
#' Both `.var` and `denom_by` columns must be numeric (integer, double, or |
|
| 13 |
#' logical coerced to numeric). |
|
| 14 |
#' |
|
| 15 |
#' @inheritParams proposal_argument_convention |
|
| 16 |
#' @param .stats (`character`)\cr one or more of `"sum"`, `"sum_unique"`, |
|
| 17 |
#' `"ratio"`, `"ratio_unique"`. Default: `"sum"`. |
|
| 18 |
#' @param denom_by (`string` or `NULL`)\cr denominator column; required for |
|
| 19 |
#' `"ratio"` and `"ratio_unique"`. |
|
| 20 |
#' @param id_var (`string` or `NULL`)\cr subject-id column for unique row selection; |
|
| 21 |
#' required for `"sum_unique"` and `"ratio_unique"`. |
|
| 22 |
#' @param .indent_mods (named `integer` or `NULL`)\cr indent modifiers for the |
|
| 23 |
#' row labels. Defaults to `NULL` (no modification). |
|
| 24 |
#' @param na_str (`character` or `NULL`)\cr string(s) used to replace `NA` |
|
| 25 |
#' values in the formatted output. Defaults to `NULL` (no replacement). |
|
| 26 |
#' |
|
| 27 |
#' @return A `RowsVerticalSection` for use by rtables. |
|
| 28 |
#' |
|
| 29 |
#' @author VR |
|
| 30 |
#' |
|
| 31 |
#' @importFrom rtables in_rows |
|
| 32 |
#' @export |
|
| 33 |
#' |
|
| 34 |
#' @examples |
|
| 35 |
#' df <- data.frame( |
|
| 36 |
#' USUBJID = c("S01", "S01", "S02", "S03"),
|
|
| 37 |
#' ARM = factor(c("A", "A", "A", "B")),
|
|
| 38 |
#' EVENTS = c(1, 1, 0, 1), |
|
| 39 |
#' DAYS = c(10, 10, 20, 15) |
|
| 40 |
#' ) |
|
| 41 |
#' |
|
| 42 |
#' # All 4 stats in a layout (S01 duplicate tests unique logic) |
|
| 43 |
#' lyt <- basic_table() |> |
|
| 44 |
#' split_cols_by("ARM") |>
|
|
| 45 |
#' analyze( |
|
| 46 |
#' "EVENTS", |
|
| 47 |
#' afun = a_sum_ratio_j, |
|
| 48 |
#' extra_args = list( |
|
| 49 |
#' .stats = c("sum", "sum_unique", "ratio", "ratio_unique"),
|
|
| 50 |
#' denom_by = "DAYS", |
|
| 51 |
#' id_var = "USUBJID" |
|
| 52 |
#' ) |
|
| 53 |
#' ) |
|
| 54 |
#' |
|
| 55 |
#' build_table(lyt, df) |
|
| 56 |
a_sum_ratio_j <- function( |
|
| 57 |
df, |
|
| 58 |
.var, |
|
| 59 |
.stats = "sum", |
|
| 60 |
denom_by = NULL, |
|
| 61 |
id_var = NULL, |
|
| 62 |
.formats = NULL, |
|
| 63 |
.labels = NULL, |
|
| 64 |
.indent_mods = NULL, |
|
| 65 |
na_str = NULL, |
|
| 66 |
... |
|
| 67 |
) {
|
|
| 68 |
# Data Validation ------------------------------------------------------------ |
|
| 69 | 18x |
valid_stats <- c("sum", "sum_unique", "ratio", "ratio_unique")
|
| 70 | ||
| 71 | 18x |
checkmate::assert_data_frame(df) |
| 72 | 18x |
checkmate::assert_string(.var) |
| 73 | 18x |
checkmate::assert_names(colnames(df), must.include = .var) |
| 74 | 18x |
checkmate::assert_numeric(df[[.var]], .var.name = .var) |
| 75 | 17x |
checkmate::assert_subset(.stats, choices = valid_stats) |
| 76 | ||
| 77 | 16x |
needs_ratio <- any(.stats %in% c("ratio", "ratio_unique"))
|
| 78 | 16x |
needs_unique <- any(.stats %in% c("sum_unique", "ratio_unique"))
|
| 79 | ||
| 80 | 16x |
if (needs_ratio) {
|
| 81 | 11x |
checkmate::assert_string(denom_by) |
| 82 | 10x |
checkmate::assert_names(colnames(df), must.include = denom_by) |
| 83 | 10x |
checkmate::assert_numeric(df[[denom_by]], .var.name = denom_by) |
| 84 |
} |
|
| 85 | 14x |
if (needs_unique) {
|
| 86 | 3x |
checkmate::assert_string(id_var) |
| 87 | 2x |
checkmate::assert_names(colnames(df), must.include = id_var) |
| 88 |
} |
|
| 89 | ||
| 90 |
# Helper funcs ----------------------------------------------- |
|
| 91 | 13x |
sum_plain <- function(v) {
|
| 92 | 22x |
sum(df[[v]], na.rm = TRUE) |
| 93 |
} |
|
| 94 | ||
| 95 | 13x |
sum_unique <- function(v) {
|
| 96 | 4x |
d <- unique(df[!is.na(df[[v]]), c(id_var, v), drop = FALSE]) |
| 97 | 4x |
sum(d[[v]], na.rm = TRUE) |
| 98 |
} |
|
| 99 | ||
| 100 | 13x |
safe_ratio <- function(n, d) {
|
| 101 | 11x |
if (is.na(d) || d == 0) {
|
| 102 | 3x |
c(n, NA_real_) |
| 103 |
} else {
|
|
| 104 | 8x |
c(n, n / d) |
| 105 |
} |
|
| 106 |
} |
|
| 107 | ||
| 108 |
# Default formats and labels ------------------------------------------------- |
|
| 109 | 13x |
default_fmt <- c( |
| 110 | 13x |
sum = "xx", |
| 111 | 13x |
sum_unique = "xx", |
| 112 | 13x |
ratio = "xx (xx.x%)", |
| 113 | 13x |
ratio_unique = "xx (xx.x%)" |
| 114 |
) |
|
| 115 | 13x |
default_lbl <- c( |
| 116 | 13x |
sum = "Sum", |
| 117 | 13x |
sum_unique = "Sum (unique)", |
| 118 | 13x |
ratio = "Ratio", |
| 119 | 13x |
ratio_unique = "Ratio (unique)" |
| 120 |
) |
|
| 121 | ||
| 122 |
# Compute stats --------------------------------------------------------------- |
|
| 123 | 13x |
n_plain <- if (any(.stats %in% c("sum", "ratio"))) {
|
| 124 | 13x |
sum_plain(.var) |
| 125 |
} else {
|
|
| 126 | ! |
NULL |
| 127 |
} |
|
| 128 | 13x |
n_unique <- if (any(.stats %in% c("sum_unique", "ratio_unique"))) {
|
| 129 | 2x |
sum_unique(.var) |
| 130 |
} else {
|
|
| 131 | 11x |
NULL |
| 132 |
} |
|
| 133 | 13x |
d_plain <- if (any(.stats %in% c("ratio")) && !is.null(denom_by)) {
|
| 134 | 9x |
sum_plain(denom_by) |
| 135 |
} else {
|
|
| 136 | 4x |
NULL |
| 137 |
} |
|
| 138 | 13x |
d_unique <- if (any(.stats %in% c("ratio_unique")) && !is.null(denom_by)) {
|
| 139 | 2x |
sum_unique(denom_by) |
| 140 |
} else {
|
|
| 141 | 11x |
NULL |
| 142 |
} |
|
| 143 | ||
| 144 |
# Build result list --------------------------------------------------------------- |
|
| 145 | 13x |
x_stats <- list() |
| 146 | 13x |
if ("sum" %in% .stats) {
|
| 147 | 10x |
x_stats[["sum"]] <- n_plain |
| 148 |
} |
|
| 149 | 13x |
if ("sum_unique" %in% .stats) {
|
| 150 | 2x |
x_stats[["sum_unique"]] <- n_unique |
| 151 |
} |
|
| 152 | 13x |
if ("ratio" %in% .stats) {
|
| 153 | 9x |
x_stats[["ratio"]] <- safe_ratio(n_plain, d_plain) |
| 154 |
} |
|
| 155 | 13x |
if ("ratio_unique" %in% .stats) {
|
| 156 | 2x |
x_stats[["ratio_unique"]] <- safe_ratio(n_unique, d_unique) |
| 157 |
} |
|
| 158 | ||
| 159 |
# Resolve formats and labels ------------------------------------------ |
|
| 160 | 13x |
fmts <- default_fmt[names(x_stats)] |
| 161 | 13x |
labels <- default_lbl[names(x_stats)] |
| 162 | 13x |
if (!is.null(.formats)) {
|
| 163 | 2x |
fmts[names(.formats)] <- .formats |
| 164 |
} |
|
| 165 | 13x |
if (!is.null(.labels)) {
|
| 166 | 2x |
labels[names(.labels)] <- .labels |
| 167 |
} |
|
| 168 | ||
| 169 | 13x |
.format_na_strs <- if (!is.null(na_str)) {
|
| 170 | ! |
lapply(names(fmts), function(x) na_str) |
| 171 |
} else {
|
|
| 172 | 13x |
NULL |
| 173 |
} |
|
| 174 | ||
| 175 | 13x |
in_rows( |
| 176 | 13x |
.list = x_stats, |
| 177 | 13x |
.formats = as.list(fmts), |
| 178 | 13x |
.names = labels, |
| 179 | 13x |
.labels = labels, |
| 180 | 13x |
.indent_mods = .indent_mods, |
| 181 | 13x |
.format_na_strs = .format_na_strs |
| 182 |
) |
|
| 183 |
} |
| 1 |
#' Split Function Factory for the Response Tables (RESP01) |
|
| 2 |
#' |
|
| 3 |
#' The main purpose here is to have a column dependent split into either comparative |
|
| 4 |
#' statistic (relative risk or odds ratio with p-value) in the 'Overall' column, |
|
| 5 |
#' and count proportions and corresponding confidence intervals in the other treatment |
|
| 6 |
#' arm columns. |
|
| 7 |
#' |
|
| 8 |
#' @inheritParams proposal_argument_convention |
|
| 9 |
#' @param method (`string`)\cr which method to use for the comparative statistics. |
|
| 10 |
#' |
|
| 11 |
#' @return A split function for use in the response table RESP01 and similar ones. |
|
| 12 |
#' @seealso [rtables::make_split_fun()] describing the requirements for this kind of |
|
| 13 |
#' post-processing function. |
|
| 14 |
#' @export |
|
| 15 |
#' |
|
| 16 |
#' @examples |
|
| 17 |
#' split_fun <- resp01_split_fun_fct( |
|
| 18 |
#' method = "or_cmh", |
|
| 19 |
#' conf_level = 0.95 |
|
| 20 |
#' ) |
|
| 21 |
resp01_split_fun_fct <- function(method = c("rr", "or_logistic", "or_cmh"), conf_level) {
|
|
| 22 | 2x |
method <- match.arg(method) |
| 23 | 2x |
(assert_proportion_value)(conf_level) |
| 24 | ||
| 25 | 2x |
post_fun <- function(ret, spl, fulldf, .spl_context) {
|
| 26 | 8x |
all_expr <- expression(TRUE) |
| 27 |
# Here check if we are in 'Overall' column or not. |
|
| 28 | 8x |
this_col <- .spl_context[nrow(.spl_context), "value"][[1]] |
| 29 | 8x |
in_overall <- this_col == "Overall" |
| 30 |
# Accordingly, return left and right column labels and value identifiers. |
|
| 31 | 8x |
if (in_overall) {
|
| 32 |
# In the Overall column, we want the comparison statistic and corresponding p-value. |
|
| 33 | 2x |
comp_stat_name <- if (method == "rr") "Relative Risk" else "Odds Ratio" |
| 34 | 2x |
short_split_result( |
| 35 | 2x |
comp_stat_ci = paste0(comp_stat_name, " (", f_conf_level(conf_level), ")~[super a]"),
|
| 36 | 2x |
pval = "p-value~[super b]", |
| 37 | 2x |
fulldf = fulldf |
| 38 |
) |
|
| 39 |
} else {
|
|
| 40 |
# In the treatment arm columns, we want counts and CI for proportions. |
|
| 41 | 6x |
short_split_result( |
| 42 | 6x |
count_prop = "n (%)", |
| 43 | 6x |
prop_ci = paste0(f_conf_level(conf_level), " for %"), |
| 44 | 6x |
fulldf = fulldf |
| 45 |
) |
|
| 46 |
} |
|
| 47 |
} |
|
| 48 | 2x |
make_split_fun(post = list(post_fun)) |
| 49 |
} |
|
| 50 | ||
| 51 |
#' Content Row Function for Counts of Subgroups in Response Tables (RESP01) |
|
| 52 |
#' |
|
| 53 |
#' @inheritParams proposal_argument_convention |
|
| 54 |
#' @param .alt_df (`data.frame`)\cr alternative data frame used for denominator calculation. |
|
| 55 |
#' @param label_fstr (`string`)\cr format string for the label. |
|
| 56 |
#' |
|
| 57 |
#' @return The correct [rtables::in_rows()] result. |
|
| 58 |
#' @export |
|
| 59 |
#' |
|
| 60 |
#' @examples |
|
| 61 |
#' fake_spl_context <- data.frame( |
|
| 62 |
#' cur_col_split_val = I(list(c(ARM = "A: Drug X", count_prop = "count_prop"))) |
|
| 63 |
#' ) |
|
| 64 |
#' resp01_counts_cfun( |
|
| 65 |
#' df = DM, |
|
| 66 |
#' labelstr = "Blue", |
|
| 67 |
#' .spl_context = fake_spl_context, |
|
| 68 |
#' .alt_df = DM, |
|
| 69 |
#' label_fstr = "Color: %s" |
|
| 70 |
#' ) |
|
| 71 |
resp01_counts_cfun <- function(df, labelstr, .spl_context, .alt_df, label_fstr) {
|
|
| 72 | 3x |
this_col_split <- .spl_context[nrow(.spl_context), "cur_col_split_val"][[1]] |
| 73 | 3x |
if (this_col_split[1] != "Overall" && this_col_split[2] == "count_prop") {
|
| 74 | 1x |
in_rows(nrow(.alt_df), .formats = "xx", .labels = sprintf(label_fstr, labelstr)) |
| 75 |
} else {
|
|
| 76 | 2x |
NULL |
| 77 |
} |
|
| 78 |
} |
|
| 79 | ||
| 80 |
#' Formatted Analysis Function for Comparative Statistic in Response Tables (RESP01) |
|
| 81 |
#' |
|
| 82 |
#' This function applies to a `logical` column called `.var` from `df`. |
|
| 83 |
#' The response proportion is compared between the treatment arms identified |
|
| 84 |
#' by column `arm`. |
|
| 85 |
#' |
|
| 86 |
#' @inheritParams proposal_argument_convention |
|
| 87 |
#' @param include (`flag`)\cr whether to include the results for this variable. |
|
| 88 |
#' @param arm (`string`)\cr column name in the data frame that identifies the treatment arms. |
|
| 89 |
#' @param formats (`list`)\cr containing formats for `comp_stat_ci` and `pval`. |
|
| 90 |
#' @param methods (`list`)\cr containing methods for comparative statistics. The element `comp_stat_ci` can be |
|
| 91 |
#' 'rr' (relative risk), 'or_cmh' (odds ratio with CMH estimation and p-value) or 'or_logistic' (odds ratio |
|
| 92 |
#' estimated by conditional or standard logistic regression). The element `pval` can be 'fisher' (Fisher's |
|
| 93 |
#' exact test) or 'chisq' (chi-square test), only used when using unstratified analyses with 'or_logistic'. |
|
| 94 |
#' @param stat (`string`)\cr the statistic to return, either `comp_stat_ci` |
|
| 95 |
#' or `pval`. |
|
| 96 |
#' |
|
| 97 |
#' @return The formatted result as [rtables::rcell()]. |
|
| 98 |
#' @seealso [resp01_a_comp_stat_factor()] for the `factor` equivalent. |
|
| 99 |
#' @export |
|
| 100 |
#' |
|
| 101 |
#' @examples |
|
| 102 |
#' dm <- droplevels(subset(formatters::DM, SEX %in% c("F", "M")))
|
|
| 103 |
#' dm$RESP <- as.logical(sample(c(TRUE, FALSE), size = nrow(DM), replace = TRUE)) |
|
| 104 |
#' |
|
| 105 |
#' resp01_a_comp_stat_logical( |
|
| 106 |
#' dm, |
|
| 107 |
#' .var = "RESP", |
|
| 108 |
#' conf_level = 0.9, |
|
| 109 |
#' include = TRUE, |
|
| 110 |
#' arm = "SEX", |
|
| 111 |
#' strata = "RACE", |
|
| 112 |
#' stat = "comp_stat_ci", |
|
| 113 |
#' methods = list(comp_stat_ci = "or_cmh"), |
|
| 114 |
#' formats = list( |
|
| 115 |
#' comp_stat_ci = jjcsformat_xx("xx.xx (xx.xx - xx.xx)"),
|
|
| 116 |
#' pval = jjcsformat_pval_fct(0.05) |
|
| 117 |
#' ) |
|
| 118 |
#' ) |
|
| 119 |
resp01_a_comp_stat_logical <- function(df, |
|
| 120 |
.var, |
|
| 121 |
conf_level, |
|
| 122 |
include, |
|
| 123 |
arm, |
|
| 124 |
strata, |
|
| 125 |
formats, |
|
| 126 |
methods, |
|
| 127 |
stat = c("comp_stat_ci", "pval")) {
|
|
| 128 | 31x |
checkmate::assert_logical(df[[.var]]) |
| 129 | 31x |
checkmate::assert_factor(df[[arm]], n.levels = 2L) |
| 130 | 31x |
checkmate::assert_flag(include) |
| 131 | 31x |
stat <- match.arg(stat) |
| 132 | ||
| 133 | 31x |
if (include) {
|
| 134 | 10x |
checkmate::assert_list(methods) |
| 135 | 10x |
checkmate::assert_subset(methods$comp_stat_ci, c("rr", "or_cmh", "or_logistic"))
|
| 136 | ||
| 137 | 10x |
df_by <- split(df, f = df[[arm]]) |
| 138 | 10x |
this_df <- df_by[[1]] |
| 139 | 10x |
this_ref_group <- df_by[[2]] |
| 140 | ||
| 141 | 10x |
this_res <- if (methods$comp_stat_ci == "rr") {
|
| 142 |
# Stratified CMH test for point estimate and p-value, and Wald statistic for the confidence interval |
|
| 143 | 1x |
s_res <- s_relative_risk( |
| 144 | 1x |
this_df, |
| 145 | 1x |
.var = .var, |
| 146 | 1x |
.ref_group = this_ref_group, |
| 147 | 1x |
.in_ref_col = FALSE, |
| 148 | 1x |
variables = list(strata = strata), |
| 149 | 1x |
conf_level = conf_level |
| 150 |
) |
|
| 151 | 1x |
list(comp_stat_ci = s_res$rel_risk_ci, pval = s_res$pval) |
| 152 | 10x |
} else if (methods$comp_stat_ci == "or_logistic") {
|
| 153 |
# Odds Ratio Variant 1: (Conditional/Standard) Logistic regression for odds ratio point estimate and |
|
| 154 |
# confidence interval. |
|
| 155 | 3x |
s_res <- s_odds_ratio_j( |
| 156 | 3x |
this_df, |
| 157 | 3x |
.var = .var, |
| 158 | 3x |
.ref_group = this_ref_group, |
| 159 | 3x |
.in_ref_col = FALSE, |
| 160 | 3x |
.df_row = rbind(this_df, this_ref_group), |
| 161 | 3x |
variables = list(arm = arm, strata = strata), |
| 162 | 3x |
conf_level = conf_level, |
| 163 | 3x |
method = "exact" # Only used if strata are present. |
| 164 |
) |
|
| 165 | 3x |
pval <- if (is.null(strata)) {
|
| 166 | 3x |
checkmate::assert_subset(methods$pval, c("fisher", "chisq"))
|
| 167 | ||
| 168 |
# If not stratified, then the p-value can come either from Fisher's exact or chi-square test. |
|
| 169 | 3x |
p_res <- s_test_proportion_diff( |
| 170 | 3x |
this_df, |
| 171 | 3x |
.var = .var, |
| 172 | 3x |
.ref_group = this_ref_group, |
| 173 | 3x |
.in_ref_col = FALSE, |
| 174 | 3x |
method = methods$pval |
| 175 |
) |
|
| 176 | 3x |
p_res$pval |
| 177 |
} else {
|
|
| 178 | ! |
s_res$pval |
| 179 |
} |
|
| 180 | 3x |
list(comp_stat_ci = s_res$or_ci, pval = pval) |
| 181 | 10x |
} else if (methods$comp_stat_ci == "or_cmh") {
|
| 182 |
# Odds Ratio Variant 2: Stratified CMH test for odds ratio point estimate and confidence interval and |
|
| 183 |
# corresponding CMH test p-value. |
|
| 184 | 6x |
s_res <- s_odds_ratio_j( |
| 185 | 6x |
this_df, |
| 186 | 6x |
.var = .var, |
| 187 | 6x |
.ref_group = this_ref_group, |
| 188 | 6x |
.in_ref_col = FALSE, |
| 189 | 6x |
.df_row = rbind(this_df, this_ref_group), |
| 190 | 6x |
variables = list(arm = arm, strata = strata), |
| 191 | 6x |
conf_level = conf_level, |
| 192 | 6x |
method = "cmh" |
| 193 |
) |
|
| 194 | 6x |
list(comp_stat_ci = s_res$or_ci, pval = s_res$pval) |
| 195 |
} |
|
| 196 | 10x |
rcell(this_res[[stat]], format = formats[[stat]]) |
| 197 |
} else {
|
|
| 198 | 21x |
NULL |
| 199 |
} |
|
| 200 |
} |
|
| 201 | ||
| 202 |
#' Formatted Analysis Function for Comparative Statistic in Response Tables (RESP01) |
|
| 203 |
#' |
|
| 204 |
#' This function applies to a `factor` column called `.var` from `df`. |
|
| 205 |
#' |
|
| 206 |
#' @inheritParams proposal_argument_convention |
|
| 207 |
#' @param include (`character`)\cr for which factor levels to include the comparison |
|
| 208 |
#' statistic results. |
|
| 209 |
#' @param \dots see [resp01_a_comp_stat_logical()] for additional required arguments. |
|
| 210 |
#' |
|
| 211 |
#' @return The formatted result as [rtables::rcell()]. |
|
| 212 |
#' @export |
|
| 213 |
#' |
|
| 214 |
#' @examples |
|
| 215 |
#' dm <- droplevels(subset(formatters::DM, SEX %in% c("F", "M")))
|
|
| 216 |
#' |
|
| 217 |
#' resp01_a_comp_stat_factor( |
|
| 218 |
#' dm, |
|
| 219 |
#' .var = "COUNTRY", |
|
| 220 |
#' conf_level = 0.9, |
|
| 221 |
#' include = c("USA", "CHN"),
|
|
| 222 |
#' arm = "SEX", |
|
| 223 |
#' strata = "RACE", |
|
| 224 |
#' stat = "comp_stat_ci", |
|
| 225 |
#' methods = list(comp_stat_ci = "or_cmh"), |
|
| 226 |
#' formats = list( |
|
| 227 |
#' comp_stat_ci = jjcsformat_xx("xx.xx (xx.xx - xx.xx)"),
|
|
| 228 |
#' pval = jjcsformat_pval_fct(0.05) |
|
| 229 |
#' ) |
|
| 230 |
#' ) |
|
| 231 |
resp01_a_comp_stat_factor <- function(df, |
|
| 232 |
.var, |
|
| 233 |
include, |
|
| 234 |
...) {
|
|
| 235 | 3x |
checkmate::assert_factor(df[[.var]]) |
| 236 | 3x |
checkmate::assert_character(include, null.ok = TRUE) |
| 237 | 3x |
checkmate::assert_subset(x = include, choices = levels(df[[.var]])) |
| 238 | ||
| 239 | 3x |
design_df <- as.data.frame(h_get_design_mat(df, .var)) |
| 240 | ||
| 241 | 3x |
res <- sapply(X = levels(df[[.var]]), simplify = FALSE, FUN = function(lvl) {
|
| 242 | 27x |
df[[.var]] <- design_df[[lvl]] |
| 243 | 27x |
resp01_a_comp_stat_logical(df = df, .var = .var, include = lvl %in% include, ...) |
| 244 |
}) |
|
| 245 | 3x |
in_rows(.list = res, .labels = levels(df[[.var]])) |
| 246 |
} |
|
| 247 | ||
| 248 |
#' Formatted Analysis and Content Summary Function for Response Tables (RESP01) |
|
| 249 |
#' |
|
| 250 |
#' This function applies to both `factor` and `logical` columns called |
|
| 251 |
#' `.var` from `df`. Depending on the position in the split, it returns the |
|
| 252 |
#' right formatted results for the RESP01 and related layouts. |
|
| 253 |
#' |
|
| 254 |
#' @inheritParams proposal_argument_convention |
|
| 255 |
#' @inheritParams resp01_a_comp_stat_logical |
|
| 256 |
#' @param include_comp (`character` or `flag`)\cr whether to include comparative |
|
| 257 |
#' statistic results, either `character` for factors or `flag` for logicals. |
|
| 258 |
#' @param .alt_df (`data.frame`)\cr alternative data frame used for denominator calculation. |
|
| 259 |
#' @param arm (`string`)\cr column name in the data frame that identifies the treatment arms. |
|
| 260 |
#' @param label (`string`)\cr only for logicals, which label to use. (For factors, the |
|
| 261 |
#' labels are the factor levels.) |
|
| 262 |
#' @param formats (`list`)\cr containing formats for `prop_ci`, `comp_stat_ci` |
|
| 263 |
#' and `pval`. |
|
| 264 |
#' @param methods (`list`)\cr containing methods for comparative statistics. The element `comp_stat_ci` can be |
|
| 265 |
#' 'rr' (relative risk), 'or_cmh' (odds ratio with CMH estimation and p-value) or 'or_logistic' (odds ratio |
|
| 266 |
#' estimated by conditional or standard logistic regression). The element `pval` can be 'fisher' (Fisher's |
|
| 267 |
#' exact test) or 'chisq' (chi-square test), only used when using unstratified analyses with 'or_logistic'. |
|
| 268 |
#' The element `prop_ci` specifies the method for proportion confidence interval calculation. |
|
| 269 |
#' |
|
| 270 |
#' @return The formatted result as [rtables::in_rows()] result. |
|
| 271 |
#' @export |
|
| 272 |
#' |
|
| 273 |
#' @examples |
|
| 274 |
#' fake_spl_context <- data.frame( |
|
| 275 |
#' cur_col_split_val = I(list(c(ARM = "A: Drug X", count_prop = "count_prop"))) |
|
| 276 |
#' ) |
|
| 277 |
#' dm <- droplevels(subset(DM, SEX %in% c("F", "M")))
|
|
| 278 |
#' resp01_acfun( |
|
| 279 |
#' dm, |
|
| 280 |
#' .alt_df = dm, |
|
| 281 |
#' .var = "COUNTRY", |
|
| 282 |
#' .spl_context = fake_spl_context, |
|
| 283 |
#' conf_level = 0.9, |
|
| 284 |
#' include_comp = c("USA", "CHN"),
|
|
| 285 |
#' arm = "SEX", |
|
| 286 |
#' strata = "RACE", |
|
| 287 |
#' methods = list( |
|
| 288 |
#' comp_stat_ci = "or_cmh", |
|
| 289 |
#' pval = "", |
|
| 290 |
#' prop_ci = "wald" |
|
| 291 |
#' ), |
|
| 292 |
#' formats = list( |
|
| 293 |
#' prop_ci = jjcsformat_xx("xx.% - xx.%"),
|
|
| 294 |
#' comp_stat_ci = jjcsformat_xx("xx.xx (xx.xx - xx.xx)"),
|
|
| 295 |
#' pval = jjcsformat_pval_fct(0.05) |
|
| 296 |
#' ) |
|
| 297 |
#' ) |
|
| 298 |
#' fake_spl_context2 <- data.frame( |
|
| 299 |
#' cur_col_split_val = I(list(c(ARM = "Overall", comp_stat_ci = "comp_stat_ci"))) |
|
| 300 |
#' ) |
|
| 301 |
#' resp01_acfun( |
|
| 302 |
#' dm, |
|
| 303 |
#' .alt_df = dm, |
|
| 304 |
#' .var = "COUNTRY", |
|
| 305 |
#' .spl_context = fake_spl_context2, |
|
| 306 |
#' conf_level = 0.9, |
|
| 307 |
#' include_comp = c("USA", "CHN"),
|
|
| 308 |
#' arm = "SEX", |
|
| 309 |
#' strata = "RACE", |
|
| 310 |
#' methods = list( |
|
| 311 |
#' comp_stat_ci = "or_cmh", |
|
| 312 |
#' pval = "", |
|
| 313 |
#' prop_ci = "wald" |
|
| 314 |
#' ), |
|
| 315 |
#' formats = list( |
|
| 316 |
#' prop_ci = jjcsformat_xx("xx.% - xx.%"),
|
|
| 317 |
#' comp_stat_ci = jjcsformat_xx("xx.xx (xx.xx - xx.xx)"),
|
|
| 318 |
#' pval = jjcsformat_pval_fct(0.05) |
|
| 319 |
#' ) |
|
| 320 |
#' ) |
|
| 321 |
resp01_acfun <- function(df, |
|
| 322 |
labelstr = NULL, |
|
| 323 |
label = NULL, |
|
| 324 |
.var, |
|
| 325 |
.spl_context, |
|
| 326 |
include_comp, |
|
| 327 |
.alt_df, |
|
| 328 |
conf_level, |
|
| 329 |
arm, |
|
| 330 |
strata, |
|
| 331 |
formats, |
|
| 332 |
methods) {
|
|
| 333 | 3x |
this_col_split <- .spl_context[nrow(.spl_context), "cur_col_split_val"][[1]] |
| 334 | 3x |
x <- df[[.var]] |
| 335 | 3x |
is_factor <- is.factor(x) |
| 336 | 3x |
res <- if (this_col_split[1] != "Overall") {
|
| 337 | 1x |
if (this_col_split[2] == "count_prop") {
|
| 338 | 1x |
if (is_factor) {
|
| 339 | 1x |
s_proportion_factor(x, .alt_df = .alt_df) |
| 340 |
} else {
|
|
| 341 | ! |
s_proportion_logical(x, .alt_df = .alt_df)$n_prop |
| 342 |
} |
|
| 343 |
} else {
|
|
| 344 | ! |
checkmate::assert_true(this_col_split[2] == "prop_ci") |
| 345 | ! |
if (is_factor) {
|
| 346 | ! |
a_proportion_ci_factor( |
| 347 | ! |
df, |
| 348 | ! |
.var = .var, |
| 349 | ! |
.alt_df = .alt_df, |
| 350 | ! |
conf_level = conf_level, |
| 351 | ! |
formats = formats, |
| 352 | ! |
method = methods$prop_ci |
| 353 |
) |
|
| 354 |
} else {
|
|
| 355 | ! |
a_proportion_ci_logical( |
| 356 | ! |
x, |
| 357 | ! |
.alt_df = .alt_df, |
| 358 | ! |
conf_level = conf_level, |
| 359 | ! |
formats = formats, |
| 360 | ! |
method = methods$prop_ci |
| 361 |
) |
|
| 362 |
} |
|
| 363 |
} |
|
| 364 |
} else {
|
|
| 365 | 2x |
checkmate::assert_true(this_col_split[1] == "Overall") |
| 366 | 2x |
if (is_factor) {
|
| 367 | 1x |
resp01_a_comp_stat_factor( |
| 368 | 1x |
df, |
| 369 | 1x |
.var = .var, |
| 370 | 1x |
conf_level = conf_level, |
| 371 | 1x |
include = include_comp, |
| 372 | 1x |
arm = arm, |
| 373 | 1x |
strata = strata, |
| 374 | 1x |
stat = this_col_split[2], |
| 375 | 1x |
formats = formats, |
| 376 | 1x |
methods = methods |
| 377 |
) |
|
| 378 |
} else {
|
|
| 379 | 1x |
resp01_a_comp_stat_logical( |
| 380 | 1x |
df, |
| 381 | 1x |
.var = .var, |
| 382 | 1x |
conf_level = conf_level, |
| 383 | 1x |
include = include_comp, |
| 384 | 1x |
arm = arm, |
| 385 | 1x |
strata = strata, |
| 386 | 1x |
stat = this_col_split[2], |
| 387 | 1x |
formats = formats, |
| 388 | 1x |
methods = methods |
| 389 |
) |
|
| 390 |
} |
|
| 391 |
} |
|
| 392 | 3x |
if (is_factor) {
|
| 393 | 2x |
res |
| 394 |
} else {
|
|
| 395 | 1x |
if (!is.null(labelstr)) {
|
| 396 | 1x |
label <- labelstr |
| 397 |
} |
|
| 398 | 1x |
in_rows(res, .labels = label) |
| 399 |
} |
|
| 400 |
} |
| 1 |
#' @name a_two_tier |
|
| 2 |
#' |
|
| 3 |
#' @title Two Tier Analysis Function |
|
| 4 |
#' |
|
| 5 |
#' @author GB, WW. |
|
| 6 |
#' |
|
| 7 |
#' @description `a_two_tier()`: The analysis function used as an `afun` in \link[rtables]{analyze}.
|
|
| 8 |
#' This function simulates a final additional level of nesting with a |
|
| 9 |
#' traditional analyze call inside it. |
|
| 10 |
#' |
|
| 11 |
#' This makes it possible to create what *appear to be* group summary or |
|
| 12 |
#' content rows and to *optionally or conditionally* generate one or more |
|
| 13 |
#' "detail" rows underneath it. |
|
| 14 |
#' |
|
| 15 |
#' For example, in a disposition table, one might want counts for completed |
|
| 16 |
#' and ongoing patients with no further detail underneath, but a breakdown of |
|
| 17 |
#' specific reasons beneath the count of patients who discontinued treatment. |
|
| 18 |
#' |
|
| 19 |
#' @details `a_two_tier()`: Both the analysis variable and `inner_var` must be factors. |
|
| 20 |
#' Detail rows are differentiated by having an indent mod of one, causing them |
|
| 21 |
#' to hang indented under their corresponding group row. |
|
| 22 |
#' |
|
| 23 |
#' @note In its current form, this function may not retain any formatting or |
|
| 24 |
#' labeling instructions added by `grp_fun` or `detail_fun`, and it will |
|
| 25 |
#' override any `.indent_mods` values specified by them. This behavior may |
|
| 26 |
#' change in future versions. |
|
| 27 |
#' |
|
| 28 |
#' @inheritParams proposal_argument_convention |
|
| 29 |
#' @param inner_var (`string`)\cr single variable name to use when generating |
|
| 30 |
#' the detail rows. |
|
| 31 |
#' @param drill_down_levs (`character`)\cr the level(s) of `.var` under which |
|
| 32 |
#' detail rows should be generated. |
|
| 33 |
#' @param use_all_levels (`flag`)\cr controls which factor levels will be |
|
| 34 |
#' present for `inner_var` (both in `df`/`x` and in `.df_row`) when calling |
|
| 35 |
#' `detail_fun`. |
|
| 36 |
#' If `TRUE`, all levels (those present on the factor `.df_row[[inner_var]]`, |
|
| 37 |
#' *regardless if the level is observed in the row group or not) will be |
|
| 38 |
#' present when creating detail rows. |
|
| 39 |
#' Otherwise (the default), only the levels observed |
|
| 40 |
#' *anywhere in the row group, i.e., within `.df_row`* will be present. |
|
| 41 |
#' @param grp_fun (`function`)\cr analysis function to be used when generating |
|
| 42 |
#' the "group summary" outer rows. |
|
| 43 |
#' @param detail_fun (`function`)\cr analysis function to be used when generating |
|
| 44 |
#' "detail" inner rows. |
|
| 45 |
#' @param .alt_df_full (`dataframe`)\cr denominator dataset for fraction and |
|
| 46 |
#' relative risk calculations.\cr |
|
| 47 |
#' this argument gets populated by the rtables split machinery |
|
| 48 |
#' (see [rtables::additional_fun_params]). |
|
| 49 |
#' @param ... additional arguments passed directly to `grp_fun` and `detail_fun`. |
|
| 50 |
#' |
|
| 51 |
#' @return `a_two_tier()`: A `RowsVerticalSection` object including both the group row and all |
|
| 52 |
#' detail rows, if applicable, for the facet. |
|
| 53 |
#' |
|
| 54 |
#' @import checkmate |
|
| 55 |
#' @export |
|
| 56 |
#' |
|
| 57 |
#' @examples |
|
| 58 |
#' |
|
| 59 |
#' # Example 1 |
|
| 60 |
#' |
|
| 61 |
#' lyt_obs_levels <- basic_table() |> |
|
| 62 |
#' split_cols_by("ARM") |>
|
|
| 63 |
#' split_rows_by("EOSSTT", child_labels = "hidden") |>
|
|
| 64 |
#' analyze("EOSSTT",
|
|
| 65 |
#' afun = a_two_tier, |
|
| 66 |
#' extra_args = list( |
|
| 67 |
#' grp_fun = simple_analysis, |
|
| 68 |
#' detail_fun = simple_analysis, |
|
| 69 |
#' inner_var = "DCSREAS", |
|
| 70 |
#' drill_down_levs = "DISCONTINUED" |
|
| 71 |
#' ) |
|
| 72 |
#' ) |
|
| 73 |
#' |
|
| 74 |
#' tbl <- build_table(lyt_obs_levels, ex_adsl) |
|
| 75 |
#' tbl |
|
| 76 |
#' |
|
| 77 |
#' lyt_all_levels <- basic_table() |> |
|
| 78 |
#' split_cols_by("ARM") |>
|
|
| 79 |
#' split_rows_by("EOSSTT", child_labels = "hidden") |>
|
|
| 80 |
#' analyze("EOSSTT",
|
|
| 81 |
#' afun = a_two_tier, |
|
| 82 |
#' extra_args = list( |
|
| 83 |
#' grp_fun = simple_analysis, |
|
| 84 |
#' detail_fun = simple_analysis, |
|
| 85 |
#' inner_var = "DCSREAS", |
|
| 86 |
#' drill_down_levs = "DISCONTINUED", |
|
| 87 |
#' use_all_levels = TRUE |
|
| 88 |
#' ) |
|
| 89 |
#' ) |
|
| 90 |
#' |
|
| 91 |
#' adsl_subset <- subset(ex_adsl, DCSREAS != "ADVERSE EVENT") |
|
| 92 |
#' levels(adsl_subset$DCSREAS) |
|
| 93 |
#' |
|
| 94 |
#' tbl_all_levels <- build_table(lyt_all_levels, adsl_subset) |
|
| 95 |
#' tbl_all_levels |
|
| 96 |
#' |
|
| 97 |
#' tbl_obs_levels <- build_table(lyt_obs_levels, adsl_subset) |
|
| 98 |
#' tbl_obs_levels |
|
| 99 |
#' |
|
| 100 |
#' # Example 2 |
|
| 101 |
#' |
|
| 102 |
#' library(dplyr) |
|
| 103 |
#' |
|
| 104 |
#' trtvar <- "ARM" |
|
| 105 |
#' ctrl_grp <- "B: Placebo" |
|
| 106 |
#' |
|
| 107 |
#' adsl <- ex_adsl |> select(c("USUBJID", "STRATA1", "EOSSTT", "DCSREAS", all_of(trtvar)))
|
|
| 108 |
#' adsl$colspan_trt <- factor( |
|
| 109 |
#' ifelse(adsl[[trtvar]] == ctrl_grp, " ", "Active Study Agent"), |
|
| 110 |
#' levels = c("Active Study Agent", " ")
|
|
| 111 |
#' ) |
|
| 112 |
#' adsl$rrisk_header <- "Risk Difference (%) (95% CI)" |
|
| 113 |
#' adsl$rrisk_label <- paste(adsl[[trtvar]], paste("vs", ctrl_grp))
|
|
| 114 |
#' |
|
| 115 |
#' colspan_trt_map <- create_colspan_map( |
|
| 116 |
#' df = adsl, |
|
| 117 |
#' non_active_grp = ctrl_grp, |
|
| 118 |
#' non_active_grp_span_lbl = " ", |
|
| 119 |
#' active_grp_span_lbl = "Active Study Agent", |
|
| 120 |
#' colspan_var = "colspan_trt", |
|
| 121 |
#' trt_var = trtvar |
|
| 122 |
#' ) |
|
| 123 |
#' |
|
| 124 |
#' a_freq_j_args <- list( |
|
| 125 |
#' .stats = "count_unique_fraction", |
|
| 126 |
#' denom = "n_altdf", |
|
| 127 |
#' ref_path = c("colspan_trt", " ", trtvar, ctrl_grp)
|
|
| 128 |
#' ) |
|
| 129 |
#' |
|
| 130 |
#' two_tier_args <- list( |
|
| 131 |
#' grp_fun = a_freq_j, |
|
| 132 |
#' detail_fun = a_freq_j, |
|
| 133 |
#' inner_var = "DCSREAS", |
|
| 134 |
#' drill_down_levs = "DISCONTINUED" |
|
| 135 |
#' ) |
|
| 136 |
#' |
|
| 137 |
#' lyt_rrisk <- basic_table() |> |
|
| 138 |
#' split_cols_by("colspan_trt", split_fun = trim_levels_to_map(map = colspan_trt_map)) |>
|
|
| 139 |
#' split_cols_by(trtvar) |> |
|
| 140 |
#' split_cols_by("rrisk_header", nested = FALSE) |>
|
|
| 141 |
#' split_cols_by(trtvar, labels_var = "rrisk_label", split_fun = remove_split_levels(ctrl_grp)) |> |
|
| 142 |
#' split_rows_by("STRATA1") |>
|
|
| 143 |
#' split_rows_by("EOSSTT", child_labels = "hidden") |>
|
|
| 144 |
#' analyze("EOSSTT", afun = a_two_tier, extra_args = c(two_tier_args, a_freq_j_args))
|
|
| 145 |
#' |
|
| 146 |
#' adsl_subset <- subset( |
|
| 147 |
#' adsl, |
|
| 148 |
#' EOSSTT != "COMPLETED" & (is.na(DCSREAS) | DCSREAS != "PROTOCOL VIOLATION") |
|
| 149 |
#' ) |
|
| 150 |
#' |
|
| 151 |
#' tbl_rrisk <- build_table(lyt_rrisk, adsl_subset, alt_counts_df = adsl_subset) |
|
| 152 |
#' tbl_rrisk |
|
| 153 |
#' |
|
| 154 |
a_two_tier <- function(df, |
|
| 155 |
labelstr = NULL, |
|
| 156 |
.var, |
|
| 157 |
.N_col, |
|
| 158 |
.df_row, |
|
| 159 |
inner_var, |
|
| 160 |
drill_down_levs, |
|
| 161 |
.spl_context, |
|
| 162 |
use_all_levels = FALSE, |
|
| 163 |
grp_fun, |
|
| 164 |
detail_fun, |
|
| 165 |
.alt_df_full = NULL, |
|
| 166 |
...) {
|
|
| 167 | 72x |
assert_string(inner_var) |
| 168 | 72x |
assert_character(drill_down_levs) |
| 169 | 72x |
assert_flag(use_all_levels) |
| 170 | 72x |
assert_function(grp_fun) |
| 171 | 72x |
assert_function(detail_fun) |
| 172 | ||
| 173 | 72x |
if (!all(sapply(c(.var, inner_var), \(x) is.factor(df[[x]])))) {
|
| 174 | ! |
stop("a_two_tier requires both input variables .var and inner_var as factor")
|
| 175 |
} |
|
| 176 | ||
| 177 | 72x |
cur_grp <- tail(.spl_context$value, 1) |
| 178 | 72x |
df[[.var]] <- factor(df[[.var]], levels = cur_grp) |
| 179 | 72x |
.df_row[[.var]] <- factor(.df_row[[.var]], levels = cur_grp) |
| 180 | 72x |
args <- list( |
| 181 | 72x |
labelstr = labelstr, |
| 182 | 72x |
.var = .var, |
| 183 | 72x |
.N_col = .N_col, |
| 184 | 72x |
.df_row = .df_row, |
| 185 | 72x |
.spl_context = .spl_context, |
| 186 | 72x |
.alt_df_full = .alt_df_full, |
| 187 |
... |
|
| 188 |
) |
|
| 189 | 72x |
if (names(formals(grp_fun))[1] == "df") {
|
| 190 | ! |
args <- c(list(df = df), args) |
| 191 |
} else {
|
|
| 192 | 72x |
args <- c(list(x = df[[.var]]), args) |
| 193 |
} |
|
| 194 | 72x |
if (identical(grp_fun, a_freq_j)) {
|
| 195 |
# this will ensure row with cur_grp will be presented |
|
| 196 |
# even if 0 observed in current row |
|
| 197 | ! |
args[["drop_levels"]] <- FALSE |
| 198 | ! |
args <- c(list(val = cur_grp), args) |
| 199 |
} |
|
| 200 | 72x |
cell_vals <- unclass( |
| 201 | 72x |
do.call(grp_fun, args) |
| 202 |
) |
|
| 203 | 72x |
names(cell_vals) <- attr(cell_vals, "row_label") |
| 204 | ||
| 205 |
## calculate the drill-down values if necessary |
|
| 206 | 72x |
if (cur_grp %in% drill_down_levs && any(!is.na(.df_row[[inner_var]]))) {
|
| 207 |
## have to make sure we use all levels for the whole row group |
|
| 208 |
## so that each column gets the same number of values in the same order |
|
| 209 | 12x |
all_inner_levs <- levels(df[[inner_var]]) |
| 210 | 12x |
if (use_all_levels) {
|
| 211 | 3x |
detail_levs <- all_inner_levs |
| 212 |
} else {
|
|
| 213 | 9x |
detail_levs <- all_inner_levs[all_inner_levs %in% all_inner_levs[.df_row[[inner_var]]]] |
| 214 |
} |
|
| 215 | ||
| 216 | 12x |
inner_vec <- factor(df[[inner_var]], levels = detail_levs) |
| 217 | 12x |
df[[inner_var]] <- factor(df[[inner_var]], levels = detail_levs) |
| 218 | 12x |
.df_row[[inner_var]] <- factor(.df_row[[inner_var]], levels = detail_levs) |
| 219 | 12x |
det_args <- list( |
| 220 | 12x |
.var = inner_var, |
| 221 | 12x |
.N_col = .N_col, |
| 222 | 12x |
.df_row = .df_row, |
| 223 | 12x |
.spl_context = .spl_context, |
| 224 | 12x |
.alt_df_full = .alt_df_full, |
| 225 |
... |
|
| 226 |
) |
|
| 227 | 12x |
if (names(formals(detail_fun))[1] == "df") {
|
| 228 | ! |
det_args <- c(list(df = df), det_args) |
| 229 |
} else {
|
|
| 230 | 12x |
det_args <- c(list(x = df[[inner_var]]), det_args) |
| 231 |
} |
|
| 232 | ||
| 233 | 12x |
detail_vals <- unclass( |
| 234 | 12x |
do.call(detail_fun, det_args) |
| 235 |
) |
|
| 236 | ||
| 237 | 12x |
names(detail_vals) <- attr(detail_vals, "row_label") |
| 238 |
} else {
|
|
| 239 | 60x |
detail_vals <- list() |
| 240 |
} |
|
| 241 | 72x |
in_rows( |
| 242 | 72x |
.list = c(cell_vals, detail_vals), |
| 243 | 72x |
.indent_mods = c(0, rep(1, length.out = length(detail_vals))) |
| 244 |
) |
|
| 245 |
} |
|
| 246 | ||
| 247 | ||
| 248 |
#' @rdname a_two_tier |
|
| 249 |
#' |
|
| 250 |
#' @description `a_three_tier()`: Extends `a_two_tier()` by adding a third level |
|
| 251 |
#' of nesting: a group summary outer row, mid-level detail rows (one per level |
|
| 252 |
#' of `inner_var1`), and innermost detail rows (one per level of `inner_var2`) |
|
| 253 |
#' nested beneath each mid-level row. |
|
| 254 |
#' |
|
| 255 |
#' @details `a_three_tier()`: The analysis variable, `inner_var1`, and `inner_var2` must all be |
|
| 256 |
#' factors. Mid-level rows are indented by one and innermost rows by two, |
|
| 257 |
#' relative to the group summary row. |
|
| 258 |
#' |
|
| 259 |
#' @inheritParams a_two_tier |
|
| 260 |
#' @param inner_var1 (`string`)\cr single variable name to use when generating |
|
| 261 |
#' the mid-level (second tier) detail rows. |
|
| 262 |
#' @param inner_var2 (`string`)\cr single variable name to use when generating |
|
| 263 |
#' the innermost (third tier) detail rows, nested under each level of `inner_var1`. |
|
| 264 |
#' |
|
| 265 |
#' @return `a_three_tier()`: A `RowsVerticalSection` object including the group row, mid-level |
|
| 266 |
#' detail rows, and innermost detail rows, if applicable, for the facet. |
|
| 267 |
#' |
|
| 268 |
#' @import checkmate |
|
| 269 |
#' @export |
|
| 270 |
#' |
|
| 271 |
a_three_tier <- function(df, |
|
| 272 |
labelstr = NULL, |
|
| 273 |
.var, |
|
| 274 |
.N_col, |
|
| 275 |
.df_row, |
|
| 276 |
inner_var1, |
|
| 277 |
inner_var2, |
|
| 278 |
drill_down_levs, |
|
| 279 |
.spl_context, |
|
| 280 |
use_all_levels = FALSE, |
|
| 281 |
grp_fun, |
|
| 282 |
detail_fun, |
|
| 283 |
.alt_df_full = NULL, |
|
| 284 |
...) {
|
|
| 285 | 72x |
assert_string(inner_var1) |
| 286 | 72x |
assert_string(inner_var2) |
| 287 | 72x |
assert_character(drill_down_levs) |
| 288 | 72x |
assert_flag(use_all_levels) |
| 289 | 72x |
assert_function(grp_fun) |
| 290 | 72x |
assert_function(detail_fun) |
| 291 | ||
| 292 | 72x |
if (!all(sapply(c(.var, inner_var1, inner_var2), \(x) is.factor(df[[x]])))) {
|
| 293 | ! |
stop("a_three_tier requires all input variables .var, inner_var1 and inner_var2 as factor")
|
| 294 |
} |
|
| 295 | ||
| 296 | 72x |
cur_grp <- tail(.spl_context$value, 1) |
| 297 | 72x |
df[[.var]] <- factor(df[[.var]], levels = cur_grp) |
| 298 | 72x |
.df_row[[.var]] <- factor(.df_row[[.var]], levels = cur_grp) |
| 299 | 72x |
args <- list( |
| 300 | 72x |
labelstr = labelstr, |
| 301 | 72x |
.var = .var, |
| 302 | 72x |
.N_col = .N_col, |
| 303 | 72x |
.df_row = .df_row, |
| 304 | 72x |
.spl_context = .spl_context, |
| 305 | 72x |
.alt_df_full = .alt_df_full, |
| 306 |
... |
|
| 307 |
) |
|
| 308 | 72x |
if (names(formals(grp_fun))[1] == "df") {
|
| 309 | ! |
args <- c(list(df = df), args) |
| 310 |
} else {
|
|
| 311 | 72x |
args <- c(list(x = df[[.var]]), args) |
| 312 |
} |
|
| 313 | ||
| 314 | 72x |
if (identical(grp_fun, a_freq_j)) {
|
| 315 |
# this will ensure row with cur_grp will be presented |
|
| 316 |
# even if 0 observed in current row |
|
| 317 | ! |
args[["drop_levels"]] <- FALSE |
| 318 | ! |
args <- c(list(val = cur_grp), args) |
| 319 |
} |
|
| 320 |
# perform calculation for outer level values |
|
| 321 | 72x |
cell_vals <- unclass( |
| 322 | 72x |
do.call(grp_fun, args) |
| 323 |
) |
|
| 324 | 72x |
names(cell_vals) <- attr(cell_vals, "row_label") |
| 325 | ||
| 326 |
## calculate the drill-down values if necessary |
|
| 327 | 72x |
if (cur_grp %in% drill_down_levs && any(!is.na(.df_row[[inner_var1]]))) {
|
| 328 |
## have to make sure we use all levels for the whole row group |
|
| 329 |
## so that each column gets the same number of values in the same order |
|
| 330 | 12x |
all_inner_levs <- levels(df[[inner_var1]]) |
| 331 | ||
| 332 | 12x |
if (use_all_levels) {
|
| 333 | 3x |
detail_levs <- all_inner_levs |
| 334 |
} else {
|
|
| 335 | 9x |
detail_levs <- all_inner_levs[all_inner_levs %in% all_inner_levs[.df_row[[inner_var1]]]] |
| 336 |
} |
|
| 337 | ||
| 338 | 12x |
inner_vec <- factor(df[[inner_var1]], levels = detail_levs) |
| 339 | 12x |
df[[inner_var1]] <- factor(df[[inner_var1]], levels = detail_levs) |
| 340 | 12x |
.df_row[[inner_var1]] <- factor(.df_row[[inner_var1]], levels = detail_levs) |
| 341 | ||
| 342 |
# inner_vec will be looped over each level separately to act as a row_group |
|
| 343 |
# .df_row_inner will reflect .df_row + filter to level of inner_var |
|
| 344 |
# inner_vec2 can be done at once |
|
| 345 |
# .df_row_inner |
|
| 346 | 12x |
df_orig <- df |
| 347 | 12x |
df_row_orig <- .df_row |
| 348 | 12x |
detail_vals <- c() |
| 349 | 12x |
indentmods <- c() |
| 350 | 12x |
detail_vals_lbl <- c() |
| 351 | 12x |
for (i in seq_along(detail_levs)) {
|
| 352 | 33x |
inner_lev_i <- detail_levs[i] |
| 353 |
# restrict to current row & i-th level of inner_va1 |
|
| 354 | 33x |
.df_row <- df_row_orig[df_row_orig[[inner_var1]] == inner_lev_i, ] |
| 355 | 33x |
.df_row[[inner_var1]] <- factor(.df_row[[inner_var1]], levels = inner_lev_i) |
| 356 | 33x |
df <- df_orig[df_orig[[inner_var1]] == inner_lev_i, ] |
| 357 | 33x |
df[[inner_var1]] <- factor(df[[inner_var1]], levels = inner_lev_i) |
| 358 | ||
| 359 |
# get levels of inner_var2 |
|
| 360 | 33x |
all_inner_levs2 <- levels(df[[inner_var2]]) |
| 361 | 33x |
if (use_all_levels) {
|
| 362 | 9x |
detail_levs2 <- all_inner_levs2 |
| 363 |
} else {
|
|
| 364 |
# restrict to observed levels of inner_var2 in this updated df_row |
|
| 365 |
# cur_grp (level of outer var (.var)) and inner_lev_i (level of mid level var (inner_var1)) |
|
| 366 | 24x |
detail_levs2 <- all_inner_levs2[all_inner_levs2 %in% all_inner_levs2[.df_row[[inner_var2]]]] |
| 367 |
} |
|
| 368 | 33x |
inner_vec2 <- factor(df[[inner_var2]], levels = detail_levs2) |
| 369 | 33x |
df[[inner_var2]] <- factor(df[[inner_var2]], levels = detail_levs2) |
| 370 | 33x |
.df_row[[inner_var2]] <- factor(.df_row[[inner_var2]], levels = detail_levs2) |
| 371 | ||
| 372 | 33x |
det_args <- list( |
| 373 | 33x |
.var = inner_var1, |
| 374 | 33x |
.N_col = .N_col, |
| 375 | 33x |
.df_row = .df_row, |
| 376 | 33x |
.spl_context = .spl_context, |
| 377 | 33x |
.alt_df_full = .alt_df_full, |
| 378 |
... |
|
| 379 |
) |
|
| 380 | 33x |
if (names(formals(detail_fun))[1] == "df") {
|
| 381 | ! |
det_args <- c(list(df = df), det_args) |
| 382 |
} else {
|
|
| 383 | 33x |
det_args <- c(list(x = df[[inner_var1]]), det_args) |
| 384 |
} |
|
| 385 | ||
| 386 |
# mid level values at level inner_lev_i |
|
| 387 | 33x |
detail_vals_i <- unclass( |
| 388 | 33x |
do.call(detail_fun, det_args) |
| 389 |
) |
|
| 390 | ||
| 391 | 33x |
names(detail_vals_i) <- attr(detail_vals_i, "row_label") |
| 392 | ||
| 393 |
# now get the inner level ones |
|
| 394 | 33x |
if (any(!is.na(.df_row[[inner_var2]]))) {
|
| 395 | 30x |
det_args2 <- list( |
| 396 | 30x |
.var = inner_var2, |
| 397 | 30x |
.N_col = .N_col, |
| 398 | 30x |
.df_row = .df_row, |
| 399 | 30x |
.spl_context = .spl_context, |
| 400 | 30x |
.alt_df_full = .alt_df_full, |
| 401 |
... |
|
| 402 |
) |
|
| 403 | 30x |
if (names(formals(detail_fun))[1] == "df") {
|
| 404 | ! |
det_args2 <- c(list(df = df), det_args2) |
| 405 |
} else {
|
|
| 406 | 30x |
det_args2 <- c(list(x = df[[inner_var2]]), det_args2) |
| 407 |
} |
|
| 408 | 30x |
detail_vals2 <- unclass( |
| 409 | 30x |
do.call(detail_fun, det_args2) |
| 410 |
) |
|
| 411 | 30x |
detail_vals2_lbl <- attr(detail_vals2, "row_label") |
| 412 | 30x |
if (i > 1) {
|
| 413 | 21x |
names(detail_vals2) <- paste0(attr(detail_vals2, "row_label"), "[[", i, "]]") |
| 414 |
} else {
|
|
| 415 | 9x |
names(detail_vals2) <- attr(detail_vals2, "row_label") |
| 416 |
} |
|
| 417 | 30x |
detail_vals <- c(detail_vals, detail_vals_i, detail_vals2) |
| 418 | 30x |
indentmods <- c(indentmods, c(1, rep(2, length.out = length(detail_vals2)))) |
| 419 | 30x |
detail_vals_lbl <- c(detail_vals_lbl, names(detail_vals_i), detail_vals2_lbl) |
| 420 |
} |
|
| 421 |
} |
|
| 422 |
} else {
|
|
| 423 | 60x |
detail_vals <- list() |
| 424 | 60x |
indentmods <- NULL |
| 425 | 60x |
detail_vals_lbl <- c() |
| 426 |
} |
|
| 427 | ||
| 428 | 72x |
in_rows( |
| 429 | 72x |
.list = c(cell_vals, detail_vals), |
| 430 | 72x |
.indent_mods = c(0, indentmods), |
| 431 | 72x |
.labels = c(names(cell_vals), detail_vals_lbl) |
| 432 |
) |
|
| 433 |
} |
| 1 |
#' @name a_freq_subcol_j |
|
| 2 |
#' |
|
| 3 |
#' @title Analysis function count and percentage with extra column-subsetting in |
|
| 4 |
#' selected columns (controlled by subcol_* arguments) |
|
| 5 |
#' |
|
| 6 |
#' @inheritParams proposal_argument_convention |
|
| 7 |
#' @inheritParams a_freq_j |
|
| 8 |
#' @param subcol_split (`string`)\cr text to search colid to determine whether further subsetting |
|
| 9 |
#' should be performed. |
|
| 10 |
#' @param subcol_var (`string`)\cr name of variable containing to be searched for the text |
|
| 11 |
#' identified in subcol_val argument. |
|
| 12 |
#' @param subcol_val (`string`)\cr value to use to perform further data sub-setting. |
|
| 13 |
#' @param denom (`string`)\cr |
|
| 14 |
#' One of \cr |
|
| 15 |
#' \itemize{
|
|
| 16 |
#' \item \strong{N_col} Column count, \cr
|
|
| 17 |
#' \item \strong{n_df} Number of patients (based upon the main input dataframe `df`),\cr
|
|
| 18 |
#' \item \strong{n_altdf} Number of patients from the secondary dataframe (`.alt_df_full`),\cr
|
|
| 19 |
#' Note that argument `denom_by` will perform a row-split on the `.alt_df_full` dataframe.\cr |
|
| 20 |
#' It is a requirement that variables specified in `denom_by` are part of the row split specifications. \cr |
|
| 21 |
#' \item \strong{n_rowdf} Number of patients from the current row-level dataframe
|
|
| 22 |
#' (`.row_df` from the rtables splitting machinery).\cr |
|
| 23 |
#' \item \strong{n_parentdf} Number of patients from a higher row-level split than the current split.\cr
|
|
| 24 |
#' This higher row-level split is specified in the argument `denom_by`.\cr |
|
| 25 |
#' } |
|
| 26 |
#' @param .formats (named 'character' or 'list')\cr |
|
| 27 |
#' formats for the statistics. |
|
| 28 |
#' @seealso [a_freq_j()] for extensive `label_map` usage examples. |
|
| 29 |
#' @return list of requested statistics with formatted `rtables::CellValue()`.\cr |
|
| 30 |
#' @export |
|
| 31 |
#' |
|
| 32 |
#' @examples |
|
| 33 |
#' library(dplyr) |
|
| 34 |
#' |
|
| 35 |
#' ADSL <- ex_adsl |> |
|
| 36 |
#' select(USUBJID, ARM) |
|
| 37 |
#' |
|
| 38 |
#' ADSL$COLSPAN_REL <- "AEs" |
|
| 39 |
#' |
|
| 40 |
#' ADAE <- ex_adae |> |
|
| 41 |
#' select(USUBJID, ARM, AEDECOD, AREL) |
|
| 42 |
#' |
|
| 43 |
#' ADAE <- ADAE |> |
|
| 44 |
#' mutate( |
|
| 45 |
#' AEREL = case_when( |
|
| 46 |
#' AREL == "Y" ~ "RELATED", |
|
| 47 |
#' AREL == "N" ~ "NOT RELATED" |
|
| 48 |
#' ), |
|
| 49 |
#' AEREL = factor(AEREL), |
|
| 50 |
#' COLSPAN_REL = "AEs" |
|
| 51 |
#' ) |
|
| 52 |
#' |
|
| 53 |
#' combodf <- tribble( |
|
| 54 |
#' ~valname, ~label, ~levelcombo, ~exargs, |
|
| 55 |
#' "RELATED", "Related AEs", c("AEs"), list()
|
|
| 56 |
#' ) |
|
| 57 |
#' |
|
| 58 |
#' lyt <- basic_table(show_colcounts = TRUE) |> |
|
| 59 |
#' split_cols_by("COLSPAN_REL", split_fun = rtables::add_combo_levels(combodf, trim = TRUE)) |>
|
|
| 60 |
#' split_cols_by("ARM") |>
|
|
| 61 |
#' analyze("AEDECOD",
|
|
| 62 |
#' afun = a_freq_subcol_j, |
|
| 63 |
#' extra_args = list( |
|
| 64 |
#' subcol_split = "RELATED", |
|
| 65 |
#' subcol_var = "AEREL", |
|
| 66 |
#' subcol_val = "RELATED" |
|
| 67 |
#' ) |
|
| 68 |
#' ) |
|
| 69 |
#' |
|
| 70 |
#' result <- build_table(lyt, ADAE, alt_counts_df = ADSL) |
|
| 71 |
#' |
|
| 72 |
#' result |
|
| 73 |
a_freq_subcol_j <- function( |
|
| 74 |
df, |
|
| 75 |
labelstr = NULL, |
|
| 76 |
.var = NA, |
|
| 77 |
val = NULL, |
|
| 78 |
# arguments specific to a_freq_subcol_j |
|
| 79 |
subcol_split = NULL, |
|
| 80 |
subcol_var = NULL, |
|
| 81 |
subcol_val = NULL, |
|
| 82 |
# arguments specific to a_freq_subcol_j till here |
|
| 83 |
.df_row, |
|
| 84 |
.spl_context, |
|
| 85 |
.N_col, |
|
| 86 |
id = "USUBJID", |
|
| 87 |
denom = c("N_col", "n_df", "n_altdf", "n_rowdf", "n_parentdf"),
|
|
| 88 |
label = NULL, |
|
| 89 |
label_fstr = NULL, |
|
| 90 |
label_map = NULL, |
|
| 91 |
.alt_df_full = NULL, |
|
| 92 |
denom_by = NULL, |
|
| 93 |
.stats = c("count_unique_denom_fraction"),
|
|
| 94 |
.formats = NULL, |
|
| 95 |
.labels_n = NULL, |
|
| 96 |
.indent_mods = NULL, |
|
| 97 |
na_str = rep("NA", 3)
|
|
| 98 |
) {
|
|
| 99 | 6x |
denom <- match.arg(denom) |
| 100 | ||
| 101 | 6x |
if (!is.null(labelstr) && is.na(.var)) {
|
| 102 | ! |
stop( |
| 103 | ! |
"Argument var must be specified in call to summarize_row_groups when using cfun = a_freq_subcol_j." |
| 104 |
) |
|
| 105 |
} |
|
| 106 | ||
| 107 | 6x |
check_alt_df_full(denom, "n_altdf", .alt_df_full) |
| 108 | ||
| 109 | 6x |
res_dataprep <- h_a_freq_dataprep( |
| 110 | 6x |
df = df, |
| 111 | 6x |
labelstr = labelstr, |
| 112 | 6x |
.var = .var, |
| 113 | 6x |
val = val, |
| 114 | 6x |
drop_levels = FALSE, |
| 115 | 6x |
excl_levels = NULL, |
| 116 | 6x |
new_levels = NULL, |
| 117 | 6x |
new_levels_after = FALSE, |
| 118 | 6x |
.df_row = .df_row, |
| 119 | 6x |
.spl_context = .spl_context, |
| 120 | 6x |
.N_col = .N_col, |
| 121 | 6x |
id = id, |
| 122 | 6x |
denom = denom, |
| 123 | 6x |
variables = NULL, |
| 124 | 6x |
label = label, |
| 125 | 6x |
label_fstr = label_fstr, |
| 126 | 6x |
label_map = label_map, |
| 127 | 6x |
.alt_df_full = .alt_df_full, |
| 128 | 6x |
denom_by = denom_by, |
| 129 | 6x |
.stats = .stats |
| 130 |
) |
|
| 131 |
# res_dataprep is list with elements |
|
| 132 |
# df .df_row val |
|
| 133 |
# drop_levels excl_levels |
|
| 134 |
# alt_df parentdf new_denomdf |
|
| 135 |
# .stats |
|
| 136 |
# make these elements available in current environment |
|
| 137 | 6x |
df <- res_dataprep$df |
| 138 | 6x |
.df_row <- res_dataprep$.df_row |
| 139 | 6x |
val <- res_dataprep$val |
| 140 | 6x |
alt_df <- res_dataprep$alt_df |
| 141 | 6x |
parentdf <- res_dataprep$parentdf |
| 142 | 6x |
new_denomdf <- res_dataprep$new_denomdf |
| 143 | 6x |
.stats <- res_dataprep$.stats |
| 144 | ||
| 145 |
## colid can be used to figure out if we're in subcolum |
|
| 146 | 6x |
colid <- .spl_context$cur_col_id[[1]] |
| 147 | ||
| 148 |
### this is the core code for subsetting to appropriate subcol_val |
|
| 149 | 6x |
insubcol <- grepl(subcol_split, colid, fixed = TRUE) |
| 150 | 6x |
if (insubcol) {
|
| 151 | 3x |
df <- subset(df, df[[subcol_var]] == subcol_val) |
| 152 |
} |
|
| 153 | ||
| 154 |
## the same s-function can be used as in a_freq_j |
|
| 155 | 6x |
x_stats <- s_freq_j( |
| 156 | 6x |
df, |
| 157 | 6x |
.var = .var, |
| 158 | 6x |
.df_row = .df_row, |
| 159 | 6x |
val = val, |
| 160 | 6x |
alt_df = new_denomdf, |
| 161 | 6x |
parent_df = new_denomdf, |
| 162 | 6x |
id = id, |
| 163 | 6x |
denom = denom, |
| 164 | 6x |
.N_col = .N_col, |
| 165 | 6x |
countsource = "df" |
| 166 |
) |
|
| 167 | ||
| 168 | 6x |
.stats_adj <- .stats |
| 169 | ||
| 170 | 6x |
res_prepinrows <- h_a_freq_prepinrows( |
| 171 | 6x |
x_stats, |
| 172 | 6x |
.stats_adj, |
| 173 | 6x |
.formats, |
| 174 | 6x |
labelstr, |
| 175 | 6x |
label_fstr, |
| 176 | 6x |
label, |
| 177 | 6x |
.indent_mods, |
| 178 | 6x |
.labels_n, |
| 179 | 6x |
na_str |
| 180 |
) |
|
| 181 |
# res_prepinrows is list with elements |
|
| 182 |
# x_stats .formats .labels .indent_mods .format_na_strs |
|
| 183 |
# make these elements available in current environment |
|
| 184 | 6x |
x_stats <- res_prepinrows$x_stats |
| 185 | 6x |
.formats <- res_prepinrows$.formats |
| 186 | 6x |
.labels <- res_prepinrows$.labels |
| 187 | 6x |
.indent_mods <- res_prepinrows$.indent_mods |
| 188 | 6x |
.format_na_strs <- res_prepinrows$.format_na_strs |
| 189 | ||
| 190 |
### final step: turn requested stats into rtables rows |
|
| 191 | 6x |
inrows <- in_rows( |
| 192 | 6x |
.list = x_stats, |
| 193 | 6x |
.formats = .formats, |
| 194 | 6x |
.labels = .labels, |
| 195 | 6x |
.indent_mods = .indent_mods, |
| 196 | 6x |
.format_na_strs = .format_na_strs |
| 197 |
) |
|
| 198 | ||
| 199 | 6x |
return(inrows) |
| 200 |
} |
| 1 |
#' @keywords internal |
|
| 2 |
formatter_references <- list( |
|
| 3 |
"jjcsformat_xx" = |
|
| 4 |
jjcsformat_xx("xx.xxxxxxxxx"),
|
|
| 5 |
"jjcsformat_fraction" = |
|
| 6 |
jjcsformat_cnt_den_fract_fct(type = "count_fraction"), |
|
| 7 |
"jjcsformat_pval" = |
|
| 8 |
jjcsformat_pval_fct(alpha = 0), |
|
| 9 |
"jjcsformat_range" = |
|
| 10 |
jjcsformat_range_fct("xx.xx"),
|
|
| 11 |
"format_xx" = |
|
| 12 |
format_xx("xx"),
|
|
| 13 |
"format_extreme_values" = |
|
| 14 |
format_extreme_values(2L), |
|
| 15 |
"format_extreme_values_ci" = |
|
| 16 |
format_extreme_values_ci(2L), |
|
| 17 |
"format_count_fraction_fixed_dp" = |
|
| 18 |
format_count_fraction_fixed_dp, |
|
| 19 |
"format_count_fraction" = |
|
| 20 |
format_count_fraction, |
|
| 21 |
"format_fraction_fixed_dp" = |
|
| 22 |
format_fraction_fixed_dp, |
|
| 23 |
"format_sigfig_j" = format_sigfig_j(sigfig = 3, "xx") |
|
| 24 |
) |
|
| 25 | ||
| 26 |
#' @keywords internal |
|
| 27 |
body_matches <- function(fmtfun, reference_fun) {
|
|
| 28 | ! |
identical(deparse(body(fmtfun)), deparse(body(reference_fun))) |
| 29 |
} |
|
| 30 | ||
| 31 |
#' @keywords internal |
|
| 32 |
get_input_jjcs_fmtfun <- function(fmtfun) {
|
|
| 33 | ! |
if (length(fmtfun) != 1) {
|
| 34 | ! |
stop("fmtfun should be of length 1")
|
| 35 |
} |
|
| 36 | ||
| 37 | ! |
if (is.list(fmtfun)) {
|
| 38 | ! |
fmtfun <- fmtfun[[1]] |
| 39 |
} |
|
| 40 | ||
| 41 | ! |
result <- list( |
| 42 | ! |
fun_fact = "", |
| 43 | ! |
str = "", |
| 44 | ! |
d = NA_integer_, |
| 45 | ! |
str_formatters = NA, |
| 46 | ! |
type = "", |
| 47 | ! |
alpha = "", |
| 48 | ! |
message = "", |
| 49 | ! |
fmt_package = "", |
| 50 | ! |
is_fun = FALSE, |
| 51 | ! |
fun = fmtfun, |
| 52 | ! |
round_type = FALSE, |
| 53 | ! |
whole_integer = NA, |
| 54 | ! |
drop0trailing = NA, |
| 55 | ! |
zero_threshold = 0 |
| 56 |
) |
|
| 57 | ||
| 58 | ! |
valid_fmt_str <- unlist(list_valid_format_labels(), use.names = FALSE) |
| 59 | ||
| 60 |
# Handle character inputs separately. |
|
| 61 | ! |
if (is.character(fmtfun)) {
|
| 62 | ! |
if (fmtfun %in% valid_fmt_str) {
|
| 63 | ! |
result$fun_fact <- "" |
| 64 | ! |
result$str <- fmtfun |
| 65 | ! |
result$str_formatters <- TRUE |
| 66 | ! |
result$fmt_package <- "formatters" |
| 67 | ! |
result$round_type <- TRUE |
| 68 |
} else {
|
|
| 69 | ! |
result$fun_fact <- "string" |
| 70 | ! |
result$str <- fmtfun |
| 71 | ! |
result$str_formatters <- FALSE |
| 72 | ! |
result$message <- paste( |
| 73 | ! |
"fmtfun is a string but not a supported formatting value" |
| 74 |
) |
|
| 75 |
} |
|
| 76 | ||
| 77 | ! |
return(result) |
| 78 |
} |
|
| 79 | ||
| 80 | ! |
if (!is.function(fmtfun)) {
|
| 81 | ! |
result$message <- paste( |
| 82 | ! |
"fmtfun is not a function/character" |
| 83 |
) |
|
| 84 | ||
| 85 | ! |
message(result$message) |
| 86 | ||
| 87 | ! |
return(result) |
| 88 |
} |
|
| 89 | ||
| 90 |
# from here on fmtfun is a function |
|
| 91 | ! |
x_body_matches <- sapply(formatter_references, body_matches, fmtfun = fmtfun) |
| 92 | ! |
formatter_key <- if (any(x_body_matches)) {
|
| 93 | ! |
names(formatter_references)[which(x_body_matches)[1L]] |
| 94 |
} else {
|
|
| 95 | ! |
"unknown" |
| 96 |
} |
|
| 97 | ||
| 98 | ! |
result$is_fun <- TRUE |
| 99 | ! |
if (formatter_key != "unknown") {
|
| 100 | ! |
result$round_type <- "round_type" %in% names(formals(fmtfun)) |
| 101 |
} |
|
| 102 | ! |
switch(formatter_key, |
| 103 |
"jjcsformat_xx" = {
|
|
| 104 | ! |
result$fun_fact <- "jjcsformat_xx" |
| 105 | ! |
result$str <- get0( |
| 106 | ! |
"str", |
| 107 | ! |
envir = environment(fmtfun), |
| 108 | ! |
ifnotfound = "" |
| 109 |
) |
|
| 110 | ! |
result$str_formatters <- FALSE |
| 111 | ! |
result$fmt_package <- "junco" |
| 112 |
}, |
|
| 113 |
"jjcsformat_fraction" = {
|
|
| 114 | ! |
result$fun_fact <- "jjcsformat_fraction" |
| 115 | ! |
result$type <- get0( |
| 116 | ! |
"type", |
| 117 | ! |
envir = environment(fmtfun), |
| 118 | ! |
ifnotfound = "" |
| 119 |
) |
|
| 120 | ! |
result$fmt_package <- "junco" |
| 121 |
}, |
|
| 122 |
"jjcsformat_pval" = {
|
|
| 123 | ! |
result$fun_fact <- "jjcsformat_pval" |
| 124 | ! |
result$alpha <- as.character( |
| 125 | ! |
get0( |
| 126 | ! |
"alpha", |
| 127 | ! |
envir = environment(fmtfun), |
| 128 | ! |
ifnotfound = "" |
| 129 |
) |
|
| 130 |
) |
|
| 131 | ! |
result$fmt_package <- "junco" |
| 132 |
}, |
|
| 133 |
"jjcsformat_range" = {
|
|
| 134 | ! |
result$fun_fact <- "jjcsformat_range" |
| 135 | ! |
result$str <- as.character( |
| 136 | ! |
get0( |
| 137 | ! |
"str", |
| 138 | ! |
envir = environment(fmtfun), |
| 139 | ! |
ifnotfound = "" |
| 140 |
) |
|
| 141 |
) |
|
| 142 | ! |
result$fmt_package <- "junco" |
| 143 |
}, |
|
| 144 |
"format_xx" = {
|
|
| 145 | ! |
result$fun_fact <- "format_xx" |
| 146 | ! |
result$str <- as.character( |
| 147 | ! |
get0( |
| 148 | ! |
"str", |
| 149 | ! |
envir = environment(fmtfun), |
| 150 | ! |
ifnotfound = "" |
| 151 |
) |
|
| 152 |
) |
|
| 153 | ! |
result$fmt_package <- "tern" |
| 154 |
}, |
|
| 155 |
"format_extreme_values" = {
|
|
| 156 | ! |
result$fun_fact <- "format_extreme_values" |
| 157 | ! |
result$str <- "???" |
| 158 | ! |
result$fmt_package <- "tern" |
| 159 |
}, |
|
| 160 |
"format_extreme_values_ci" = {
|
|
| 161 | ! |
result$fun_fact <- "format_extreme_values_ci" |
| 162 | ! |
result$str <- "???" |
| 163 | ! |
result$fmt_package <- "tern" |
| 164 |
}, |
|
| 165 |
"format_count_fraction_fixed_dp" = {
|
|
| 166 | ! |
result$fun_fact <- "format_count_fraction_fixed_dp" |
| 167 | ! |
result$fmt_package <- "tern" |
| 168 |
}, |
|
| 169 |
"format_count_fraction" = {
|
|
| 170 | ! |
result$fun_fact <- "format_count_fraction" |
| 171 | ! |
result$fmt_package <- "tern" |
| 172 |
}, |
|
| 173 |
"format_fraction_fixed_dp" = {
|
|
| 174 | ! |
result$fun_fact <- "format_fraction_fixed_dp" |
| 175 | ! |
result$fmt_package <- "tern" |
| 176 |
}, |
|
| 177 |
"format_sigfig_j" = {
|
|
| 178 | ! |
result$fun_fact <- "format_sigfig_j" |
| 179 | ! |
result$str <- as.character( |
| 180 | ! |
get0( |
| 181 | ! |
"format", |
| 182 | ! |
envir = environment(fmtfun), |
| 183 | ! |
ifnotfound = "" |
| 184 |
) |
|
| 185 |
) |
|
| 186 | ! |
result$d <- as.numeric( |
| 187 | ! |
get0( |
| 188 | ! |
"sigfig", |
| 189 | ! |
envir = environment(fmtfun), |
| 190 | ! |
ifnotfound = "" |
| 191 |
) |
|
| 192 |
) |
|
| 193 | ! |
result$whole_integer <- |
| 194 | ! |
get0( |
| 195 | ! |
"whole_integer", |
| 196 | ! |
envir = environment(fmtfun), |
| 197 | ! |
ifnotfound = FALSE |
| 198 |
) |
|
| 199 | ! |
result$drop0trailing <- |
| 200 | ! |
get0( |
| 201 | ! |
"drop0trailing", |
| 202 | ! |
envir = environment(fmtfun), |
| 203 | ! |
ifnotfound = FALSE |
| 204 |
) |
|
| 205 | ! |
result$zero_threshold <- |
| 206 | ! |
get0( |
| 207 | ! |
"zero_threshold", |
| 208 | ! |
envir = environment(fmtfun), |
| 209 | ! |
ifnotfound = 0L |
| 210 |
) |
|
| 211 | ! |
result$fmt_package <- "junco" |
| 212 |
}, |
|
| 213 |
"unknown" = {
|
|
| 214 | ! |
result$fun_fact <- "???" |
| 215 | ! |
result$fmt_package <- "???" |
| 216 | ! |
result$message <- paste( |
| 217 | ! |
"fmtfun is an unknown formatting function not defined", |
| 218 | ! |
"in formatters/tern/junco" |
| 219 |
) |
|
| 220 |
} |
|
| 221 |
) |
|
| 222 | ||
| 223 | ! |
result |
| 224 |
} |
|
| 225 | ||
| 226 | ||
| 227 |
#' @name fmt_utils |
|
| 228 |
#' |
|
| 229 |
#' @title Utilities for reviewing format specification objects |
|
| 230 |
#' |
|
| 231 |
#' @details fmt_utils general details |
|
| 232 |
#' |
|
| 233 |
#' @description `r lifecycle::badge("experimental")`
|
|
| 234 |
#' |
|
| 235 |
#' @param myfmts A named list of either formatting specifications (ie stats with its associated formats) |
|
| 236 |
#' or a list of variables with formatting specifications. |
|
| 237 |
#' |
|
| 238 |
#' @param recursive `logical(1)` |
|
| 239 |
#' Set to `TRUE` when requesting details from named list of variables with specifications. |
|
| 240 |
#' Set to `FALSE` when requesting details from named list of statistics with format specifications. |
|
| 241 |
#' |
|
| 242 |
#' @param as_tibble `logical(1)` When `TRUE`, convert the information into a tibble for ease of reviewing. |
|
| 243 |
#' |
|
| 244 |
#' |
|
| 245 |
#' @param fmt1 A named list of formatting specifications (ie stats with its associated format) |
|
| 246 |
#' @param fmt2 Another named list of formatting specifications (ie stats with its associated format), |
|
| 247 |
#' to compare against `fmt1` |
|
| 248 |
NULL |
|
| 249 | ||
| 250 |
#' @describeIn fmt_utils A utility function to collect information on formatting specification in list or tibble format. |
|
| 251 |
#' |
|
| 252 |
#' The following information is collected: |
|
| 253 |
#' - var (variable name): Only applicable if the format specification is a named list with formattings specs |
|
| 254 |
#' (eg list("AGE" = c("mean" = "xx", "se" = "xx.x"), "BMI" = c("mean" = "xx.x", "se" = "xx.xx"))), eg processed via
|
|
| 255 |
#' `fmt_spec_var_d` function. |
|
| 256 |
#' - stat (name of statistic) |
|
| 257 |
#' - fun_fact : name of function factory that has produced the format specification |
|
| 258 |
#' (if spec is from formatting function, blank if spec is string).\cr |
|
| 259 |
#' Only the following known formatting functions are supported: |
|
| 260 |
#' |
|
| 261 |
#' - jjcsformat_xx |
|
| 262 |
#' - jjcsformat_cnt_den_fract_fct, |
|
| 263 |
#' - jjcsformat_pval_fct, |
|
| 264 |
#' - jjcsformat_range_fct, |
|
| 265 |
#' - tern::format_xx, |
|
| 266 |
#' - tern::format_extreme_values, |
|
| 267 |
#' - tern::format_extreme_values_ci, |
|
| 268 |
#' - tern::format_count_fraction_fixed_dp, |
|
| 269 |
#' - tern::format_count_fraction, |
|
| 270 |
#' - tern::format_fraction_fixed_dp, |
|
| 271 |
#' - format_sigfig_j |
|
| 272 |
#' |
|
| 273 |
#' - str: string from specification, or str from formatting function factory (`jjcsformat_xx`, ....) |
|
| 274 |
#' - str_formatters: (`logical`) Is the str value a valid format string as in ([list_valid_format_labels()])? |
|
| 275 |
#' - type: when formatting function factory `jjcsformat_cnt_den_fract_fct` is used |
|
| 276 |
#' - alpha: when formatting function factory `jjcsformat_pval_fct` is used |
|
| 277 |
#' - message: Message when formatting function is unknown, or input string was |
|
| 278 |
#' not valid format string as in ([list_valid_format_labels()]) |
|
| 279 |
#' - fmt_package: In which package was the formatting specification defined. |
|
| 280 |
#' - is_fun: (`logical`) Is the formatting specification a function? |
|
| 281 |
#' - round_type: (`logical`) Does the formatting support `round_type` argument/behavior? |
|
| 282 |
#' - fun: Formatting string or body of the formatting function. |
|
| 283 |
#' |
|
| 284 |
#' Only when one of the formats provided is based upon formatting function factory `format_sigfig_j` setup: |
|
| 285 |
#' - d: value used for `sigfig` |
|
| 286 |
#' - whole_integer: value used for `whole_integer` |
|
| 287 |
#' - drop0trailing: value used for `drop0trailing` |
|
| 288 |
#' - zero_threshold: value used for `zero_threshold` |
|
| 289 |
#' @examples |
|
| 290 |
#' # Example for get_fmt_details -- |
|
| 291 |
#' junco_def_d <- c( |
|
| 292 |
#' "mean" = "xx.dx", |
|
| 293 |
#' "mean_sd" = "xx.dx (xx.dxx)", |
|
| 294 |
#' "range" = "(xx.d, xx.d)" |
|
| 295 |
#' ) |
|
| 296 |
#' |
|
| 297 |
#' myfmts <- fmt_spec_single_d( |
|
| 298 |
#' d = 1, |
|
| 299 |
#' stats_in = c("mean", "mean_sd"),
|
|
| 300 |
#' fmt_d_def = junco_def_d, |
|
| 301 |
#' fmt_d_in = NULL |
|
| 302 |
#' ) |
|
| 303 |
#' |
|
| 304 |
#' fmt_details <- get_fmt_details(myfmts, as_tibble = TRUE) |
|
| 305 |
#' |
|
| 306 |
#' myfmts2 <- fmt_spec_single_d( |
|
| 307 |
#' d = 3, |
|
| 308 |
#' stats_in = c("mean", "mean_sd"),
|
|
| 309 |
#' fmt_d_def = junco_def_d, |
|
| 310 |
#' fmt_d_in = NULL |
|
| 311 |
#' ) |
|
| 312 |
#' fmt_details2 <- get_fmt_details(myfmts2, as_tibble = TRUE) |
|
| 313 |
#' fmt_details2 |
|
| 314 |
#' |
|
| 315 |
#' # Example for get_fmt_details using format specs for variables -- |
|
| 316 |
#' # processed with fmt_spec_var_d |
|
| 317 |
#' var <- c("AGE" = 0, "BMI" = 1, "BMRKR1" = 2)
|
|
| 318 |
#' vars_fmt <- fmt_spec_var_d(var, |
|
| 319 |
#' stats_in = NULL, |
|
| 320 |
#' fmt_d_def = junco_def_d, |
|
| 321 |
#' fmt_d_in = NULL |
|
| 322 |
#' ) |
|
| 323 |
#' fmt_details2 <- get_fmt_details(vars_fmt, as_tibble = TRUE, recursive = TRUE) |
|
| 324 |
#' |
|
| 325 |
#' @export |
|
| 326 |
get_fmt_details <- function(myfmts, recursive = FALSE, as_tibble = TRUE) {
|
|
| 327 | ! |
lst <- if (recursive) {
|
| 328 | ! |
lapply(myfmts, get_fmt_details, as_tibble = as_tibble) |
| 329 |
} else {
|
|
| 330 | ! |
lapply(myfmts, get_input_jjcs_fmtfun) |
| 331 |
} |
|
| 332 | ||
| 333 | ! |
names(lst) <- names(myfmts) |
| 334 | ||
| 335 | ! |
if (!recursive && as_tibble) {
|
| 336 | ! |
df <- do.call( |
| 337 | ! |
rbind, |
| 338 | ! |
lapply(names(lst), function(nm) {
|
| 339 | ! |
xx <- lst[[nm]] |
| 340 | ! |
xx$fun <- NULL |
| 341 | ! |
data.frame( |
| 342 | ! |
stat = nm, |
| 343 | ! |
as.data.frame(xx, stringsAsFactors = FALSE) |
| 344 |
) |
|
| 345 |
}) |
|
| 346 |
) |
|
| 347 | ! |
df <- dplyr::as_tibble(df) |
| 348 | ||
| 349 | ! |
df$fun <- lapply(lst, `[[`, "fun") |
| 350 | ! |
lst <- df |
| 351 |
} |
|
| 352 | ||
| 353 | ! |
if (recursive && as_tibble) {
|
| 354 | ! |
lst <- dplyr::bind_rows(lst, .id = "var") |
| 355 |
} |
|
| 356 | ||
| 357 | ! |
if (as_tibble && !any(lst[["fun_fact"]] == "format_sigfig_j")) {
|
| 358 |
# drop format_sigfig_j specific columns |
|
| 359 | ! |
dropcols <- c("d", "whole_integer", "drop0trailing", "zero_threshold")
|
| 360 | ! |
lst <- lst[, setdiff(names(lst), dropcols), drop = FALSE] |
| 361 |
} |
|
| 362 | ! |
lst |
| 363 |
} |
|
| 364 | ||
| 365 | ||
| 366 |
#' @noRd |
|
| 367 |
#' @param fmt_t1 Tibble information from specification 1 |
|
| 368 |
#' @param fmt_t2 Tibble information from specification 2 |
|
| 369 |
#' @keywords internal |
|
| 370 |
compare_fmt_tibble <- function(fmt_t1, fmt_t2) {
|
|
| 371 | ! |
nms <- names(fmt_t1) |
| 372 | ! |
nms <- nms[nms != "fun"] |
| 373 | ! |
fmt_t1_x <- fmt_t1[, nms] |
| 374 | ! |
fmt_t2_x <- fmt_t2[, nms] |
| 375 | ! |
c1 <- identical(fmt_t1_x, fmt_t2_x) |
| 376 | ||
| 377 | ! |
fmt_t1_y <- fmt_t1[["fun"]][[1]] |
| 378 | ! |
fmt_t2_y <- fmt_t2[["fun"]][[1]] |
| 379 | ! |
if (is.function(fmt_t1_y) && is.function(fmt_t2_y)) {
|
| 380 | ! |
c2 <- body_matches(fmt_t1_y, fmt_t2_y) |
| 381 | ! |
} else if (is.character(fmt_t1_y) && is.character(fmt_t2_y)) {
|
| 382 | ! |
c2 <- identical(fmt_t1_y, fmt_t2_y) |
| 383 |
} else {
|
|
| 384 | ! |
c2 <- FALSE |
| 385 |
} |
|
| 386 | ||
| 387 | ! |
rslt <- c1 & c2 |
| 388 |
} |
|
| 389 | ||
| 390 |
#' @describeIn fmt_utils A utility to report findings in differences between 2 formatting specification vectors. |
|
| 391 |
#' It returns a list object with the following information: |
|
| 392 |
#' - spec1 : For statistics with a different specification, tibble with specification details from `fmt1`. |
|
| 393 |
#' - spec2 : For statistics with a different specification, tibble with specification details from `fmt2`. |
|
| 394 |
#' The tibbles in `spec1` and `spec2` contain information as described in `get_fmt_details()`. |
|
| 395 |
#' - msg: Message on how many stats have a difference in specification. |
|
| 396 |
#' - comm_stats: Vector of stats in common between `fmt1` and `fmt2`. |
|
| 397 |
#' - diff_stats: Vector of stats with any difference identified. |
|
| 398 |
#' @examples |
|
| 399 |
#' |
|
| 400 |
#' # Example for compare_fmt_specs -- |
|
| 401 |
#' test1_target <- junco_default_formats |
|
| 402 |
#' # junco_def_d_all has been constructed with d = 1 as reference |
|
| 403 |
#' # compare against junco_default_formats |
|
| 404 |
#' test1_d <- fmt_spec_single_d( |
|
| 405 |
#' d = 1, |
|
| 406 |
#' stats_in = NULL, |
|
| 407 |
#' fmt_d_def = junco_def_d_all, |
|
| 408 |
#' fmt_d_in = NULL |
|
| 409 |
#' ) |
|
| 410 |
#' |
|
| 411 |
#' check1 <- compare_fmt_specs(test1_d, test1_target) |
|
| 412 |
#' check1 |
|
| 413 |
#' |
|
| 414 |
#' # more differences when using other d |
|
| 415 |
#' test2_d <- fmt_spec_single_d( |
|
| 416 |
#' d = 2, |
|
| 417 |
#' stats_in = NULL, |
|
| 418 |
#' fmt_d_def = junco_def_d_all, |
|
| 419 |
#' fmt_d_in = NULL |
|
| 420 |
#' ) |
|
| 421 |
#' |
|
| 422 |
#' check2 <- compare_fmt_specs(test2_d, test1_target) |
|
| 423 |
#' |
|
| 424 |
#' @export |
|
| 425 |
#' |
|
| 426 |
compare_fmt_specs <- function(fmt1, fmt2) {
|
|
| 427 | ! |
result <- list( |
| 428 | ! |
spec1 = NULL, |
| 429 | ! |
spec2 = NULL, |
| 430 | ! |
msg = NULL, |
| 431 | ! |
comm_stats = NULL, |
| 432 | ! |
diff_stats = NULL |
| 433 |
) |
|
| 434 | ! |
stats_diff <- NULL |
| 435 | ||
| 436 | ! |
stats1 <- names(fmt1) |
| 437 | ! |
stats2 <- names(fmt2) |
| 438 | ! |
stats <- intersect(stats1, stats2) |
| 439 | ! |
if (length(stats) == 0) {
|
| 440 | ! |
result$msg <- "No stats in common" |
| 441 | ! |
result$comm_stats <- NULL |
| 442 |
} else {
|
|
| 443 | ! |
result$comm_stats <- stats |
| 444 | ! |
fmt1 <- fmt1[stats] |
| 445 | ! |
fmt2 <- fmt2[stats] |
| 446 | ||
| 447 | ! |
check <- sapply(names(fmt1), \(stat) {
|
| 448 | ! |
fmt_t1 <- get_fmt_details(list(stat = fmt1[[stat]])) |
| 449 | ! |
fmt_t2 <- get_fmt_details(list(stat = fmt2[[stat]])) |
| 450 | ! |
fmt_t1[["stat"]] <- stat |
| 451 | ! |
fmt_t2[["stat"]] <- stat |
| 452 | ||
| 453 | ! |
compare_fmt_tibble(fmt_t1, fmt_t2) |
| 454 |
}) |
|
| 455 | ||
| 456 | ! |
if (sum(!check) == 0) {
|
| 457 | ! |
result$msg <- "All common stats specs identical" |
| 458 |
} else {
|
|
| 459 | ! |
stats_diff <- check[!check] |
| 460 | ! |
stats_diff <- names(stats_diff) |
| 461 | ! |
result$spec1 <- get_fmt_details(fmt1[stats_diff]) |
| 462 | ! |
result$spec2 <- get_fmt_details(fmt2[stats_diff]) |
| 463 | ! |
result$msg <- paste0(length(stats_diff), " common stats with differences in specs") |
| 464 | ! |
result$diff_stats <- stats_diff |
| 465 |
} |
|
| 466 |
} |
|
| 467 | ||
| 468 | ! |
if (!is.null(result$msg)) {
|
| 469 | ! |
message(result$msg) |
| 470 | ! |
message("More details are available in created object.")
|
| 471 |
} |
|
| 472 | ! |
return(result) |
| 473 |
} |
| 1 |
#' Relative risk estimation |
|
| 2 |
#' |
|
| 3 |
#' The analysis function [a_relative_risk()] is used to create a layout element |
|
| 4 |
#' to estimate the relative risk for response within a studied population using |
|
| 5 |
#' stratified CMH or unstratified Wald methods. |
|
| 6 |
#' The primary analysis variable, `vars`, is a logical variable indicating |
|
| 7 |
#' whether a response has occurred for each record. |
|
| 8 |
#' For CMH, a stratification variable must be supplied via the |
|
| 9 |
#' `strata` element of the `variables` argument. |
|
| 10 |
#' |
|
| 11 |
#' @details The variance of the CMH relative risk estimate is calculated using |
|
| 12 |
#' the Greenland and Robins (1985) variance estimation. |
|
| 13 |
#' The unstratified `wald` and `waldcc` methods use a normal approximation |
|
| 14 |
#' for the log relative risk. |
|
| 15 |
#' `waldcc` adds 0.5 to |
|
| 16 |
#' each response count and each group total in both the estimate and variance. |
|
| 17 |
#' With `wald`, a zero response count gives undefined confidence limits and |
|
| 18 |
#' p-value (`NA`). |
|
| 19 |
#' |
|
| 20 |
#' @param df (`data.frame`)\cr input data frame. |
|
| 21 |
#' @param .var (`string`)\cr name of the response variable. |
|
| 22 |
#' @param ref_path (`character`)\cr path to the reference group. |
|
| 23 |
#' @param .spl_context (`environment`)\cr split context environment. |
|
| 24 |
#' @param ... Additional arguments passed to the statistics function. |
|
| 25 |
#' @param .stats (`character`)\cr statistics to calculate. |
|
| 26 |
#' @param .formats (`list`)\cr formats for the statistics. |
|
| 27 |
#' @param .labels (`list`)\cr labels for the statistics. |
|
| 28 |
#' @param .indent_mods (`list`)\cr indentation modifications for the statistics. |
|
| 29 |
#' @param .ref_group (`data.frame`)\cr reference group data frame. |
|
| 30 |
#' @param .in_ref_col (`logical(1)`)\cr whether the current column is the reference column. |
|
| 31 |
#' @param variables (`list`)\cr list with strata variable names. |
|
| 32 |
#' @param conf_level (`numeric`)\cr confidence level for the confidence interval. |
|
| 33 |
#' @param complete_cases (`logical(1)`) \cr whether to remove incomplete rows from `df` |
|
| 34 |
#' before constructing the response, group, strata, and contingency table. |
|
| 35 |
#' @param method (`string`)\cr relative risk method: `cmh`, `wald`, or `waldcc`. |
|
| 36 |
#' @param weights_method (`string`)\cr method to use for weights calculation in stratified analysis. |
|
| 37 |
#' |
|
| 38 |
#' @name relative_risk |
|
| 39 |
NULL |
|
| 40 | ||
| 41 |
#' @describeIn relative_risk Statistics function estimating the relative risk for response. |
|
| 42 |
#' |
|
| 43 |
#' @return |
|
| 44 |
#' * `s_relative_risk()` returns a named list of elements `rel_risk_ci` and `pval`. |
|
| 45 |
#' |
|
| 46 |
#' @examples |
|
| 47 |
#' nex <- 100 |
|
| 48 |
#' dta <- data.frame( |
|
| 49 |
#' "rsp" = sample(c(TRUE, FALSE), nex, TRUE), |
|
| 50 |
#' "grp" = sample(c("A", "B"), nex, TRUE),
|
|
| 51 |
#' "f1" = sample(c("a1", "a2"), nex, TRUE),
|
|
| 52 |
#' "f2" = sample(c("x", "y", "z"), nex, TRUE),
|
|
| 53 |
#' stringsAsFactors = TRUE |
|
| 54 |
#' ) |
|
| 55 |
#' |
|
| 56 |
#' s_relative_risk( |
|
| 57 |
#' df = subset(dta, grp == "A"), |
|
| 58 |
#' .var = "rsp", |
|
| 59 |
#' .ref_group = subset(dta, grp == "B"), |
|
| 60 |
#' .in_ref_col = FALSE, |
|
| 61 |
#' variables = list(strata = c("f1", "f2")),
|
|
| 62 |
#' conf_level = 0.90 |
|
| 63 |
#' ) |
|
| 64 |
#' @export |
|
| 65 |
s_relative_risk <- function( |
|
| 66 |
df, |
|
| 67 |
.var, |
|
| 68 |
.ref_group = NULL, |
|
| 69 |
.in_ref_col = NULL, |
|
| 70 |
variables = list(strata = NULL), |
|
| 71 |
conf_level = 0.95, |
|
| 72 |
complete_cases = FALSE, |
|
| 73 |
method = c("cmh", "wald", "waldcc"),
|
|
| 74 |
weights_method = "cmh" |
|
| 75 |
) {
|
|
| 76 | 17x |
checkmate::assert_flag(.in_ref_col, null.ok = TRUE) |
| 77 | 17x |
method <- match.arg(method) |
| 78 | 16x |
weights_method <- match.arg(weights_method) |
| 79 | ||
| 80 | 16x |
y <- if (is.null(.in_ref_col) || .in_ref_col) {
|
| 81 | 5x |
list( |
| 82 | 5x |
rel_risk_ci = c(est = NA_real_, lcl = NA_real_, ucl = NA_real_), |
| 83 | 5x |
pval = NA_real_ |
| 84 |
) |
|
| 85 |
} else {
|
|
| 86 | 11x |
prepared <- tern::h_prepare_rsp_table( |
| 87 | 11x |
df = df, |
| 88 | 11x |
df_ref = .ref_group, |
| 89 | 11x |
var = .var, |
| 90 | 11x |
strata_vars = if (method == "cmh") variables$strata else NULL, |
| 91 | 11x |
complete_cases = complete_cases |
| 92 |
) |
|
| 93 | ||
| 94 | 11x |
if (method == "cmh") {
|
| 95 | 5x |
prop_ratio_cmh(tbl = prepared$tbl, conf_level = conf_level) |
| 96 |
} else {
|
|
| 97 | 6x |
prop_ratio_wald(tbl = prepared$tbl, conf_level = conf_level, correction = (method == "waldcc")) |
| 98 |
} |
|
| 99 |
} |
|
| 100 | ||
| 101 | 15x |
y$rel_risk_ci <- with_label( |
| 102 | 15x |
x = y$rel_risk_ci, |
| 103 | 15x |
label = paste0("Relative risk (", f_conf_level(conf_level), ")")
|
| 104 |
) |
|
| 105 | 15x |
y$pval <- unname(y$pval) |
| 106 | ||
| 107 | 15x |
y |
| 108 |
} |
|
| 109 | ||
| 110 |
#' @describeIn relative_risk Formatted analysis function which is used as `afun`. Note that the |
|
| 111 |
#' junco specific `ref_path` and `.spl_context` arguments are used for reference column information. |
|
| 112 |
#' |
|
| 113 |
#' @return |
|
| 114 |
#' * `a_relative_risk()` returns the corresponding list with formatted [rtables::CellValue()]. |
|
| 115 |
#' |
|
| 116 |
#' @examples |
|
| 117 |
#' nex <- 100 |
|
| 118 |
#' dta <- data.frame( |
|
| 119 |
#' "rsp" = sample(c(TRUE, FALSE), nex, TRUE), |
|
| 120 |
#' "grp" = sample(c("A", "B"), nex, TRUE),
|
|
| 121 |
#' "f1" = sample(c("a1", "a2"), nex, TRUE),
|
|
| 122 |
#' "f2" = sample(c("x", "y", "z"), nex, TRUE),
|
|
| 123 |
#' stringsAsFactors = TRUE |
|
| 124 |
#' ) |
|
| 125 |
#' |
|
| 126 |
#' l <- basic_table() |> |
|
| 127 |
#' split_cols_by(var = "grp") |> |
|
| 128 |
#' analyze( |
|
| 129 |
#' vars = "rsp", |
|
| 130 |
#' afun = a_relative_risk, |
|
| 131 |
#' extra_args = list( |
|
| 132 |
#' conf_level = 0.90, |
|
| 133 |
#' variables = list(strata = "f1"), |
|
| 134 |
#' ref_path = c("grp", "B")
|
|
| 135 |
#' ) |
|
| 136 |
#' ) |
|
| 137 |
#' |
|
| 138 |
#' build_table(l, df = dta) |
|
| 139 |
#' @export |
|
| 140 |
#' @order 2 |
|
| 141 |
a_relative_risk <- function( |
|
| 142 |
df, |
|
| 143 |
.var, |
|
| 144 |
ref_path, |
|
| 145 |
.spl_context, |
|
| 146 |
..., |
|
| 147 |
.stats = NULL, |
|
| 148 |
.formats = NULL, |
|
| 149 |
.labels = NULL, |
|
| 150 |
.indent_mods = NULL |
|
| 151 |
) {
|
|
| 152 |
# Check for additional parameters to the statistics function |
|
| 153 | 6x |
dots_extra_args <- list(...) |
| 154 | ||
| 155 |
# Only support default stats, not custom stats |
|
| 156 | 6x |
.stats <- .split_std_from_custom_stats(.stats)$default_stats |
| 157 | ||
| 158 |
# Obtain reference column information |
|
| 159 | 6x |
ref <- get_ref_info(ref_path, .spl_context) |
| 160 | ||
| 161 |
# Apply statistics function |
|
| 162 | 6x |
x_stats <- .apply_stat_functions( |
| 163 | 6x |
default_stat_fnc = s_relative_risk, |
| 164 | 6x |
custom_stat_fnc_list = NULL, |
| 165 | 6x |
args_list = c( |
| 166 | 6x |
df = list(df), |
| 167 | 6x |
.var = .var, |
| 168 | 6x |
.ref_group = list(ref$ref_group), |
| 169 | 6x |
.in_ref_col = ref$in_ref_col, |
| 170 | 6x |
dots_extra_args |
| 171 |
) |
|
| 172 |
) |
|
| 173 | ||
| 174 |
# Format according to specifications |
|
| 175 | 6x |
format_stats( |
| 176 | 6x |
x_stats, |
| 177 | 6x |
method_groups = "relative_risk", |
| 178 | 6x |
stats_in = .stats, |
| 179 | 6x |
formats_in = .formats, |
| 180 | 6x |
labels_in = .labels, |
| 181 | 6x |
indents_in = .indent_mods |
| 182 |
) |
|
| 183 |
} |
|
| 184 | ||
| 185 | ||
| 186 |
#' @keywords internal |
|
| 187 |
safe_mh_test <- function(...) {
|
|
| 188 | 13x |
tryCatch( |
| 189 | 13x |
stats::mantelhaen.test(...), |
| 190 | 13x |
error = function(e) list(p.value = NA_real_) |
| 191 |
) |
|
| 192 |
} |
|
| 193 | ||
| 194 |
#' Relative Risk CMH Statistic |
|
| 195 |
#' |
|
| 196 |
#' Calculates the relative risk which is defined as the ratio between the |
|
| 197 |
#' response rates between the experimental treatment group and the control treatment group, adjusted |
|
| 198 |
#' for stratification factors by applying Cochran-Mantel-Haenszel (CMH) weights. |
|
| 199 |
#' |
|
| 200 |
#' @inheritParams proposal_argument_convention |
|
| 201 |
#' @param tbl (`array`)\cr a 2 by 2 by number of strata contingency table, as returned |
|
| 202 |
#' in the `tbl` element of [tern::h_prepare_rsp_table()]. Rows (first dimension) are reference |
|
| 203 |
#' then treatment; columns (second dimension) are responders then nonresponders; |
|
| 204 |
#' the third dimension represents the strata. |
|
| 205 |
#' |
|
| 206 |
#' @return A list with elements `rel_risk_ci` and `pval`. |
|
| 207 |
#' @examples |
|
| 208 |
#' |
|
| 209 |
#' set.seed(2) |
|
| 210 |
#' rsp <- factor( |
|
| 211 |
#' sample(c(TRUE, FALSE), 100, TRUE), |
|
| 212 |
#' levels = c(TRUE, FALSE) |
|
| 213 |
#' ) |
|
| 214 |
#' grp <- sample(c("Placebo", "Treatment"), 100, TRUE)
|
|
| 215 |
#' grp <- factor(grp, levels = c("Placebo", "Treatment"))
|
|
| 216 |
#' strata_data <- data.frame( |
|
| 217 |
#' "f1" = sample(c("a", "b"), 100, TRUE),
|
|
| 218 |
#' "f2" = sample(c("x", "y", "z"), 100, TRUE),
|
|
| 219 |
#' stringsAsFactors = TRUE |
|
| 220 |
#' ) |
|
| 221 |
#' |
|
| 222 |
#' tbl <- table(grp, rsp, interaction(strata_data)) |
|
| 223 |
#' prop_ratio_cmh( |
|
| 224 |
#' tbl = tbl, |
|
| 225 |
#' conf_level = 0.90 |
|
| 226 |
#' ) |
|
| 227 |
#' |
|
| 228 |
#' @export |
|
| 229 |
prop_ratio_cmh <- function(tbl, conf_level = 0.95) {
|
|
| 230 | 13x |
assert_response_table(tbl, stratified = TRUE) |
| 231 | 10x |
checkmate::assert_number(conf_level, lower = 0, upper = 1) |
| 232 | 10x |
if (any(apply(tbl, 3L, sum) < 5)) {
|
| 233 | ! |
warning("Less than 5 observations in some strata.")
|
| 234 |
} |
|
| 235 |
# Rows are reference then treatment; columns are strata, including a single stratum. |
|
| 236 | 10x |
n_resp <- matrix(tbl[, 1L, ], nrow = 2) |
| 237 | 10x |
n <- margin.table(tbl, margin = c(1L, 3L)) |
| 238 | 10x |
checkmate::assert_integerish(as.vector(n), lower = 0) |
| 239 | ||
| 240 |
# CMH statistic for relative risk, Treatment over Control |
|
| 241 | 10x |
use_stratum <- colSums(n > 0) == 2L |
| 242 | 10x |
n <- n[, use_stratum, drop = FALSE] |
| 243 | 10x |
n_resp <- n_resp[, use_stratum, drop = FALSE] |
| 244 | 10x |
n_total <- colSums(n) |
| 245 | ||
| 246 | 10x |
rel_risk_num <- sum(n_resp[2L, ] * n[1L, ] / n_total) |
| 247 | 10x |
rel_risk_denom <- sum(n_resp[1L, ] * n[2L, ] / n_total) |
| 248 | 10x |
log_rel_risk <- log(rel_risk_num) - log(rel_risk_denom) |
| 249 | 10x |
rel_risk <- exp(log_rel_risk) |
| 250 | ||
| 251 | 10x |
var_num <- sum((n[1L, ] * n[2L, ] * colSums(n_resp) - n_resp[1L, ] * n_resp[2L, ] * n_total) / n_total^2) |
| 252 | 10x |
var_denom <- rel_risk_num * rel_risk_denom |
| 253 | 10x |
var_log_rel_risk <- var_num / var_denom |
| 254 | 10x |
se_log_rel_risk <- sqrt(var_log_rel_risk) |
| 255 | ||
| 256 | 10x |
z <- stats::qnorm((1 + conf_level) / 2) |
| 257 | 10x |
log_rel_risk_ci <- log_rel_risk + c(-1, +1) * z * se_log_rel_risk |
| 258 | 10x |
ci <- exp(log_rel_risk_ci) |
| 259 | 10x |
rel_risk_ci <- stats::setNames(c(rel_risk, ci), c("est", "lcl", "ucl"))
|
| 260 | ||
| 261 | 10x |
if (any(rowSums(tbl[, 1L, , drop = FALSE]) == 0)) {
|
| 262 | 4x |
rel_risk_ci <- c(est = NA_real_, lcl = 0, ucl = Inf) |
| 263 |
} |
|
| 264 | ||
| 265 | 10x |
pval <- safe_mh_test(tbl, correct = FALSE)$p.value |
| 266 | ||
| 267 | 10x |
list( |
| 268 | 10x |
rel_risk_ci = rel_risk_ci, |
| 269 | 10x |
pval = pval |
| 270 |
) |
|
| 271 |
} |
|
| 272 | ||
| 273 | ||
| 274 |
#' Relative Risk Wald Statistic |
|
| 275 |
#' |
|
| 276 |
#' Estimates the relative response risk in the second group divided by that in the first |
|
| 277 |
#' group, with log-scale Wald confidence limits and a two-sided Wald p-value. |
|
| 278 |
#' |
|
| 279 |
#' @inheritParams proposal_argument_convention |
|
| 280 |
#' @param tbl (`matrix`)\cr a 2 by 2 contingency table, as returned in the |
|
| 281 |
#' `tbl` element of [tern::h_prepare_rsp_table()]. Rows are reference then |
|
| 282 |
#' treatment; columns are responders then nonresponders. |
|
| 283 |
#' @param correction (`flag`)\cr whether to add 0.5 to the response counts and |
|
| 284 |
#' group totals, in order to support the case where there are zero responses in either group |
|
| 285 |
#' (Haldane-Anscombe correction). |
|
| 286 |
#' @return A `list` containing two elements: |
|
| 287 |
#' * `rel_risk_ci`: A named numeric vector (`est`, `lcl`, `ucl`) specifying the point estimate, |
|
| 288 |
#' lower confidence limit, and upper confidence limit of the relative risk. |
|
| 289 |
#' * `pval`: A numeric scalar representing the two-sided Wald p-value. |
|
| 290 |
#' |
|
| 291 |
#' @examples |
|
| 292 |
#' prop_ratio_wald( |
|
| 293 |
#' tbl = rbind(ref = c(1, 1), trt = c(2, 1)), |
|
| 294 |
#' correction = TRUE |
|
| 295 |
#' ) |
|
| 296 |
#' @export |
|
| 297 |
prop_ratio_wald <- function(tbl, conf_level = 0.95, correction = FALSE) {
|
|
| 298 | 28x |
assert_response_table(tbl) |
| 299 | 25x |
checkmate::assert_number(conf_level, lower = 0, upper = 1) |
| 300 | 24x |
checkmate::assert_flag(correction) |
| 301 | ||
| 302 | 23x |
n_resp <- tbl[, 1L] |
| 303 | 23x |
n <- rowSums(tbl) |
| 304 | 23x |
checkmate::assert_true(all(n > 0)) |
| 305 | ||
| 306 | 22x |
if (correction) {
|
| 307 | 11x |
add_on <- 0.5 |
| 308 | 11x |
n_resp <- n_resp + add_on |
| 309 | 11x |
n <- n + add_on |
| 310 |
} |
|
| 311 | 22x |
estimate <- (n_resp[2L] / n[2L]) / (n_resp[1L] / n[1L]) |
| 312 | ||
| 313 | 22x |
if (any(n_resp == 0)) {
|
| 314 | 3x |
if (is.nan(estimate)) {
|
| 315 | 1x |
estimate <- NA_real_ |
| 316 |
} |
|
| 317 | 3x |
ci <- c(NA_real_, NA_real_) |
| 318 | 3x |
pval <- NA_real_ |
| 319 |
} else {
|
|
| 320 | 19x |
se <- sqrt(sum(1 / n_resp - 1 / n)) |
| 321 | 19x |
log_rr <- log(estimate) |
| 322 | 19x |
z <- stats::qnorm((1 + conf_level) / 2) |
| 323 | 19x |
ci <- exp(log_rr + c(-1, 1) * z * se) |
| 324 | 19x |
pval <- if (se == 0) 1 else 2 * stats::pnorm(-abs(log_rr / se)) |
| 325 |
} |
|
| 326 | ||
| 327 | 22x |
list( |
| 328 | 22x |
rel_risk_ci = stats::setNames( |
| 329 | 22x |
c(estimate, ci), |
| 330 | 22x |
c("est", "lcl", "ucl")
|
| 331 |
), |
|
| 332 | 22x |
pval = unname(pval) |
| 333 |
) |
|
| 334 |
} |
| 1 |
#' Stratified Exposure-Adjusted Incidence Rate (EAIR) |
|
| 2 |
#' @name a_eair_strat_j |
|
| 3 |
#' @description |
|
| 4 |
#' Statistical/Analysis function for presenting a stratified exposure-adjusted |
|
| 5 |
#' incidence rate (EAIR) and its Wald confidence interval across strata |
|
| 6 |
#' (e.g., study size weighted exposure). |
|
| 7 |
#' |
|
| 8 |
#' This function is an extension to the unstratified function `a_eair100_j()`. |
|
| 9 |
#' |
|
| 10 |
#' @details |
|
| 11 |
#' The weighting method uses raw event counts `n_ti` in both the rate and variance. |
|
| 12 |
#' \deqn{\hat r_{ti} = \texttt{num\_p\_year} \times \frac{n_{ti}}{T_{ti}}}
|
|
| 13 |
#' |
|
| 14 |
#' |
|
| 15 |
#' The strata weight is `w_i = T_0i + T_1i` (total person-years across |
|
| 16 |
#' **all** groups in strata `i`), and the pooled weighted rate is: |
|
| 17 |
#' \deqn{\hat r_t = \sum_i \frac{w_i}{W} \hat r_{ti}, \quad W = \sum_i w_i}
|
|
| 18 |
#' |
|
| 19 |
#' The Wald 95% CI for the pooled rate is: |
|
| 20 |
#' \deqn{\hat r_t \pm z_{1-\alpha/2} \times \frac{\texttt{num\_p\_year}}{W}
|
|
| 21 |
#' \sqrt{\sum_i w_i^2 \frac{n_{ti}}{T_{ti}^2}}}.
|
|
| 22 |
#' |
|
| 23 |
#' The weighted rate difference and its Wald CI are: |
|
| 24 |
#' \deqn{\hat\delta = \sum_i \frac{w_i}{W}(\hat r_{1i} - \hat r_{0i})}
|
|
| 25 |
#' \deqn{\hat\delta \pm z_{1-\alpha/2} \times \frac{\texttt{num\_p\_year}}{W}
|
|
| 26 |
#' \sqrt{\sum_i w_i^2 \left(\frac{n_{1i}+c}{T_{1i}^2} + \frac{n_{0i}}{T_{0i}^2}\right)}}
|
|
| 27 |
#' |
|
| 28 |
#' @inherit a_eair100_j params |
|
| 29 |
#' @inherit s_eair100_levii_j params |
|
| 30 |
#' |
|
| 31 |
#' @param strata (`string`)\cr variable identifying strata (e.g., study identifier). |
|
| 32 |
#' |
|
| 33 |
#' @return |
|
| 34 |
#' * `s_eair_strat_levii_j()` returns a list containing the following statistics: |
|
| 35 |
#' |
|
| 36 |
#' \itemize{
|
|
| 37 |
#' \item `n_event_total`: total event count across strata (current arm) |
|
| 38 |
#' \item `person_years_total`: total person-years across strata (current arm) |
|
| 39 |
#' \item `eair_strat`: stratified pooled EAIR per `num_p_year` person-years |
|
| 40 |
#' \item `n_eair_strat`: composite of `n_event_total` and `eair_strat` |
|
| 41 |
#' \item `eair_strat_ci`: Wald CI for `eair_strat` |
|
| 42 |
#' \item `eair_strat_est_ci`: composite of `eair_strat` and `eair_strat_ci` |
|
| 43 |
#' \item `eair_strat_n`: composite of `eair_strat` and `n_event_total` |
|
| 44 |
#' \item `eair_strat_n_py`: composite of `eair_strat`, `n_event_total` and `person_years_total` |
|
| 45 |
#' \item `eair_strat_diff_est_ci`: stratified rate difference with Wald CI |
|
| 46 |
#' (only when `vs_ref_group = TRUE`, otherwise `NULL`) |
|
| 47 |
#' \item `eair_strat_diff_est`: estimate for the difference in stratified rate |
|
| 48 |
#' \item `eair_strat_diff_ci`: CI (Wald type) for the difference in stratified rate |
|
| 49 |
#' } |
|
| 50 |
#' |
|
| 51 |
#' |
|
| 52 |
#' The list of available statistics (core columns) can also be viewed by |
|
| 53 |
#' running `junco_get_stats("a_eair_strat_j")`.
|
|
| 54 |
#' @seealso [a_eair100_j()] |
|
| 55 |
#' @references |
|
| 56 |
#' Scosyrev, E & Pethe, A (2022). Confidence intervals for exposure-adjusted rate |
|
| 57 |
#' differences in randomized trials. |
|
| 58 |
#' Pharmaceutical Statistics, 2022;21:103–121. |
|
| 59 |
#' doi: 10.1002/pst.2155 |
|
| 60 |
#' |
|
| 61 |
#' Crowe, B., Chuang-Stein, C., Lettis, S., & Brueckner, A. (2016). |
|
| 62 |
#' Reporting adverse drug reactions in product labels. |
|
| 63 |
#' Therapeutic Innovation & Regulatory Science, 50(4), 455-463. |
|
| 64 |
#' doi: 10.1177/2168479016628574 |
|
| 65 | ||
| 66 | ||
| 67 |
#' |
|
| 68 |
#' |
|
| 69 |
#' @keywords internal |
|
| 70 |
s_eair_strat_levii_j <- function( |
|
| 71 |
levii, |
|
| 72 |
df, |
|
| 73 |
.df_row, |
|
| 74 |
.alt_df_full, |
|
| 75 |
.var, |
|
| 76 |
strata, |
|
| 77 |
id = "USUBJID", |
|
| 78 |
fup_var, |
|
| 79 |
occ_var = NULL, |
|
| 80 |
occ_dy = NULL, |
|
| 81 |
num_p_year = 100, |
|
| 82 |
count_multiple_events = FALSE, |
|
| 83 |
conf_level = 0.95, |
|
| 84 |
vs_ref_group = FALSE, |
|
| 85 |
.in_ref_col, |
|
| 86 |
cur_col_expr, |
|
| 87 |
ref_col_expr, |
|
| 88 |
inriskdiffcol = FALSE |
|
| 89 |
) {
|
|
| 90 | 40x |
checkmate::assert_string(strata) |
| 91 | 40x |
checkmate::assert_string(fup_var) |
| 92 | 40x |
checkmate::assert_string(occ_var, null.ok = TRUE) |
| 93 | 40x |
checkmate::assert_flag(vs_ref_group) |
| 94 | 40x |
if (!is.null(occ_var)) {
|
| 95 | 36x |
checkmate::assert_string(occ_dy, null.ok = FALSE) |
| 96 |
} |
|
| 97 | 40x |
checkmate::assert_names(colnames(.alt_df_full), must.include = c(id, fup_var, strata)) |
| 98 | ||
| 99 | 40x |
denom_df_full_mod <- h_get_eair_df( |
| 100 | 40x |
levii = levii, |
| 101 | 40x |
df = .df_row, |
| 102 | 40x |
denom_df = .alt_df_full, |
| 103 | 40x |
.var = .var, |
| 104 | 40x |
id = id, |
| 105 | 40x |
fup_var = fup_var, |
| 106 | 40x |
occ_var = occ_var, |
| 107 | 40x |
occ_dy = occ_dy, |
| 108 | 40x |
count_multiple_events = count_multiple_events, |
| 109 | 40x |
strata = strata |
| 110 |
) |
|
| 111 | 40x |
denom_df_full_mod <- denom_df_full_mod$df_denom |
| 112 | ||
| 113 |
# study weights from FULL denominator (all arms combined per study) |
|
| 114 | 40x |
w_by_strata <- tapply( |
| 115 | 40x |
denom_df_full_mod[["mod_fup_var"]], |
| 116 | 40x |
denom_df_full_mod[[strata]], |
| 117 | 40x |
sum, |
| 118 | 40x |
na.rm = TRUE |
| 119 |
) |
|
| 120 | 40x |
strata_levels <- names(w_by_strata) |
| 121 | ||
| 122 |
# gather common arguments for .get_stratum |
|
| 123 | 40x |
.alt_df_full_cur_group <- subset(.alt_df_full, eval(cur_col_expr)) |
| 124 | 40x |
args_get_stratum <- list( |
| 125 | 40x |
strata = strata, |
| 126 | 40x |
strata_levels = strata_levels, |
| 127 | 40x |
levii = levii, |
| 128 | 40x |
.var = .var, |
| 129 | 40x |
id = id, |
| 130 | 40x |
fup_var = fup_var, |
| 131 | 40x |
occ_var = occ_var, |
| 132 | 40x |
occ_dy = occ_dy, |
| 133 | 40x |
count_multiple_events = count_multiple_events |
| 134 |
) |
|
| 135 | 40x |
stratum_stats <- do.call( |
| 136 | 40x |
.get_stratum_stats, |
| 137 | 40x |
args = append( |
| 138 | 40x |
list( |
| 139 | 40x |
df_arm = df, |
| 140 | 40x |
denom_arm = .alt_df_full_cur_group |
| 141 |
), |
|
| 142 | 40x |
args_get_stratum |
| 143 |
) |
|
| 144 |
) |
|
| 145 | ||
| 146 |
# gather common arguments for h_s_eair_strat_numbers |
|
| 147 | 40x |
args_get_numbers <- list( |
| 148 | 40x |
strata = strata, |
| 149 | 40x |
w_by_strata = w_by_strata, |
| 150 | 40x |
num_p_year = num_p_year |
| 151 |
) |
|
| 152 | ||
| 153 | 40x |
curcore <- do.call( |
| 154 | 40x |
h_s_eair_strat_numbers, |
| 155 | 40x |
args = append( |
| 156 | 40x |
list(stratum_stats = stratum_stats), |
| 157 | 40x |
args_get_numbers |
| 158 |
) |
|
| 159 |
) |
|
| 160 | ||
| 161 | 40x |
n_vec <- curcore$n_vec |
| 162 | 40x |
py_vec <- curcore$py_vec |
| 163 | 40x |
r_vec <- curcore$r_vec |
| 164 | 40x |
w_vec <- curcore$w_vec |
| 165 | ||
| 166 | 40x |
.stats <- h_s_eair_strat_estimates( |
| 167 | 40x |
n_vec = n_vec, |
| 168 | 40x |
py_vec = py_vec, |
| 169 | 40x |
r_vec = r_vec, |
| 170 | 40x |
w_vec = w_vec, |
| 171 | 40x |
conf_level = conf_level, |
| 172 | 40x |
num_p_year = num_p_year |
| 173 |
) |
|
| 174 | ||
| 175 |
# rate difference vs reference arm |
|
| 176 | 40x |
diff_stats <- list() |
| 177 | 40x |
if (vs_ref_group) {
|
| 178 | 25x |
ref_denom_df <- subset(.alt_df_full, eval(ref_col_expr)) |
| 179 | 25x |
ref_df <- subset(.df_row, eval(ref_col_expr)) |
| 180 | ||
| 181 | 25x |
ref_stratum_stats <- do.call( |
| 182 | 25x |
.get_stratum_stats, |
| 183 | 25x |
args = append( |
| 184 | 25x |
list( |
| 185 | 25x |
df_arm = ref_df, |
| 186 | 25x |
denom_arm = ref_denom_df |
| 187 |
), |
|
| 188 | 25x |
args_get_stratum |
| 189 |
) |
|
| 190 |
) |
|
| 191 | ||
| 192 | 25x |
refcore <- do.call( |
| 193 | 25x |
h_s_eair_strat_numbers, |
| 194 | 25x |
args = append( |
| 195 | 25x |
list(stratum_stats = ref_stratum_stats), |
| 196 | 25x |
args_get_numbers |
| 197 |
) |
|
| 198 |
) |
|
| 199 | ||
| 200 | 25x |
ref_n_vec <- refcore$n_vec |
| 201 | 25x |
ref_py_vec <- refcore$py_vec |
| 202 | 25x |
ref_r_vec <- refcore$r_vec |
| 203 | ||
| 204 | 25x |
diff_stats <- h_s_eair_strat_diff_estimates( |
| 205 | 25x |
n_vec = n_vec, |
| 206 | 25x |
py_vec = py_vec, |
| 207 | 25x |
r_vec = r_vec, |
| 208 | 25x |
w_vec = w_vec, |
| 209 | 25x |
ref_n_vec = ref_n_vec, |
| 210 | 25x |
ref_py_vec = ref_py_vec, |
| 211 | 25x |
ref_r_vec = ref_r_vec, |
| 212 | 25x |
num_p_year = num_p_year, |
| 213 | 25x |
conf_level = conf_level |
| 214 |
) |
|
| 215 |
} else {
|
|
| 216 | 15x |
diff_stats$eair_strat_diff_est_ci <- numeric(0) |
| 217 |
} |
|
| 218 | ||
| 219 | 40x |
attr(diff_stats$eair_strat_diff_est_ci, "label") <- paste0( |
| 220 | 40x |
"Difference in stratified EAIR (",
|
| 221 | 40x |
formatC(conf_level * 100, format = "f", digits = 0), "% CI, Wald)" |
| 222 |
) |
|
| 223 | ||
| 224 | 40x |
.stats <- append(.stats, diff_stats) |
| 225 | 40x |
.stats |
| 226 |
} |
|
| 227 | ||
| 228 |
#' @describeIn a_eair_strat_j |
|
| 229 |
#' Formatted analysis function for stratified (eg study size-weighted) EAIR, used as `afun` |
|
| 230 |
#' in [rtables::analyze()]. |
|
| 231 |
#' |
|
| 232 |
#' |
|
| 233 |
#' @return |
|
| 234 |
#' * `a_eair_strat_j()` returns a list of formatted [rtables::CellValue()] rows. |
|
| 235 |
#' Within a risk difference column, |
|
| 236 |
#' the statistics `eair_strat` and `n_eair_strat` are replaced by `eair_strat_diff_est_ci` |
|
| 237 |
#' (difference in EAIR between current group and reference group, |
|
| 238 |
#' together with it's confidence interval). |
|
| 239 |
#' @export |
|
| 240 |
#' |
|
| 241 |
#' @examples |
|
| 242 |
#' library(tern) |
|
| 243 |
#' library(dplyr) |
|
| 244 |
#' |
|
| 245 |
#' set.seed(42) |
|
| 246 |
#' n <- 120 |
|
| 247 |
#' adsl <- data.frame( |
|
| 248 |
#' USUBJID = paste0("S", seq_len(n)),
|
|
| 249 |
#' ARM = rep(c("Treatment", "Placebo"), each = n / 2),
|
|
| 250 |
#' STUDY = rep(c("Study1", "Study2", "Study3"), times = n / 3),
|
|
| 251 |
#' TRTDURY = runif(n, 0.5, 3), |
|
| 252 |
#' stringsAsFactors = FALSE |
|
| 253 |
#' ) |
|
| 254 |
#' adae <- data.frame( |
|
| 255 |
#' USUBJID = sample(adsl$USUBJID, 40), |
|
| 256 |
#' AEDECOD = "Headache", |
|
| 257 |
#' ASTDY = sample(30:500, 40, replace = TRUE), |
|
| 258 |
#' AOCCPFL = "Y", |
|
| 259 |
#' stringsAsFactors = FALSE |
|
| 260 |
#' ) |
|
| 261 |
#' aefup <- merge(adae, adsl, by = "USUBJID") |
|
| 262 |
#' |
|
| 263 |
#' # Standard method (matches rdcomaeinci SAS macro) |
|
| 264 |
#' lyt <- basic_table() |> |
|
| 265 |
#' split_cols_by("ARM") |>
|
|
| 266 |
#' analyze( |
|
| 267 |
#' "AEDECOD", |
|
| 268 |
#' afun = a_eair_strat_j, |
|
| 269 |
#' extra_args = list( |
|
| 270 |
#' strata = "STUDY", |
|
| 271 |
#' fup_var = "TRTDURY", |
|
| 272 |
#' occ_var = "AOCCPFL", |
|
| 273 |
#' occ_dy = "ASTDY", |
|
| 274 |
#' .stats = c("eair_strat", "eair_strat_ci")
|
|
| 275 |
#' ) |
|
| 276 |
#' ) |
|
| 277 |
#' result <- build_table(lyt, aefup, alt_counts_df = adsl) |
|
| 278 |
#' result |
|
| 279 |
a_eair_strat_j <- function( |
|
| 280 |
df, |
|
| 281 |
labelstr = NULL, |
|
| 282 |
.var, |
|
| 283 |
.df_row, |
|
| 284 |
.spl_context, |
|
| 285 |
.alt_df_full = NULL, |
|
| 286 |
id = "USUBJID", |
|
| 287 |
strata, |
|
| 288 |
fup_var, |
|
| 289 |
occ_var = NULL, |
|
| 290 |
occ_dy = NULL, |
|
| 291 |
num_p_year = 100, |
|
| 292 |
count_multiple_events = FALSE, |
|
| 293 |
drop_levels = FALSE, |
|
| 294 |
riskdiff = FALSE, |
|
| 295 |
ref_path = NULL, |
|
| 296 |
conf_level = 0.95, |
|
| 297 |
.stats = "eair_strat", |
|
| 298 |
.formats = NULL, |
|
| 299 |
.labels = NULL, |
|
| 300 |
.indent_mods = NULL, |
|
| 301 |
na_str = rep("NA", 3),
|
|
| 302 |
row_labels_adj = FALSE, |
|
| 303 |
riskdiff_setup = c("horizontal", "vertical")
|
|
| 304 |
) {
|
|
| 305 | 33x |
riskdiff_setup <- match.arg(riskdiff_setup) |
| 306 | ||
| 307 | 33x |
if (is.null(.alt_df_full)) {
|
| 308 | ! |
stop("a_eair_strat_j: .alt_df_full cannot be NULL. Specify `alt_counts_df` in `build_table`.")
|
| 309 |
} |
|
| 310 | 33x |
if (riskdiff && is.null(ref_path)) {
|
| 311 | 1x |
stop("a_eair_strat_j: ref_path cannot be NULL when riskdiff = TRUE.")
|
| 312 |
} |
|
| 313 | 32x |
checkmate::assert_string(strata) |
| 314 | 32x |
checkmate::assert_names(colnames(.alt_df_full), must.include = c(id, fup_var, strata)) |
| 315 | ||
| 316 |
# if no stats requested, get all stats |
|
| 317 | 31x |
.stats <- junco_get_stats( |
| 318 | 31x |
"a_eair_strat_j", |
| 319 | 31x |
stats_in = .stats, |
| 320 | 31x |
custom_stats_in = NULL |
| 321 |
) |
|
| 322 |
if ( |
|
| 323 | 31x |
any( |
| 324 | 31x |
.stats %in% c("eair_strat_diff_est_ci", "eair_strat_diff_est", "eair_strat_diff_ci")
|
| 325 |
) && |
|
| 326 | 31x |
riskdiff_setup == "horizontal" |
| 327 |
) {
|
|
| 328 | ! |
.stats <- |
| 329 | ! |
.stats[!(.stats %in% c("eair_strat_diff_est_ci", "eair_strat_diff_est", "eair_strat_diff_ci"))]
|
| 330 |
} |
|
| 331 | ||
| 332 | 31x |
cur_col_expr <- .spl_context$cur_col_expr[[1]] |
| 333 | 31x |
colid <- .spl_context$cur_col_id[[1]] |
| 334 | 31x |
inriskdiffcol <- grepl("difference", tolower(colid), fixed = TRUE)
|
| 335 | ||
| 336 |
# reference group expressions and data (for rate difference) |
|
| 337 | 31x |
ref_col_expr <- NULL |
| 338 | 31x |
ref_df <- NULL |
| 339 | 31x |
ref_denom_df <- NULL |
| 340 | 31x |
.in_ref_col <- NULL |
| 341 | ||
| 342 | 31x |
ref_info <- get_ref_info_expanded( |
| 343 | 31x |
df, |
| 344 | 31x |
.var, |
| 345 | 31x |
.df_row, |
| 346 | 31x |
.spl_context, |
| 347 | 31x |
ref_path, |
| 348 | 31x |
riskdiff, |
| 349 | 31x |
riskdiff_setup |
| 350 |
) |
|
| 351 | ||
| 352 | 31x |
perform_vs_ref_stats <- ref_info$perform_vs_ref_stats |
| 353 | 31x |
ref_col_expr <- ref_info$ref_col_expr |
| 354 | 31x |
.in_ref_col <- ref_info$.in_ref_col |
| 355 | ||
| 356 | 31x |
if (riskdiff) {
|
| 357 | 29x |
ref_denom_df <- subset(.alt_df_full, eval(ref_col_expr)) |
| 358 | 29x |
ref_df <- subset(.df_row, eval(ref_col_expr)) |
| 359 |
} |
|
| 360 | ||
| 361 | 31x |
inrowsplit <- FALSE |
| 362 | 31x |
if (.var == tail(.spl_context$split, 1L)) {
|
| 363 |
# note: this will catch the rowsplit only if parent_name in split_rows_by call is .var (default) |
|
| 364 | 24x |
drop_levels <- TRUE |
| 365 | 24x |
inrowsplit <- TRUE |
| 366 |
} |
|
| 367 | ||
| 368 |
### combine all preprocessing of incoming df/.df_row in one function |
|
| 369 |
### do this outside stats derivation functions (s_freq_j/) |
|
| 370 |
### use all of val/excl_levels/drop_levels//new_levels/label/label_map/labelstr/label_fstr |
|
| 371 | 31x |
upd_dfrow <- h_upd_dfrow( |
| 372 | 31x |
.df_row, |
| 373 | 31x |
.var, |
| 374 | 31x |
val = NULL, |
| 375 | 31x |
excl_levels = NULL, |
| 376 | 31x |
drop_levels = drop_levels, |
| 377 | 31x |
new_levels = NULL, |
| 378 | 31x |
new_levels_after = FALSE, |
| 379 | 31x |
label = NULL, |
| 380 | 31x |
label_map = NULL, |
| 381 | 31x |
labelstr = labelstr, |
| 382 | 31x |
label_fstr = NULL, |
| 383 | 31x |
.spl_context = .spl_context |
| 384 |
) |
|
| 385 | ||
| 386 | 31x |
.df_row <- upd_dfrow$df_row |
| 387 | 31x |
df <- upd_dfrow$df |
| 388 | ||
| 389 | 31x |
levs <- levels(.df_row[[.var]]) |
| 390 | ! |
if (is.null(levs)) levs <- unique(.df_row[[.var]]) |
| 391 | ||
| 392 | 31x |
fn_Args <- list( |
| 393 | 31x |
strata = strata, |
| 394 | 31x |
df = df, |
| 395 | 31x |
.df_row = .df_row, |
| 396 | 31x |
.var = .var, |
| 397 | 31x |
id = id, |
| 398 | 31x |
fup_var = fup_var, |
| 399 | 31x |
occ_var = occ_var, |
| 400 | 31x |
occ_dy = occ_dy, |
| 401 | 31x |
num_p_year = num_p_year, |
| 402 | 31x |
count_multiple_events = count_multiple_events, |
| 403 | 31x |
cur_col_expr = cur_col_expr, |
| 404 | 31x |
ref_col_expr = ref_col_expr, |
| 405 | 31x |
vs_ref_group = perform_vs_ref_stats, |
| 406 | 31x |
.in_ref_col = !perform_vs_ref_stats, |
| 407 | 31x |
inriskdiffcol = riskdiff_setup == "horizontal" && inriskdiffcol, |
| 408 | 31x |
conf_level = conf_level, |
| 409 | 31x |
.alt_df_full = .alt_df_full |
| 410 |
) |
|
| 411 | ||
| 412 | 31x |
y <- mapply( |
| 413 | 31x |
s_eair_strat_levii_j, |
| 414 | 31x |
levii = levs, |
| 415 | 31x |
MoreArgs = fn_Args, |
| 416 | 31x |
SIMPLIFY = FALSE |
| 417 |
) |
|
| 418 | ||
| 419 |
### rearrange list y to list to x_stats |
|
| 420 |
# y is a nested list with |
|
| 421 |
# outer elements levels of incoming variable |
|
| 422 |
# inner elements stat elements statistics from |
|
| 423 | ||
| 424 |
#### this is to ensure the remainder of the code can stay the same as in a_freq_j |
|
| 425 | 31x |
stnms <- junco_get_stats("a_eair_strat_j")
|
| 426 | 31x |
x_stats <- extract_x_stats(y, stnms) |
| 427 | ||
| 428 |
# x_stats is a nested list with |
|
| 429 |
# outer elements stat elements statistics (all 5 from stnms) |
|
| 430 |
# inner elements levels of incoming variable |
|
| 431 |
# this will ensure we can perform subsetting on .stats |
|
| 432 |
# like x_stats[.stats] |
|
| 433 | ||
| 434 |
# transpose_named_list cannot be used as this will not ensure all 5 poss stats are in |
|
| 435 | 31x |
if (inriskdiffcol && riskdiff_setup == "horizontal") {
|
| 436 | ! |
.stats_adj <- |
| 437 | ! |
replace( |
| 438 | ! |
.stats, |
| 439 | ! |
.stats %in% |
| 440 | ! |
c("eair_strat", "n_eair_strat", "eair_strat_n", "eair_strat_n_py"),
|
| 441 | ! |
"eair_strat_diff_est_ci" |
| 442 |
) |
|
| 443 |
} else {
|
|
| 444 | 31x |
.stats_adj <- .stats |
| 445 |
} |
|
| 446 | ||
| 447 | 31x |
.stats <- .stats_adj |
| 448 | ||
| 449 |
# Fill in formatting defaults |
|
| 450 | ||
| 451 | ||
| 452 | 31x |
x_stats <- x_stats[.stats] |
| 453 | 31x |
x_stats_orig <- x_stats |
| 454 |
# from now onwards, retransform x_stats back into structure |
|
| 455 |
# list with outer elements stats levels from incoming variable |
|
| 456 |
# inner elements statistics |
|
| 457 | 31x |
x_stats <- transpose_named_list(x_stats) |
| 458 | ||
| 459 | 31x |
levels_per_stats <- lapply(x_stats, names) |
| 460 |
# in case no default label get the label from the x_stats directly |
|
| 461 |
# this is to cover statistics with dynamic label, such as |
|
| 462 |
# n_event Either Number of events or Number of subjects with events |
|
| 463 | 31x |
.labels_pre <- junco_get_labels_from_attrib(.stats, .labels, x_stats) |
| 464 | ||
| 465 | 31x |
.formats <- junco_get_formats_from_stats(.stats, .formats, levels_per_stats) |
| 466 | 31x |
.labels <- junco_get_labels_from_stats(.stats, .labels_pre, levels_per_stats) |
| 467 | 31x |
.labels <- .unlist_keep_nulls(.labels) |
| 468 | ||
| 469 |
# make adjustments to rowlabels when not inrowsplit |
|
| 470 | 31x |
if (!inrowsplit && length(.stats) > 1) {
|
| 471 |
# if more than one stat requested and variable is not in a prior splitrowscall |
|
| 472 |
# prepend the level of the incoming variable (which is now in names of x_stats) to the label |
|
| 473 | 5x |
.labels <- paste(rep(names(x_stats), each = length(.stats)), .labels) |
| 474 | 26x |
} else if (!inrowsplit && length(.stats) == 1 && row_labels_adj) {
|
| 475 |
# if one stat requested and variable is not in a prior splitrowscall |
|
| 476 |
# show the levels of the incoming variable, rather than the statistic |
|
| 477 | ! |
.labels <- names(x_stats) |
| 478 |
} |
|
| 479 | 31x |
.labels <- gsub("^\\s+", "", .labels)
|
| 480 | ||
| 481 | 31x |
.indent_mods <- junco_get_indents_from_stats( |
| 482 | 31x |
.stats, |
| 483 | 31x |
.indent_mods, |
| 484 | 31x |
levels_per_stats |
| 485 |
) |
|
| 486 | 31x |
.indent_mods <- .unlist_keep_nulls(.indent_mods) |
| 487 | ||
| 488 |
# .format_na_strs processing |
|
| 489 |
# if na_str = c(NA, NA, NA) |
|
| 490 |
# this will ensure the ci (NA, NA, NA) will be represented as NE (NE, NE) |
|
| 491 |
# the value NE is defined as the default to replace NA in our jjcs format |
|
| 492 | ||
| 493 | 31x |
if (!is.null(na_str)) {
|
| 494 | 31x |
.format_na_strs <- lapply(names(.formats), FUN = function(x) {
|
| 495 | 152x |
na_str |
| 496 |
}) |
|
| 497 |
} else {
|
|
| 498 | ! |
.format_na_strs <- NULL |
| 499 |
} |
|
| 500 | ||
| 501 |
# Unlist stats + names |
|
| 502 | 31x |
x_stats <- .unlist_keep_nulls(x_stats) |
| 503 | 31x |
names(x_stats) <- names(.formats) |
| 504 | ||
| 505 |
### final step: turn requested stats into rtables rows |
|
| 506 | 31x |
inrows <- in_rows( |
| 507 | 31x |
.list = x_stats, |
| 508 | 31x |
.formats = .formats, |
| 509 | 31x |
.labels = .labels, |
| 510 | 31x |
.indent_mods = .indent_mods, |
| 511 | 31x |
.format_na_strs = .format_na_strs |
| 512 |
) |
|
| 513 |
} |
|
| 514 | ||
| 515 | ||
| 516 |
h_s_eair_strat_numbers <- function(strata, |
|
| 517 |
w_by_strata, |
|
| 518 |
stratum_stats, |
|
| 519 |
num_p_year) {
|
|
| 520 | 65x |
w_vec <- as.numeric(w_by_strata) |
| 521 | 65x |
W <- sum(w_vec) |
| 522 | 65x |
cur_stats <- stratum_stats |
| 523 | 65x |
n_vec <- sapply(cur_stats, `[[`, "n") |
| 524 | 65x |
py_vec <- sapply(cur_stats, `[[`, "py") |
| 525 | ||
| 526 |
# per-study rates |
|
| 527 | 65x |
r_vec <- num_p_year * (n_vec) / py_vec |
| 528 | ||
| 529 | 65x |
return(list( |
| 530 | 65x |
n_vec = n_vec, |
| 531 | 65x |
py_vec = py_vec, |
| 532 | 65x |
r_vec = r_vec, |
| 533 | 65x |
w_vec = w_vec |
| 534 |
)) |
|
| 535 |
} |
|
| 536 | ||
| 537 |
h_s_eair_strat_estimates <- function(n_vec, |
|
| 538 |
py_vec, |
|
| 539 |
r_vec, |
|
| 540 |
w_vec, |
|
| 541 |
conf_level, |
|
| 542 |
num_p_year) {
|
|
| 543 | 40x |
W <- sum(w_vec) |
| 544 | ||
| 545 |
# weighted pooled rate |
|
| 546 | 40x |
eair_strat <- if (W > 0) sum(w_vec * r_vec) / W else NA_real_ |
| 547 | ||
| 548 |
# Wald variance for pooled rate: (num_p_year/W)^2 * sum(w_i^2 * (n_i) / T_i^2) |
|
| 549 | 40x |
root_sum <- sum(w_vec^2 * (n_vec) / py_vec^2) |
| 550 | 40x |
z <- stats::qnorm(1 - (1 - conf_level) / 2) |
| 551 | 40x |
se_rate <- (num_p_year / W) * sqrt(root_sum) |
| 552 | 40x |
eair_ci <- c(max(0, eair_strat - z * se_rate), eair_strat + z * se_rate) |
| 553 | 40x |
n_total <- sum(n_vec) |
| 554 | 40x |
py_total <- sum(py_vec) |
| 555 | ||
| 556 |
# assemble return list |
|
| 557 | 40x |
x <- list() |
| 558 | ||
| 559 | 40x |
x$n_event_total <- stats::setNames(n_total, "n_event_total") |
| 560 | 40x |
attr(x$n_event_total, "label") <- "n (events)" |
| 561 | ||
| 562 | 40x |
x$person_years_total <- stats::setNames(py_total, "person_years_total") |
| 563 | 40x |
attr(x$person_years_total, "label") <- "Total person-years" |
| 564 | ||
| 565 | 40x |
x$eair_strat <- stats::setNames(eair_strat, "eair_strat") |
| 566 | 40x |
attr(x$eair_strat, "label") <- paste0( |
| 567 | 40x |
"Stratified EAIR per ", num_p_year, " PY" |
| 568 |
) |
|
| 569 | ||
| 570 | 40x |
x$n_eair_strat <- stats::setNames( |
| 571 | 40x |
c(n_total, eair_strat), |
| 572 | 40x |
c("n_event_total", "eair_strat")
|
| 573 |
) |
|
| 574 | 40x |
attr(x$n_eair_strat, "label") <- paste0("n (stratified EAIR per ", num_p_year, " PY)")
|
| 575 | ||
| 576 | 40x |
x$eair_strat_ci <- stats::setNames(eair_ci, c("lcl", "ucl"))
|
| 577 | 40x |
attr(x$eair_strat_ci, "label") <- paste0( |
| 578 | 40x |
formatC(conf_level * 100, format = "f", digits = 0), "% CI (Wald)" |
| 579 |
) |
|
| 580 | ||
| 581 | 40x |
x$eair_strat_est_ci <- stats::setNames( |
| 582 | 40x |
c(eair_strat, eair_ci), |
| 583 | 40x |
c("eair_strat", "lcl", "ucl")
|
| 584 |
) |
|
| 585 | 40x |
attr(x$eair_strat_est_ci, "label") <- paste0( |
| 586 | 40x |
"Stratified EAIR (", formatC(conf_level * 100, format = "f", digits = 0), "% CI)"
|
| 587 |
) |
|
| 588 | ||
| 589 | 40x |
x$eair_strat_n <- stats::setNames( |
| 590 | 40x |
c(eair_strat, n_total), |
| 591 | 40x |
c("eair_strat", "n_event_total")
|
| 592 |
) |
|
| 593 | 40x |
attr(x$eair_strat_n, "label") <- paste0("stratified EAIR per ", num_p_year, " PY (n)")
|
| 594 | ||
| 595 | 40x |
x$eair_strat_n_py <- stats::setNames( |
| 596 | 40x |
c(eair_strat, n_total), |
| 597 | 40x |
c("eair_strat", "n_event_total")
|
| 598 |
) |
|
| 599 | 40x |
attr(x$eair_strat_n_py, "label") <- paste0("stratified EAIR per ", num_p_year, " PY (n/person-years)")
|
| 600 | ||
| 601 | 40x |
x |
| 602 |
} |
|
| 603 | ||
| 604 |
h_s_eair_strat_diff_estimates <- function(n_vec, |
|
| 605 |
py_vec, |
|
| 606 |
r_vec, |
|
| 607 |
w_vec, |
|
| 608 |
ref_n_vec, |
|
| 609 |
ref_py_vec, |
|
| 610 |
ref_r_vec, |
|
| 611 |
num_p_year, |
|
| 612 |
conf_level) {
|
|
| 613 | 25x |
W <- sum(w_vec) |
| 614 | 25x |
z <- stats::qnorm(1 - (1 - conf_level) / 2) |
| 615 | ||
| 616 | 25x |
d_vec <- r_vec - ref_r_vec |
| 617 | 25x |
d_adj <- if (W > 0) sum(w_vec * d_vec) / W else NA_real_ |
| 618 | ||
| 619 | 25x |
root_diff <- sum(w_vec^2 * ((n_vec) / py_vec^2 + (ref_n_vec) / ref_py_vec^2)) |
| 620 | 25x |
se_diff <- (num_p_year / W) * sqrt(root_diff) |
| 621 | ||
| 622 | 25x |
diff_ci <- c(d_adj - z * se_diff, d_adj + z * se_diff) |
| 623 | ||
| 624 | 25x |
x <- list() |
| 625 | 25x |
x$eair_strat_diff_est_ci <- stats::setNames( |
| 626 | 25x |
c(d_adj, diff_ci), |
| 627 | 25x |
c("estimate", "lcl", "ucl")
|
| 628 |
) |
|
| 629 | 25x |
attr(x$eair_strat_diff_est_ci, "label") <- paste0( |
| 630 | 25x |
"Difference in stratified EAIR (",
|
| 631 | 25x |
formatC(conf_level * 100, format = "f", digits = 0), "% CI, Wald)" |
| 632 |
) |
|
| 633 | ||
| 634 | 25x |
x$eair_strat_diff_est <- stats::setNames( |
| 635 | 25x |
c(d_adj), |
| 636 | 25x |
c("estimate")
|
| 637 |
) |
|
| 638 | 25x |
attr(x$eair_strat_diff_est, "label") <- "Difference in stratified EAIR" |
| 639 | ||
| 640 | 25x |
x$eair_strat_diff_ci <- stats::setNames( |
| 641 | 25x |
c(diff_ci), |
| 642 | 25x |
c("lcl", "ucl")
|
| 643 |
) |
|
| 644 | 25x |
attr(x$eair_strat_diff_ci, "label") <- paste0( |
| 645 | 25x |
formatC(conf_level * 100, format = "f", digits = 0), "% CI (Wald)" |
| 646 |
) |
|
| 647 | ||
| 648 | 25x |
x |
| 649 |
} |
| 1 |
#' Impute missing binary responses and analyze each imputed data set |
|
| 2 |
#' |
|
| 3 |
#' Performs independent Bernoulli imputations for missing binary responses in |
|
| 4 |
#' the control and treatment groups, then calculates a CMH risk difference and |
|
| 5 |
#' p-value for each imputed data set. Only observations in `ctrlab` and |
|
| 6 |
#' `trtlab` are included in the analysis. |
|
| 7 |
#' |
|
| 8 |
#' @param dat (`data.frame`) |
|
| 9 |
#' Analysis data containing treatment, response, and stratification variables. |
|
| 10 |
#' @param p_ctrl (`numeric(1)`) |
|
| 11 |
#' Probability of response used to impute missing control-group responses. |
|
| 12 |
#' @param p_trt (`numeric(1)`) |
|
| 13 |
#' Probability of response used to impute missing treatment-group responses. |
|
| 14 |
#' @param trtvar (`string`) |
|
| 15 |
#' Name of the treatment variable in `dat`. |
|
| 16 |
#' @param ctrlab (`string`) |
|
| 17 |
#' Value of `trtvar` identifying the control group. |
|
| 18 |
#' @param trtlab (`string`) |
|
| 19 |
#' Value of `trtvar` identifying the treatment group. |
|
| 20 |
#' @param respvar (`string`) |
|
| 21 |
#' Name of a logical binary response variable in `dat`. Missing values are |
|
| 22 |
#' imputed; observed values are retained. |
|
| 23 |
#' @param stratvar (`character`) |
|
| 24 |
#' Names of one or more stratification variables in `dat`. Stratification |
|
| 25 |
#' variables must not be missing in the analysis groups. |
|
| 26 |
#' @param n_imputations (`count`) |
|
| 27 |
#' Number of independently imputed data sets to analyze. |
|
| 28 |
#' |
|
| 29 |
#' @return A tibble with one row per imputation and the following columns: |
|
| 30 |
#' |
|
| 31 |
#' * `rd_est`: CMH risk-difference estimate. |
|
| 32 |
#' * `rd_se`: standard error of the risk-difference estimate. |
|
| 33 |
#' * `rd_var`: variance of the risk-difference estimate. |
|
| 34 |
#' * `p_cmh`: CMH p-value using the Wilson-Hilferty transformation. |
|
| 35 |
#' * `z_stat`: Wilson-Hilferty z statistic used to calculate `p_cmh`. |
|
| 36 |
#' |
|
| 37 |
#' @keywords internal |
|
| 38 |
h_impute_analyze_resp <- function( |
|
| 39 |
dat, |
|
| 40 |
p_ctrl, |
|
| 41 |
p_trt, |
|
| 42 |
trtvar, |
|
| 43 |
ctrlab, |
|
| 44 |
trtlab, |
|
| 45 |
respvar, |
|
| 46 |
stratvar, |
|
| 47 |
n_imputations |
|
| 48 |
) {
|
|
| 49 | 3x |
checkmate::assert_data_frame(dat) |
| 50 | 3x |
checkmate::assert_number(p_ctrl, lower = 0, upper = 1, finite = TRUE) |
| 51 | 3x |
checkmate::assert_number(p_trt, lower = 0, upper = 1, finite = TRUE) |
| 52 | 3x |
checkmate::assert_string(trtvar) |
| 53 | 3x |
checkmate::assert_string(ctrlab) |
| 54 | 3x |
checkmate::assert_string(trtlab) |
| 55 | 3x |
checkmate::assert_string(respvar) |
| 56 | 3x |
checkmate::assert_character(stratvar, min.len = 1, any.missing = FALSE, unique = TRUE) |
| 57 | 3x |
checkmate::assert_count(n_imputations, positive = TRUE) |
| 58 | 3x |
checkmate::assert_subset(c(trtvar, respvar, stratvar), choices = names(dat)) |
| 59 | 3x |
checkmate::assert_logical(dat[[respvar]], any.missing = TRUE) |
| 60 | 3x |
checkmate::assert_true(ctrlab != trtlab) |
| 61 | ||
| 62 | 3x |
treatment <- as.character(dat[[trtvar]]) |
| 63 | 3x |
ctrl_data <- dat[which(treatment == ctrlab), , drop = FALSE] |
| 64 | 3x |
trt_data <- dat[which(treatment == trtlab), , drop = FALSE] |
| 65 | ||
| 66 | 3x |
checkmate::assert_true(nrow(ctrl_data) > 0) |
| 67 | 3x |
checkmate::assert_true(nrow(trt_data) > 0) |
| 68 | ||
| 69 | 3x |
analysis_data <- rbind(ctrl_data, trt_data) |
| 70 | 3x |
strata <- interaction(analysis_data[stratvar], drop = TRUE) |
| 71 | 3x |
response <- c(ctrl_data[[respvar]], trt_data[[respvar]]) |
| 72 | 3x |
group <- factor( |
| 73 | 3x |
rep(c("ref", "Not-ref"), c(nrow(ctrl_data), nrow(trt_data))),
|
| 74 | 3x |
levels = c("ref", "Not-ref")
|
| 75 |
) |
|
| 76 | 3x |
missing_response <- is.na(response) |
| 77 | 3x |
missing_ctrl_response <- group == "ref" & missing_response |
| 78 | 3x |
missing_trt_response <- group == "Not-ref" & missing_response |
| 79 | 3x |
n_missing_ctrl <- sum(missing_ctrl_response) |
| 80 | 3x |
n_missing_trt <- sum(missing_trt_response) |
| 81 | ||
| 82 | 3x |
results <- vector("list", n_imputations)
|
| 83 | 3x |
for (i in seq_len(n_imputations)) {
|
| 84 | 7x |
imputed_response <- response |
| 85 | 7x |
if (n_missing_ctrl > 0) {
|
| 86 | 7x |
imputed_response[missing_ctrl_response] <- as.logical(stats::rbinom( |
| 87 | 7x |
n = n_missing_ctrl, |
| 88 | 7x |
size = 1, |
| 89 | 7x |
prob = p_ctrl |
| 90 |
)) |
|
| 91 |
} |
|
| 92 | 7x |
if (n_missing_trt > 0) {
|
| 93 | 7x |
imputed_response[missing_trt_response] <- as.logical(stats::rbinom( |
| 94 | 7x |
n = n_missing_trt, |
| 95 | 7x |
size = 1, |
| 96 | 7x |
prob = p_trt |
| 97 |
)) |
|
| 98 |
} |
|
| 99 | 7x |
rd_result <- tern::prop_diff_cmh(imputed_response, group, strata, diff_se = "standard") |
| 100 | 7x |
cmh_result <- tern::prop_cmh( |
| 101 | 7x |
table(group, imputed_response, strata), |
| 102 | 7x |
transform = "wilson_hilferty" |
| 103 |
) |
|
| 104 | 7x |
results[[i]] <- c( |
| 105 | 7x |
rd_est = rd_result$diff, |
| 106 | 7x |
rd_se = rd_result$se_diff, |
| 107 | 7x |
p_cmh = as.numeric(cmh_result), |
| 108 | 7x |
z_stat = attr(cmh_result, "z_stat") |
| 109 |
) |
|
| 110 |
} |
|
| 111 | ||
| 112 | 3x |
results <- as.data.frame(do.call(rbind, results)) |
| 113 | 3x |
tibble::tibble( |
| 114 | 3x |
rd_est = results$rd_est, |
| 115 | 3x |
rd_se = results$rd_se, |
| 116 | 3x |
rd_var = results$rd_se^2, |
| 117 | 3x |
p_cmh = results$p_cmh, |
| 118 | 3x |
z_stat = results$z_stat |
| 119 |
) |
|
| 120 |
} |
|
| 121 | ||
| 122 |
#' Calculate results across binary-imputation scenarios |
|
| 123 |
#' |
|
| 124 |
#' Runs `h_impute_analyze_resp()` for each paired control and treatment response |
|
| 125 |
#' probability, pools estimates across imputations using Rubin's rules, and |
|
| 126 |
#' pools Wilson-Hilferty z statistics to obtain a p-value. A scenario where both |
|
| 127 |
#' probabilities are zero or one is deterministic and is therefore analyzed |
|
| 128 |
#' once rather than repeatedly imputed. |
|
| 129 |
#' |
|
| 130 |
#' @param dat (`data.frame`) |
|
| 131 |
#' Analysis data containing treatment, response, and stratification variables. |
|
| 132 |
#' @param p_ctrl (`numeric`) |
|
| 133 |
#' Control-group response probabilities, one for each scenario. |
|
| 134 |
#' @param p_trt (`numeric`) |
|
| 135 |
#' Treatment-group response probabilities, paired with `p_ctrl` by position. |
|
| 136 |
#' @param trtvar (`string`) |
|
| 137 |
#' Name of the treatment variable in `dat`. |
|
| 138 |
#' @param ctrlab (`string`) |
|
| 139 |
#' Value of `trtvar` identifying the control group. |
|
| 140 |
#' @param trtlab (`string`) |
|
| 141 |
#' Value of `trtvar` identifying the treatment group. |
|
| 142 |
#' @param respvar (`string`) |
|
| 143 |
#' Name of a logical binary response variable in `dat`. |
|
| 144 |
#' @param stratvar (`character`) |
|
| 145 |
#' Names of one or more stratification variables in `dat`. |
|
| 146 |
#' @param n_imputations (`count`) |
|
| 147 |
#' Number of imputations for non-deterministic scenarios. |
|
| 148 |
#' @param pvalcat (named `list`) |
|
| 149 |
#' P-value categories passed to [categorize_pval()]. |
|
| 150 |
#' |
|
| 151 |
#' @return A tibble with one row per scenario. `rd` and `rd_se` are the pooled |
|
| 152 |
#' risk-difference estimate and standard error; `p_value` and `p_cat` are the |
|
| 153 |
#' pooled p-value and its category. `marker_flag` and `effect_label` contain |
|
| 154 |
#' `"*"` when a deterministic scenario or a single-imputation p-value fallback |
|
| 155 |
#' was used. |
|
| 156 |
#' |
|
| 157 |
#' @examples |
|
| 158 |
#' dat <- data.frame( |
|
| 159 |
#' arm = rep(c("Control", "Treatment"), each = 4),
|
|
| 160 |
#' response = c(TRUE, FALSE, NA, TRUE, TRUE, FALSE, NA, FALSE), |
|
| 161 |
#' strata = rep(c("A", "B"), 4)
|
|
| 162 |
#' ) |
|
| 163 |
#' pvalcat <- list("<0.05" = c(0, 0.05), ">=0.05" = c(0.05, 1))
|
|
| 164 |
#' |
|
| 165 |
#' set.seed(123) |
|
| 166 |
#' resp_multiple_imputation( |
|
| 167 |
#' dat, |
|
| 168 |
#' p_ctrl = c(0, 0.25), |
|
| 169 |
#' p_trt = c(1, 0.75), |
|
| 170 |
#' trtvar = "arm", |
|
| 171 |
#' ctrlab = "Control", |
|
| 172 |
#' trtlab = "Treatment", |
|
| 173 |
#' respvar = "response", |
|
| 174 |
#' stratvar = "strata", |
|
| 175 |
#' n_imputations = 5, |
|
| 176 |
#' pvalcat = pvalcat |
|
| 177 |
#' ) |
|
| 178 |
#' @export |
|
| 179 |
resp_multiple_imputation <- function( |
|
| 180 |
dat, |
|
| 181 |
p_ctrl, |
|
| 182 |
p_trt, |
|
| 183 |
trtvar, |
|
| 184 |
ctrlab, |
|
| 185 |
trtlab, |
|
| 186 |
respvar, |
|
| 187 |
stratvar, |
|
| 188 |
n_imputations, |
|
| 189 |
pvalcat |
|
| 190 |
) {
|
|
| 191 | 1x |
checkmate::assert_numeric(p_ctrl, min.len = 1, lower = 0, upper = 1, finite = TRUE) |
| 192 | 1x |
checkmate::assert_numeric(p_trt, min.len = 1, lower = 0, upper = 1, finite = TRUE) |
| 193 | 1x |
checkmate::assert_true(length(p_ctrl) == length(p_trt)) |
| 194 | 1x |
checkmate::assert_count(n_imputations, positive = TRUE) |
| 195 | 1x |
checkmate::assert_list(h_normalize_pvalcat(pvalcat)) # Just to check that it works. |
| 196 | ||
| 197 | 1x |
results <- lapply(seq_along(p_ctrl), function(i) {
|
| 198 | 2x |
control_probability <- p_ctrl[[i]] |
| 199 | 2x |
treatment_probability <- p_trt[[i]] |
| 200 | 2x |
m <- if (all(c(control_probability, treatment_probability) %in% c(0, 1))) 1L else n_imputations |
| 201 | ||
| 202 | 2x |
per_imputation <- h_impute_analyze_resp( |
| 203 | 2x |
dat = dat, |
| 204 | 2x |
p_ctrl = control_probability, |
| 205 | 2x |
p_trt = treatment_probability, |
| 206 | 2x |
trtvar = trtvar, |
| 207 | 2x |
ctrlab = ctrlab, |
| 208 | 2x |
trtlab = trtlab, |
| 209 | 2x |
respvar = respvar, |
| 210 | 2x |
stratvar = stratvar, |
| 211 | 2x |
n_imputations = m |
| 212 |
) |
|
| 213 | ||
| 214 | 2x |
if (m == 1) {
|
| 215 | 1x |
estimate <- per_imputation$rd_est[[1]] |
| 216 | 1x |
standard_error <- per_imputation$rd_se[[1]] |
| 217 | 1x |
p_value <- per_imputation$p_cmh[[1]] |
| 218 | 1x |
used_fallback <- TRUE |
| 219 |
} else {
|
|
| 220 | 1x |
pooled_rd <- pool_rubin_scalar(per_imputation$rd_est, per_imputation$rd_var) |
| 221 | 1x |
estimate <- pooled_rd$est |
| 222 | 1x |
standard_error <- pooled_rd$se |
| 223 | 1x |
pooled_p_value <- pool_z_stat(per_imputation$z_stat)$p |
| 224 | 1x |
used_fallback <- is.na(pooled_p_value) || !is.finite(pooled_p_value) |
| 225 | 1x |
p_value <- if (used_fallback) per_imputation$p_cmh[[1]] else pooled_p_value |
| 226 |
} |
|
| 227 | ||
| 228 | 2x |
marker_flag <- if (m == 1 || used_fallback) "*" else "" |
| 229 | 2x |
tibble::tibble( |
| 230 | 2x |
p_ctrl = control_probability, |
| 231 | 2x |
p_trt = treatment_probability, |
| 232 | 2x |
m = m, |
| 233 | 2x |
rd = estimate, |
| 234 | 2x |
rd_se = standard_error, |
| 235 | 2x |
p_value = p_value, |
| 236 | 2x |
p_cat = categorize_pval(p_value, pvalcat), |
| 237 | 2x |
marker_flag = marker_flag, |
| 238 | 2x |
effect_label = paste0( |
| 239 | 2x |
formatC(100 * estimate, format = "f", digits = 1), |
| 240 |
"%", |
|
| 241 | 2x |
marker_flag |
| 242 |
) |
|
| 243 |
) |
|
| 244 |
}) |
|
| 245 | ||
| 246 | 1x |
tibble::as_tibble(do.call(rbind, results)) |
| 247 |
} |
| 1 |
#' @noRd |
|
| 2 |
#' @title internal helpers for ancova |
|
| 3 |
#' @description Internal helper that derives adjusted means and contrasts for a single |
|
| 4 |
#' treatment level from an `emmeans` ANCOVA fit, optionally handling interaction |
|
| 5 |
#' terms and reference group contrasts. |
|
| 6 |
#' |
|
| 7 |
#' @param emmeans_fit (`emmGrid`)\cr |
|
| 8 |
#' Object returned by [emmeans::emmeans()]. |
|
| 9 |
#' @param sum_level (`character`)\cr |
|
| 10 |
#' Single treatment level for which estimates are requested. |
|
| 11 |
#' @inheritParams tern::s_ancova |
|
| 12 |
#' |
|
| 13 |
#' @return A list with components: |
|
| 14 |
#' * `sum_fit_level`: summary statistics for the adjusted mean. |
|
| 15 |
#' * `sum_contrasts_level`: contrast estimates versus reference (if applicable). |
|
| 16 |
#' * `y`: observed response values used for `n`. |
|
| 17 |
#' |
|
| 18 |
#' @keywords internal |
|
| 19 | ||
| 20 |
h_ancova_est_single <- function(emmeans_fit, |
|
| 21 |
sum_level, |
|
| 22 |
interaction_y, |
|
| 23 |
interaction_item, |
|
| 24 |
df, |
|
| 25 |
.var, |
|
| 26 |
.df_row, |
|
| 27 |
.ref_group, |
|
| 28 |
arm, |
|
| 29 |
conf_level, |
|
| 30 |
.in_ref_col) {
|
|
| 31 |
# Ensure that there is only one element in sum_level. |
|
| 32 | 121x |
checkmate::assert_scalar(sum_level) |
| 33 | ||
| 34 | 121x |
sum_fit <- summary( |
| 35 | 121x |
emmeans_fit, |
| 36 | 121x |
level = conf_level |
| 37 |
) |
|
| 38 | ||
| 39 | 121x |
sum_fit_level <- sum_fit[sum_fit[[arm]] == sum_level, , drop = FALSE] |
| 40 | ||
| 41 |
# Get the index of the ref arm |
|
| 42 | 121x |
if (interaction_y != FALSE) {
|
| 43 | 24x |
y <- unlist(df[(df[[interaction_item]] == interaction_y), .var]) |
| 44 |
# convert characters selected in interaction_y into the numeric order |
|
| 45 | 24x |
interaction_y <- which(sum_fit_level[[interaction_item]] == interaction_y) |
| 46 | 24x |
sum_fit_level <- sum_fit_level[interaction_y, ] |
| 47 |
# if interaction is called, reset the index |
|
| 48 | 24x |
ref_key <- seq(sum_fit[[arm]][unique(.ref_group[[arm]])]) |
| 49 | 24x |
ref_key <- utils::tail(ref_key, n = 1) |
| 50 | 24x |
ref_key <- (interaction_y - 1) * length(unique(.df_row[[arm]])) + ref_key |
| 51 |
} else {
|
|
| 52 | 97x |
y <- df[[.var]] |
| 53 |
# Get the index of the ref arm when interaction is not called |
|
| 54 | 97x |
ref_key <- seq(sum_fit[[arm]][unique(.ref_group[[arm]])]) |
| 55 | 97x |
ref_key <- utils::tail(ref_key, n = 1) |
| 56 |
} |
|
| 57 | ||
| 58 | 121x |
if (.in_ref_col) {
|
| 59 | 34x |
sum_contrasts_level <- list() |
| 60 | 34x |
sum_contrasts_level[["estimate"]] <- numeric() |
| 61 | 34x |
sum_contrasts_level[["lower.CL"]] <- numeric() |
| 62 | 34x |
sum_contrasts_level[["upper.CL"]] <- numeric() |
| 63 | 34x |
sum_contrasts_level[["p.value"]] <- numeric() |
| 64 |
} else {
|
|
| 65 | 87x |
if (!is.null(.ref_group)) {
|
| 66 |
# Estimate the differences between the marginal means. |
|
| 67 | 85x |
emmeans_contrasts <- emmeans::contrast( |
| 68 | 85x |
emmeans_fit, |
| 69 |
# Compare all arms versus the control arm. |
|
| 70 | 85x |
method = "trt.vs.ctrl", |
| 71 |
# Take the arm factor from .ref_group as the control arm. |
|
| 72 | 85x |
ref = ref_key, |
| 73 | 85x |
level = conf_level |
| 74 |
) |
|
| 75 | 85x |
sum_contrasts <- summary( |
| 76 | 85x |
emmeans_contrasts, |
| 77 |
# Derive confidence intervals, t-tests and p-values. |
|
| 78 | 85x |
infer = TRUE, |
| 79 |
# Do not adjust the p-values for multiplicity. |
|
| 80 | 85x |
adjust = "none" |
| 81 |
) |
|
| 82 | ||
| 83 | 85x |
contrast_lvls <- gsub( |
| 84 | 85x |
"^\\(|\\)$", "", gsub(paste0(" - \\(*", .ref_group[[arm]][1], ".*"), "", sum_contrasts$contrast)
|
| 85 |
) |
|
| 86 | 85x |
if (!is.null(interaction_item)) {
|
| 87 | 16x |
sum_contrasts_level <- sum_contrasts[ |
| 88 | 16x |
grepl(sum_level, contrast_lvls, fixed = TRUE), , |
| 89 | 16x |
drop = FALSE |
| 90 |
] |
|
| 91 |
} else {
|
|
| 92 | 69x |
sum_contrasts_level <- sum_contrasts[ |
| 93 | 69x |
(sum_level == contrast_lvls | paste0("(", sum_level, ")") == contrast_lvls), ,
|
| 94 | 69x |
drop = FALSE |
| 95 |
] |
|
| 96 |
} |
|
| 97 | 85x |
if (interaction_y != FALSE) {
|
| 98 | 16x |
sum_contrasts_level <- sum_contrasts_level[interaction_y, , drop = FALSE] |
| 99 |
} |
|
| 100 |
} else {
|
|
| 101 | 2x |
sum_contrasts_level <- list() |
| 102 | 2x |
sum_contrasts_level[["estimate"]] <- NA |
| 103 | 2x |
sum_contrasts_level[["lower.CL"]] <- NA |
| 104 | 2x |
sum_contrasts_level[["upper.CL"]] <- NA |
| 105 | 2x |
sum_contrasts_level[["p.value"]] <- NA |
| 106 |
} |
|
| 107 |
} |
|
| 108 | 121x |
ret <- list( |
| 109 | 121x |
sum_fit_level = sum_fit_level, |
| 110 | 121x |
sum_contrasts_level = sum_contrasts_level, |
| 111 | 121x |
y = y |
| 112 |
) |
|
| 113 | 121x |
ret |
| 114 |
} |
|
| 115 | ||
| 116 | ||
| 117 | ||
| 118 |
#' Internal utility to construct contrast weights across treatment levels, |
|
| 119 |
#' supporting equal or proportional weighting schemes. |
|
| 120 |
#' @noRd |
|
| 121 |
#' @param weight_mode (`character`)\cr |
|
| 122 |
#' Weighting strategy: `"equal"` or `"proportional"`. |
|
| 123 |
#' @param normalize (`logical`)\cr |
|
| 124 |
#' Whether to normalize weights to sum to 1. |
|
| 125 |
#' @param sum_fit (`data.frame`)\cr |
|
| 126 |
#' Summary output from [emmeans::summary.emmGrid()]. |
|
| 127 |
#' @param sum_levels (`character`)\cr |
|
| 128 |
#' Treatment levels included in the combination. |
|
| 129 |
#' @param arm (`character`)\cr |
|
| 130 |
#' Treatment arm variable name. |
|
| 131 |
#' @param data (`data.frame` | `NULL`)\cr |
|
| 132 |
#' Dataset used to derive proportional weights. |
|
| 133 |
#' |
|
| 134 |
#' @return A named numeric vector of weights for all treatment levels. |
|
| 135 |
#' |
|
| 136 |
#' @keywords internal |
|
| 137 | ||
| 138 |
make_contrast_weights <- function( |
|
| 139 |
sum_fit, |
|
| 140 |
sum_levels, |
|
| 141 |
arm, |
|
| 142 |
data, |
|
| 143 |
weight_mode = c("equal", "proportional"),
|
|
| 144 |
normalize = TRUE) {
|
|
| 145 | 34x |
weight_mode <- match.arg(weight_mode) |
| 146 | 34x |
trt_levels <- levels(sum_fit[[arm]]) |
| 147 |
# weights for contrast contains all trt_levels from sum_fit, not just the sum_levels |
|
| 148 |
# zero for all levels |
|
| 149 | 34x |
w_all <- setNames(numeric(length(trt_levels)), trt_levels) |
| 150 | ||
| 151 | 34x |
if (weight_mode == "equal") {
|
| 152 | 25x |
w <- rep(1, length(sum_levels)) |
| 153 | 25x |
names(w) <- sum_levels |
| 154 | 9x |
} else if (weight_mode == "proportional") {
|
| 155 | 9x |
w <- table(data[[arm]])[sum_levels] |
| 156 |
} |
|
| 157 | ||
| 158 | 34x |
if (normalize) {
|
| 159 | 34x |
w <- w / sum(w) |
| 160 |
} |
|
| 161 | ||
| 162 |
# update the weights for sum_levels with the above values |
|
| 163 | 34x |
w_all[names(w)] <- w[names(w)] |
| 164 | 34x |
w_all |
| 165 |
} |
|
| 166 | ||
| 167 | ||
| 168 | ||
| 169 |
#' Internal helper that derives adjusted means and contrasts for combined |
|
| 170 |
#' treatment levels using contrast-based weighting from an `emmeans` ANCOVA fit. |
|
| 171 |
#' @noRd |
|
| 172 |
#' @inheritParams tern::s_ancova |
|
| 173 |
#' @param weights_combo (`character`)\cr |
|
| 174 |
#' Weighting method for combined contrasts. |
|
| 175 |
#' |
|
| 176 |
#' @return A list with components: |
|
| 177 |
#' * `sum_fit_level`: adjusted mean estimates for the combination. |
|
| 178 |
#' * `sum_contrasts_level`: contrast estimates versus reference. |
|
| 179 |
#' * `y`: observed response values used for `n`. |
|
| 180 |
#' |
|
| 181 |
#' @keywords internal |
|
| 182 | ||
| 183 |
h_ancova_est_combined <- function( |
|
| 184 |
emmeans_fit, |
|
| 185 |
sum_level, |
|
| 186 |
interaction_y, |
|
| 187 |
interaction_item, |
|
| 188 |
df, |
|
| 189 |
.var, |
|
| 190 |
.df_row, |
|
| 191 |
.ref_group, |
|
| 192 |
arm, |
|
| 193 |
conf_level, |
|
| 194 |
.in_ref_col, |
|
| 195 |
weights_combo) {
|
|
| 196 | 36x |
if (.in_ref_col) {
|
| 197 | ! |
stop("not intended for usage in reference column")
|
| 198 |
} |
|
| 199 | 36x |
if (is.null(weights_combo)) {
|
| 200 | 1x |
stop("In a combined column specifications for weights_combo must be non-NULL.")
|
| 201 |
} else {
|
|
| 202 | 35x |
weights_combo <- match.arg(weights_combo, |
| 203 | 35x |
choices = c("equal", "proportional", "proportional_marginal"),
|
| 204 | 35x |
several.ok = FALSE |
| 205 |
) |
|
| 206 |
} |
|
| 207 | 34x |
sum_fit <- summary( |
| 208 | 34x |
emmeans_fit, |
| 209 | 34x |
level = conf_level |
| 210 |
) |
|
| 211 | 34x |
ref_name <- unique(.ref_group[[arm]]) |
| 212 | ||
| 213 |
# weights for contrasts, proportional/proportianal_marginal or equal |
|
| 214 |
# proportional_marginal is only relevant for an interaction term between arm and interaction_item |
|
| 215 |
# when no interaction proportional_marginal and proportional yield the same weights |
|
| 216 |
# when interaction : |
|
| 217 |
# weights with proportional will be within the given level of the interaction_item (ie interaction_y) |
|
| 218 |
# weights with proportional_marginal will across all levels of the interaction_item |
|
| 219 | 34x |
if (weights_combo == "proportional" && (interaction_y != FALSE)) {
|
| 220 | 2x |
data_weights <- df[!is.na(.var), ] |
| 221 | 2x |
data_weights <- data_weights[(data_weights[[interaction_item]] == interaction_y), ] |
| 222 | 32x |
} else if (weights_combo %in% c("proportional", "proportional_marginal")) {
|
| 223 | 7x |
data_weights <- df[!is.na(.var), ] |
| 224 | 7x |
weights_combo <- "proportional" |
| 225 |
} else {
|
|
| 226 | 25x |
data_weights <- NULL |
| 227 |
} |
|
| 228 | ||
| 229 | 34x |
w <- make_contrast_weights( |
| 230 | 34x |
weight_mode = weights_combo, |
| 231 | 34x |
normalize = TRUE, |
| 232 | 34x |
sum_fit = sum_fit, |
| 233 | 34x |
sum_levels = sum_level, |
| 234 | 34x |
arm = arm, |
| 235 | 34x |
data = data_weights |
| 236 |
) |
|
| 237 | 34x |
w2 <- w |
| 238 | 34x |
w2[ref_name] <- -1 |
| 239 | ||
| 240 | 34x |
if (interaction_y == FALSE) {
|
| 241 | 26x |
y <- df[[.var]] |
| 242 | 26x |
w_start <- as.data.frame(emmeans_fit) |
| 243 | 26x |
w_start[["weights1"]] <- w |
| 244 | 26x |
w_start[["weights2"]] <- w2 |
| 245 |
} else {
|
|
| 246 | 8x |
y <- unlist(df[(df[[interaction_item]] == interaction_y), .var]) |
| 247 | ||
| 248 | 8x |
w_start <- as.data.frame(emmeans_fit) |
| 249 | 8x |
w_start[["weights1"]] <- 0 |
| 250 | 8x |
w_start[["weights2"]] <- 0 |
| 251 | 8x |
w_start[["weights1"]][w_start[[interaction_item]] == interaction_y] <- w |
| 252 | 8x |
w_start[["weights2"]][w_start[[interaction_item]] == interaction_y] <- w2 |
| 253 |
} |
|
| 254 | ||
| 255 | 34x |
contr <- emmeans::contrast(emmeans_fit, |
| 256 | 34x |
list(setNames(list(w_start[["weights1"]]), "Estimate contrast"), |
| 257 | 34x |
setNames(list(w_start[["weights2"]]), "Estimate contrast vs ref")), |
| 258 |
# Derive confidence intervals, t-tests and p-values. |
|
| 259 | 34x |
infer = TRUE, |
| 260 |
# Do not adjust the p-values for multiplicity. |
|
| 261 | 34x |
adjust = "none") |
| 262 | ||
| 263 | 34x |
contr_df <- as.data.frame(contr) |
| 264 | ||
| 265 | 34x |
contr_df[["emmeans"]] <- contr_df[["estimate"]] |
| 266 | 34x |
sum_fit_level <- contr_df[contr_df$contrast == "Estimate contrast", , drop = FALSE] |
| 267 | 34x |
sum_contrasts_level <- contr_df[contr_df$contrast == "Estimate contrast vs ref", , drop = FALSE] |
| 268 | ||
| 269 | 34x |
if (length(ref_name) == 0) {
|
| 270 | ! |
sum_contrasts_level <- list() |
| 271 | ! |
sum_contrasts_level[["estimate"]] <- numeric() |
| 272 | ! |
sum_contrasts_level[["lower.CL"]] <- numeric() |
| 273 | ! |
sum_contrasts_level[["upper.CL"]] <- numeric() |
| 274 | ! |
sum_contrasts_level[["p.value"]] <- numeric() |
| 275 |
} |
|
| 276 | ||
| 277 | 34x |
ret <- list( |
| 278 | 34x |
sum_fit_level = sum_fit_level, |
| 279 | 34x |
sum_contrasts_level = sum_contrasts_level, |
| 280 | 34x |
y = y |
| 281 |
) |
|
| 282 | 34x |
ret |
| 283 |
} |
|
| 284 | ||
| 285 |
#' @title Junco Extended ANCOVA Function |
|
| 286 |
#' @name s_ancova_j |
|
| 287 |
#' @inheritParams tern::s_ancova |
|
| 288 |
#' @param weights_emmeans (`string`)\cr argument from [emmeans::emmeans()], `"counterfactual"` by default. |
|
| 289 |
#' @param method_combo (`string`)\cr Method for derivations in combined column. |
|
| 290 |
#' * `contrast` Derivations for the combined level are done through contrasts from the original model |
|
| 291 |
#' (using weights per `weights_combo` specifications). |
|
| 292 |
#' * `collapse` The ancova model for the combined group will be performed with group levels that contribute |
|
| 293 |
#' to the combination collapsed into a single combined level. |
|
| 294 |
#' |
|
| 295 |
#' For more information see the vignette `ANCOVA with Combined Treatment Groups`. |
|
| 296 |
#' @param weights_combo (`string`)\cr Weights for the contrasts of the combined levels. |
|
| 297 |
#' * `equal` 1/(number of levels from arm variable included in the combination) |
|
| 298 |
#' * `proportional`, `proportional_marginal` weight for each level included in |
|
| 299 |
#' the combination is proportional to number of observations in that level |
|
| 300 |
#'\cr The difference between `proportional` and `proportional_marginal` is only |
|
| 301 |
#'relevant when the model includes an interaction between arm and other factor variable (`interaction_item`). |
|
| 302 |
#'\cr `proportional_marginal` interprets proportional over all levels of |
|
| 303 |
#' `interaction_item`, ie, the same weights will be used for all levels of `interaction_item`. |
|
| 304 |
#'\cr For `proportional` the weights will be derived within |
|
| 305 |
#'the requested level (`interaction_y`) for `interaction_item`. |
|
| 306 |
#' @description Extension to tern:::s_ancova, 3 extra statistics are returned: |
|
| 307 |
#' * `lsmean_se`: Marginal mean and estimated SE in the group. |
|
| 308 |
#' * `lsmean_ci`: Marginal mean and associated confidence interval in the group. |
|
| 309 |
#' * `lsmean_diff_with_ci`: Difference in mean and associated confidence level in one combined statistic. |
|
| 310 |
#' In addition, the LS mean weights can be specified. |
|
| 311 |
#' In addition, also a NULL .ref_group can be specified, the lsmean_diff related estimates will be returned as NA. |
|
| 312 |
#' @export |
|
| 313 |
#' @return Returns a named list of 8 statistics (3 extra compared to `tern:::s_ancova()`). |
|
| 314 |
#' @family Inclusion of ANCOVA Functions |
|
| 315 |
#' @examples |
|
| 316 |
#' library(dplyr) |
|
| 317 |
#' library(tern) |
|
| 318 |
#' |
|
| 319 |
#' df <- iris |> filter(Species == "virginica") |
|
| 320 |
#' .df_row <- iris |
|
| 321 |
#' .var <- "Petal.Length" |
|
| 322 |
#' variables <- list(arm = "Species", covariates = "Sepal.Length * Sepal.Width") |
|
| 323 |
#' .ref_group <- iris |> filter(Species == "setosa") |
|
| 324 |
#' conf_level <- 0.95 |
|
| 325 |
#' s_ancova_j(df, .var, .df_row, variables, .ref_group, .in_ref_col = FALSE, conf_level) |
|
| 326 |
s_ancova_j <- function( |
|
| 327 |
df, |
|
| 328 |
.var, |
|
| 329 |
.df_row, |
|
| 330 |
variables, |
|
| 331 |
.ref_group, |
|
| 332 |
.in_ref_col, |
|
| 333 |
conf_level, |
|
| 334 |
interaction_y = FALSE, |
|
| 335 |
interaction_item = NULL, |
|
| 336 |
weights_emmeans = "counterfactual", |
|
| 337 |
method_combo = c("contrasts", "collapse"),
|
|
| 338 |
weights_combo = NULL) {
|
|
| 339 | 163x |
if ((!is.null(interaction_item) && interaction_y == FALSE) || |
| 340 | 163x |
(is.null(interaction_item) && interaction_y != FALSE)) {
|
| 341 | ! |
stop( |
| 342 | ! |
"If interaction is needed it is required to set interaction_item to variable for interaction", |
| 343 | ! |
"\n and interaction_y to a level of the interaction variable.", |
| 344 | ! |
"\n If interaction is not needed, set interaction_y to FALSE and interaction_item to NULL." |
| 345 |
) |
|
| 346 |
} |
|
| 347 | 163x |
arm <- variables$arm |
| 348 | 163x |
method_combo <- match.arg(method_combo) |
| 349 | ||
| 350 | 163x |
.df_row <- subset(.df_row, !is.na(.df_row[[.var]])) |
| 351 | 163x |
df <- subset(df, !is.na(df[[.var]])) |
| 352 | 163x |
.ref_group <- subset(.ref_group, !is.na(.ref_group[[.var]])) |
| 353 | ||
| 354 | 163x |
n_obs_trt_lvls <- length(unique(.df_row[[arm]])) |
| 355 | ||
| 356 | 163x |
arm <- variables$arm |
| 357 | 163x |
sum_level_start <- as.character(unique(df[[arm]])) |
| 358 | ||
| 359 | 163x |
incombo <- length(sum_level_start) > 1 |
| 360 | 163x |
if (incombo && method_combo == "collapse") {
|
| 361 |
# proceed with model on updated levels for treatment group |
|
| 362 | 1x |
orig_levels <- levels(.df_row[[arm]]) |
| 363 | 1x |
new_levels <- orig_levels |
| 364 | 1x |
new_levels[orig_levels %in% sum_level_start] <- "Combined" |
| 365 | ||
| 366 | 1x |
df[[arm]] <- factor(as.character(df[[arm]]), |
| 367 | 1x |
levels = orig_levels, |
| 368 | 1x |
labels = new_levels) |
| 369 | 1x |
.df_row[[arm]] <- factor(as.character(.df_row[[arm]]), |
| 370 | 1x |
levels = orig_levels, |
| 371 | 1x |
labels = new_levels) |
| 372 | 1x |
.ref_group[[arm]] <- factor(as.character(.ref_group[[arm]]), |
| 373 | 1x |
levels = orig_levels, |
| 374 | 1x |
labels = new_levels) |
| 375 |
} |
|
| 376 | ||
| 377 |
### sparse data problems with underlying ancova function |
|
| 378 | 163x |
if (NROW(.df_row) == 0) {
|
| 379 | 3x |
ret <- NULL |
| 380 | 160x |
} else if (NROW(df) == 0 || n_obs_trt_lvls < 2) {
|
| 381 |
## all stats are NA |
|
| 382 | 3x |
ret <- list( |
| 383 | 3x |
n_fit = with_label(0, "n"), |
| 384 | 3x |
lsmean = with_label(NA, "Adjusted Mean"), |
| 385 | 3x |
lsmean_se = with_label(rep(NA, 2), "Adjusted Mean (SE)"), |
| 386 | 3x |
lsmean_ci = with_label( |
| 387 | 3x |
rep(NA, 3), |
| 388 | 3x |
paste0("Adjusted Mean", " (", f_conf_level(conf_level), ")")
|
| 389 |
), |
|
| 390 | 3x |
lsmean_diff = with_label(NA, "Difference in Adjusted Means"), |
| 391 | 3x |
lsmean_diff_ci = with_label( |
| 392 | 3x |
rep(NA, 2), |
| 393 | 3x |
paste("Difference in Adjusted Means", f_conf_level(conf_level))
|
| 394 |
), |
|
| 395 | 3x |
lsmean_diff_with_ci = with_label( |
| 396 | 3x |
rep(NA, 3), |
| 397 | 3x |
paste0( |
| 398 | 3x |
"Difference in Adjusted Means", |
| 399 |
" (",
|
|
| 400 | 3x |
f_conf_level(conf_level), |
| 401 |
")" |
|
| 402 |
) |
|
| 403 |
), |
|
| 404 | 3x |
pval = with_label(NA, "p-value") |
| 405 |
) |
|
| 406 |
} else {
|
|
| 407 | 157x |
if (NROW(.ref_group) == 0) {
|
| 408 | 2x |
.ref_group <- NULL |
| 409 | 155x |
} else if (length(levels(.df_row[[arm]])) > n_obs_trt_lvls) {
|
| 410 |
# missing arm levels need to be removed from the factor, in order to have the correct estimates, |
|
| 411 |
# and avoid errors with further code |
|
| 412 | 2x |
.df_row[[arm]] <- droplevels(.df_row[[arm]]) |
| 413 | 2x |
df[[arm]] <- factor( |
| 414 | 2x |
as.character(df[[arm]]), |
| 415 | 2x |
levels = levels(.df_row[[arm]]) |
| 416 |
) |
|
| 417 | 2x |
.ref_group[[arm]] <- factor( |
| 418 | 2x |
as.character(.ref_group[[arm]]), |
| 419 | 2x |
levels = levels(.df_row[[arm]]) |
| 420 |
) |
|
| 421 |
} |
|
| 422 | ||
| 423 | 157x |
emmeans_fit <- tern::h_ancova( |
| 424 | 157x |
.var = .var, |
| 425 | 157x |
variables = variables, |
| 426 | 157x |
.df_row = .df_row, |
| 427 | 157x |
weights_emmeans = weights_emmeans, |
| 428 | 157x |
interaction_item = interaction_item |
| 429 |
) |
|
| 430 | 157x |
sum_fit <- summary( |
| 431 | 157x |
emmeans_fit, |
| 432 | 157x |
level = conf_level |
| 433 |
) |
|
| 434 | 157x |
sum_level <- as.character(unique(df[[arm]])) |
| 435 | ||
| 436 | 157x |
if (length(sum_level) == 1) {
|
| 437 | 121x |
est_single_fit <- h_ancova_est_single( |
| 438 | 121x |
emmeans_fit, |
| 439 | 121x |
sum_level, |
| 440 | 121x |
interaction_y, |
| 441 | 121x |
interaction_item, |
| 442 | 121x |
df, |
| 443 | 121x |
.var, |
| 444 | 121x |
.df_row, |
| 445 | 121x |
.ref_group, |
| 446 | 121x |
arm, |
| 447 | 121x |
conf_level, |
| 448 | 121x |
.in_ref_col |
| 449 |
) |
|
| 450 | ||
| 451 | 121x |
sum_fit_level <- est_single_fit[["sum_fit_level"]] |
| 452 | 121x |
sum_contrasts_level <- est_single_fit[["sum_contrasts_level"]] |
| 453 | 121x |
y <- est_single_fit[["y"]] |
| 454 |
} else {
|
|
| 455 | 36x |
est_combined_fit <- h_ancova_est_combined( |
| 456 | 36x |
emmeans_fit, |
| 457 | 36x |
sum_level, |
| 458 | 36x |
interaction_y, |
| 459 | 36x |
interaction_item, |
| 460 | 36x |
df, |
| 461 | 36x |
.var, |
| 462 | 36x |
.df_row, |
| 463 | 36x |
.ref_group, |
| 464 | 36x |
arm, |
| 465 | 36x |
conf_level, |
| 466 | 36x |
.in_ref_col, |
| 467 | 36x |
weights_combo |
| 468 |
) |
|
| 469 | ||
| 470 | 34x |
sum_fit_level <- est_combined_fit[["sum_fit_level"]] |
| 471 | 34x |
sum_contrasts_level <- est_combined_fit[["sum_contrasts_level"]] |
| 472 | 34x |
y <- est_combined_fit[["y"]] |
| 473 |
} |
|
| 474 | ||
| 475 | 155x |
ret <- list( |
| 476 | 155x |
n_fit = with_label(length(y[!is.na(y)]), "n"), |
| 477 | 155x |
lsmean = with_label(sum_fit_level$emmean, "Adjusted Mean"), |
| 478 | 155x |
lsmean_se = with_label( |
| 479 | 155x |
c(sum_fit_level$emmean, sum_fit_level$SE), |
| 480 | 155x |
"Adjusted Mean (SE)" |
| 481 |
), |
|
| 482 | 155x |
lsmean_ci = with_label( |
| 483 | 155x |
c( |
| 484 | 155x |
sum_fit_level$emmean, |
| 485 | 155x |
sum_fit_level$lower.CL, |
| 486 | 155x |
sum_fit_level$upper.CL |
| 487 |
), |
|
| 488 | 155x |
paste0("Adjusted Mean", " (", f_conf_level(conf_level), ")")
|
| 489 |
), |
|
| 490 | 155x |
lsmean_diff = with_label( |
| 491 | 155x |
sum_contrasts_level$estimate, |
| 492 | 155x |
"Difference in Adjusted Means" |
| 493 |
), |
|
| 494 | 155x |
lsmean_diff_ci = with_label( |
| 495 | 155x |
c(sum_contrasts_level$lower.CL, sum_contrasts_level$upper.CL), |
| 496 | 155x |
paste("Difference in Adjusted Means", f_conf_level(conf_level))
|
| 497 |
), |
|
| 498 | 155x |
lsmean_diff_with_ci = with_label( |
| 499 | 155x |
c( |
| 500 | 155x |
sum_contrasts_level$estimate, |
| 501 | 155x |
sum_contrasts_level$lower.CL, |
| 502 | 155x |
sum_contrasts_level$upper.CL |
| 503 |
), |
|
| 504 | 155x |
paste0( |
| 505 | 155x |
"Difference in Adjusted Means", |
| 506 |
" (",
|
|
| 507 | 155x |
f_conf_level(conf_level), |
| 508 |
")" |
|
| 509 |
) |
|
| 510 |
), |
|
| 511 | 155x |
pval = with_label(sum_contrasts_level$p.value, "p-value") |
| 512 |
) |
|
| 513 |
} |
|
| 514 | 161x |
return(ret) |
| 515 |
} |
|
| 516 | ||
| 517 |
#' @name s_summarize_ancova_j |
|
| 518 |
#' @title ANCOVA Summary Function |
|
| 519 |
#' @description Combination of [tern::s_summary], and ANCOVA based estimates for mean and diff between columns, |
|
| 520 |
#' based on ANCOVA function `s_ancova_j`. |
|
| 521 |
#' @inherit s_ancova_j |
|
| 522 |
#' @param ... Additional arguments passed to `s_ancova_j`. |
|
| 523 |
#' @details Combination of [tern::s_summary], and ANCOVA based estimates for mean and diff between columns, |
|
| 524 |
#' based on ANCOVA function `s_ancova_j` |
|
| 525 |
#' @return returns the statistics from `tern::s_summary(x)`, appended with a new statistics based upon ANCOVA |
|
| 526 |
#' @export |
|
| 527 |
#' @family Inclusion of ANCOVA Functions |
|
| 528 |
# @seealso s_ancova_j |
|
| 529 |
#' @examples |
|
| 530 |
#' library(dplyr) |
|
| 531 |
#' library(tern) |
|
| 532 |
#' |
|
| 533 |
#' df <- iris |> filter(Species == "virginica") |
|
| 534 |
#' .df_row <- iris |
|
| 535 |
#' .var <- "Petal.Length" |
|
| 536 |
#' variables <- list(arm = "Species", covariates = "Sepal.Length * Sepal.Width") |
|
| 537 |
#' .ref_group <- iris |> filter(Species == "setosa") |
|
| 538 |
#' conf_level <- 0.95 |
|
| 539 |
#' s_summarize_ancova_j( |
|
| 540 |
#' df, |
|
| 541 |
#' .var = .var, |
|
| 542 |
#' .df_row = .df_row, |
|
| 543 |
#' variables = variables, |
|
| 544 |
#' .ref_group = .ref_group, |
|
| 545 |
#' .in_ref_col = FALSE, |
|
| 546 |
#' conf_level = conf_level |
|
| 547 |
#' ) |
|
| 548 |
s_summarize_ancova_j <- function( |
|
| 549 |
df, |
|
| 550 |
.var, |
|
| 551 |
.df_row, |
|
| 552 |
.ref_group, |
|
| 553 |
.in_ref_col, |
|
| 554 |
...) {
|
|
| 555 | 160x |
x <- df[[.var]] |
| 556 | 160x |
y1 <- s_summary(x) |
| 557 | 160x |
y2 <- s_ancova_j( |
| 558 | 160x |
df = df, |
| 559 | 160x |
.var = .var, |
| 560 | 160x |
.ref_group = .ref_group, |
| 561 | 160x |
.in_ref_col = .in_ref_col, |
| 562 | 160x |
.df_row = .df_row, |
| 563 |
... |
|
| 564 |
) |
|
| 565 | 158x |
c(y1, y2) |
| 566 |
} |
|
| 567 | ||
| 568 |
#' @describeIn s_summarize_ancova_j Formatted analysis function which is used as `afun`. Note that the |
|
| 569 |
#' junco specific `ref_path` and `.spl_context` arguments are used for reference column information. |
|
| 570 |
#' |
|
| 571 |
#' @param ref_path (`character`)\cr path to the reference group. |
|
| 572 |
#' @param .spl_context (`environment`)\cr split context environment. |
|
| 573 |
#' @param ... Additional arguments passed to the statistics function. |
|
| 574 |
#' @param .stats (`character`)\cr statistics to calculate. |
|
| 575 |
#' @param .formats (`list`)\cr formats for the statistics. |
|
| 576 |
#' @param .labels (`list`)\cr labels for the statistics. |
|
| 577 |
#' @param .indent_mods (`list`)\cr indentation modifications for the statistics. |
|
| 578 |
#' |
|
| 579 |
#' @return |
|
| 580 |
#' * `a_summarize_ancova_j()` returns the corresponding list with formatted [rtables::CellValue()]. |
|
| 581 |
#' |
|
| 582 |
#' @examples |
|
| 583 |
#' |
|
| 584 |
#' basic_table() |> |
|
| 585 |
#' split_cols_by("Species") |>
|
|
| 586 |
#' add_colcounts() |> |
|
| 587 |
#' analyze( |
|
| 588 |
#' vars = "Petal.Length", |
|
| 589 |
#' afun = a_summarize_ancova_j, |
|
| 590 |
#' show_labels = "hidden", |
|
| 591 |
#' na_str = tern::default_na_str(), |
|
| 592 |
#' table_names = "unadj", |
|
| 593 |
#' var_labels = "Unadjusted comparison", |
|
| 594 |
#' extra_args = list( |
|
| 595 |
#' variables = list(arm = "Species", covariates = NULL), |
|
| 596 |
#' conf_level = 0.95, |
|
| 597 |
#' .labels = c(lsmean = "Mean", lsmean_diff = "Difference in Means"), |
|
| 598 |
#' ref_path = c("Species", "setosa")
|
|
| 599 |
#' ) |
|
| 600 |
#' ) |> |
|
| 601 |
#' analyze( |
|
| 602 |
#' vars = "Petal.Length", |
|
| 603 |
#' afun = a_summarize_ancova_j, |
|
| 604 |
#' show_labels = "hidden", |
|
| 605 |
#' na_str = tern::default_na_str(), |
|
| 606 |
#' table_names = "adj", |
|
| 607 |
#' var_labels = "Adjusted comparison (covariates: Sepal.Length and Sepal.Width)", |
|
| 608 |
#' extra_args = list( |
|
| 609 |
#' variables = list( |
|
| 610 |
#' arm = "Species", |
|
| 611 |
#' covariates = c("Sepal.Length", "Sepal.Width")
|
|
| 612 |
#' ), |
|
| 613 |
#' conf_level = 0.95, |
|
| 614 |
#' ref_path = c("Species", "setosa")
|
|
| 615 |
#' ) |
|
| 616 |
#' ) |> |
|
| 617 |
#' build_table(iris) |
|
| 618 |
#' |
|
| 619 |
#' @export |
|
| 620 |
#' @order 2 |
|
| 621 |
a_summarize_ancova_j <- function( |
|
| 622 |
df, |
|
| 623 |
.var, |
|
| 624 |
.df_row, |
|
| 625 |
ref_path, |
|
| 626 |
.spl_context, |
|
| 627 |
..., |
|
| 628 |
.stats = c( |
|
| 629 |
"n", |
|
| 630 |
"mean_sd", |
|
| 631 |
"median", |
|
| 632 |
"range", |
|
| 633 |
"quantiles", |
|
| 634 |
"lsmean_se", |
|
| 635 |
"lsmean_ci", |
|
| 636 |
"lsmean_diff_with_ci", |
|
| 637 |
"pval" |
|
| 638 |
), |
|
| 639 |
.formats = NULL, |
|
| 640 |
.labels = NULL, |
|
| 641 |
.indent_mods = NULL |
|
| 642 |
) {
|
|
| 643 |
# Check for additional parameters to the statistics function |
|
| 644 | 86x |
dots_extra_args <- list(...) |
| 645 | ||
| 646 |
# Only support default stats, not custom stats |
|
| 647 | 86x |
.stats <- .split_std_from_custom_stats(.stats)$default_stats |
| 648 | ||
| 649 |
# Obtain reference column information |
|
| 650 | 86x |
ref <- get_ref_info(ref_path, .spl_context) |
| 651 | ||
| 652 |
# Apply statistics function |
|
| 653 | 86x |
x_stats <- .apply_stat_functions( |
| 654 | 86x |
default_stat_fnc = s_summarize_ancova_j, |
| 655 | 86x |
custom_stat_fnc_list = NULL, |
| 656 | 86x |
args_list = c( |
| 657 | 86x |
df = list(df), |
| 658 | 86x |
.var = .var, |
| 659 | 86x |
.df_row = list(.df_row), |
| 660 | 86x |
.ref_group = list(ref$ref_group), |
| 661 | 86x |
.in_ref_col = ref$in_ref_col, |
| 662 | 86x |
dots_extra_args |
| 663 |
) |
|
| 664 |
) |
|
| 665 | ||
| 666 |
# Format according to specifications |
|
| 667 | 84x |
format_stats( |
| 668 | 84x |
x_stats, |
| 669 | 84x |
method_groups = "summarize_ancova_j", |
| 670 | 84x |
stats_in = .stats, |
| 671 | 84x |
formats_in = .formats, |
| 672 | 84x |
labels_in = .labels, |
| 673 | 84x |
indents_in = .indent_mods |
| 674 |
) |
|
| 675 |
} |
| 1 |
#' Tabulation of RBMI Results |
|
| 2 |
#' |
|
| 3 |
#' @description `r lifecycle::badge('stable')`
|
|
| 4 |
#' |
|
| 5 |
#' These functions can be used to produce tables from RBMI. |
|
| 6 |
#' |
|
| 7 |
#' @name tabulate_rbmi |
|
| 8 |
#' |
|
| 9 |
#' @note These functions have been forked from `tern.rbmi`. Additional features are: |
|
| 10 |
#' |
|
| 11 |
#' * Additional `ref_path` argument. |
|
| 12 |
#' * Extraction of variance statistics in the `tidy()` method. |
|
| 13 |
#' * Adapted to `rbmi` forked functions update with more than two treatment groups. |
|
| 14 |
NULL |
|
| 15 | ||
| 16 |
#' @describeIn tabulate_rbmi Helper function to produce data frame with results |
|
| 17 |
#' of pool for a single visit. |
|
| 18 |
#' |
|
| 19 |
#' @param x (`list`)\cr is a list of pooled object from `rbmi` analysis results. |
|
| 20 |
#' This list includes analysis results, confidence level, hypothesis testing type. |
|
| 21 |
#' @param visit_name (`string`)\cr single visit level. |
|
| 22 |
#' @param group_names (`character`)\cr group levels. |
|
| 23 |
#' @return |
|
| 24 |
#' * `h_tidy_pool()` returns a `data.frame` with results of pooled analysis for a single visit. |
|
| 25 |
#' |
|
| 26 |
#' @export |
|
| 27 |
h_tidy_pool <- function(x, visit_name, group_names) {
|
|
| 28 | 13x |
checkmate::assert_list(x) |
| 29 | 13x |
checkmate::assert_string(visit_name) |
| 30 | 13x |
checkmate::assert_character(group_names) |
| 31 | ||
| 32 | 13x |
ref_name <- paste0("lsm_", group_names[1], "_", visit_name)
|
| 33 | 13x |
ref <- x[[ref_name]] |
| 34 | ||
| 35 | 13x |
list2df <- \(l) with(l, data.frame(est, ci_l = ci[1], ci_u = ci[2], se, pvalue, df)) |
| 36 | ||
| 37 | 13x |
var_name <- paste0("var_", visit_name)
|
| 38 | 13x |
var <- if (var_name %in% names(x)) {
|
| 39 |
# This is the case for ANCOVA. |
|
| 40 | 11x |
list2df(x[[var_name]]) |
| 41 |
} else {
|
|
| 42 |
# This is the case for MMRM. |
|
| 43 | 2x |
var_names <- paste0("var_", group_names[-1], "_", visit_name)
|
| 44 | 2x |
checkmate::assert_subset(var_names, names(x)) |
| 45 | 2x |
var_dfs <- lapply(x[var_names], list2df) |
| 46 | 2x |
var <- do.call(rbind, var_dfs) |
| 47 |
} |
|
| 48 | ||
| 49 | 13x |
contr_names <- paste0("trt_", group_names[-1], "_", visit_name)
|
| 50 | 13x |
contr_dfs <- lapply(x[contr_names], list2df) |
| 51 | 13x |
contr <- do.call(rbind, contr_dfs) |
| 52 | ||
| 53 | 13x |
alt_names <- paste0("lsm_", group_names[-1], "_", visit_name)
|
| 54 | 13x |
alt_dfs <- lapply(x[alt_names], list2df) |
| 55 | 13x |
alt <- do.call(rbind, alt_dfs) |
| 56 | ||
| 57 | 13x |
df_ref <- data.frame( |
| 58 | 13x |
visit = visit_name, |
| 59 | 13x |
group = group_names[1], |
| 60 | 13x |
est = ref$est, |
| 61 | 13x |
se_est = ref$se, |
| 62 | 13x |
lower_cl_est = ref$ci[1], |
| 63 | 13x |
upper_cl_est = ref$ci[2], |
| 64 | 13x |
est_contr = NA_real_, |
| 65 | 13x |
se_contr = NA_real_, |
| 66 | 13x |
lower_cl_contr = NA_real_, |
| 67 | 13x |
upper_cl_contr = NA_real_, |
| 68 | 13x |
p_value = NA_real_, |
| 69 | 13x |
relative_reduc = NA_real_, |
| 70 | 13x |
mse = NA_real_, |
| 71 | 13x |
df = NA_real_, |
| 72 | 13x |
stringsAsFactors = FALSE |
| 73 |
) |
|
| 74 | 13x |
df_alt <- data.frame( |
| 75 | 13x |
visit = visit_name, |
| 76 | 13x |
group = group_names[-1], |
| 77 | 13x |
est = alt$est, |
| 78 | 13x |
se_est = alt$se, |
| 79 | 13x |
lower_cl_est = alt$ci_l, |
| 80 | 13x |
upper_cl_est = alt$ci_u, |
| 81 | 13x |
est_contr = contr$est, |
| 82 | 13x |
se_contr = contr$se, |
| 83 | 13x |
lower_cl_contr = contr$ci_l, |
| 84 | 13x |
upper_cl_contr = contr$ci_u, |
| 85 | 13x |
p_value = contr$pvalue, |
| 86 | 13x |
relative_reduc = contr$est / df_ref$est, |
| 87 | 13x |
mse = var$est, |
| 88 | 13x |
df = var$df, |
| 89 | 13x |
stringsAsFactors = FALSE |
| 90 |
) |
|
| 91 | 13x |
rbind(df_ref, df_alt) |
| 92 |
} |
|
| 93 | ||
| 94 |
#' Helper method (for [`broom::tidy()`]) to prepare a data frame from an |
|
| 95 |
#' `pool` `rbmi` object containing the LS means and contrasts and multiple visits |
|
| 96 |
#' |
|
| 97 |
#' @method tidy pool |
|
| 98 |
#' @param x (`pool`) is a list of pooled object from `rbmi` analysis results. This list includes |
|
| 99 |
#' analysis results, confidence level, hypothesis testing type. |
|
| 100 |
#' @param visits (`character`)\cr all visit levels. Otherwise too hard to guess this. |
|
| 101 |
#' @param ... Additional arguments. Not used. Needed to match generic signature only. |
|
| 102 |
#' @importFrom generics tidy |
|
| 103 |
#' @export |
|
| 104 |
#' @keywords internal |
|
| 105 |
#' @return A `data.frame`. |
|
| 106 |
tidy.pool <- function(x, visits, ...) {
|
|
| 107 | 6x |
ls_raw <- x$pars |
| 108 | ||
| 109 | 6x |
has_lsm <- grepl("^lsm_", names(ls_raw))
|
| 110 | 6x |
has_first_visit <- grepl(paste0("_", visits[1]), names(ls_raw), fixed = TRUE)
|
| 111 | 6x |
is_lsm_first_visit <- has_lsm & has_first_visit |
| 112 | ||
| 113 | 6x |
group_names <- names(ls_raw)[is_lsm_first_visit] |
| 114 | 6x |
group_names <- gsub(pattern = "^lsm_", replacement = "", x = group_names) |
| 115 | 6x |
group_names <- gsub(pattern = paste0("_", visits[1]), replacement = "", x = group_names, fixed = TRUE)
|
| 116 | ||
| 117 | 6x |
spl <- rep(visits, each = length(ls_raw) / length(visits)) |
| 118 | ||
| 119 | 6x |
ls_split <- split(ls_raw, spl) |
| 120 | ||
| 121 | 6x |
ls_df <- mapply( |
| 122 | 6x |
FUN = h_tidy_pool, |
| 123 | 6x |
x = ls_split, |
| 124 | 6x |
visit_name = visits, |
| 125 | 6x |
MoreArgs = list(group_names = group_names), |
| 126 | 6x |
SIMPLIFY = FALSE |
| 127 |
) |
|
| 128 | ||
| 129 | 6x |
result <- do.call(rbind, unname(ls_df)) |
| 130 | ||
| 131 | 6x |
result$visit <- factor(result$visit, levels = visits) |
| 132 | 6x |
result$group <- factor(result$group, levels = group_names) |
| 133 | 6x |
result$conf_level <- x$conf.level |
| 134 | ||
| 135 | 6x |
result |
| 136 |
} |
|
| 137 | ||
| 138 |
#' @describeIn tabulate_rbmi Statistics function which is extracting estimates |
|
| 139 |
#' from a tidied RBMI results data frame. |
|
| 140 |
#' |
|
| 141 |
#' @param df (`data.frame`)\cr input with LS means results. |
|
| 142 |
#' @param .in_ref_col (`flag`)\cr whether reference column is specified. |
|
| 143 |
#' @param show_relative (`string`)\cr 'reduction' if (`control - treatment`, default) |
|
| 144 |
#' or 'increase' (`treatment - control`) of relative change from baseline? |
|
| 145 |
#' @return |
|
| 146 |
#' * `s_rbmi_lsmeans()` returns a list of statistics extracted from a tidied LS means data frame. |
|
| 147 |
#' @export |
|
| 148 |
s_rbmi_lsmeans <- function(df, .in_ref_col, show_relative = c("reduction", "increase")) {
|
|
| 149 | 7x |
checkmate::assert_flag(.in_ref_col) |
| 150 | ||
| 151 | 7x |
show_relative <- match.arg(show_relative) |
| 152 | 7x |
if_not_ref <- function(x) if (.in_ref_col) character() else x |
| 153 | 7x |
list( |
| 154 | 7x |
adj_mean_se = c(df$est, df$se_est), |
| 155 | 7x |
adj_mean_ci = with_label(c(df$lower_cl_est, df$upper_cl_est), f_conf_level(df$conf_level)), |
| 156 | 7x |
diff_mean_se = if_not_ref(c(df$est_contr, df$se_contr)), |
| 157 | 7x |
diff_mean_ci = with_label( |
| 158 | 7x |
if_not_ref(c(df$lower_cl_contr, df$upper_cl_contr)), |
| 159 | 7x |
f_conf_level(df$conf_level) |
| 160 |
), |
|
| 161 | 7x |
change = switch(show_relative, |
| 162 | 7x |
reduction = with_label(if_not_ref(df$relative_reduc), "Relative Reduction (%)"), |
| 163 | 7x |
increase = with_label(if_not_ref(-df$relative_reduc), "Relative Increase (%)") |
| 164 |
), |
|
| 165 | 7x |
p_value = if_not_ref(df$p_value), |
| 166 | 7x |
additional_title_row = NULL |
| 167 |
) |
|
| 168 |
} |
|
| 169 | ||
| 170 |
#' @describeIn tabulate_rbmi Formatted Analysis function which is used as `afun`. |
|
| 171 |
#' |
|
| 172 |
#' @inheritParams proposal_argument_convention |
|
| 173 |
#' @export |
|
| 174 |
a_rbmi_lsmeans <- function( |
|
| 175 |
df, |
|
| 176 |
ref_path, |
|
| 177 |
.spl_context, |
|
| 178 |
..., |
|
| 179 |
.stats = NULL, |
|
| 180 |
.formats = NULL, |
|
| 181 |
.labels = NULL, |
|
| 182 |
.indent_mods = NULL |
|
| 183 |
) {
|
|
| 184 |
# Check for additional parameters to the statistics function |
|
| 185 | 4x |
dots_extra_args <- list(...) |
| 186 | ||
| 187 |
# Only support default stats, not custom stats |
|
| 188 | 4x |
.stats <- .split_std_from_custom_stats(.stats)$default_stats |
| 189 | ||
| 190 |
# Obtain reference column information |
|
| 191 | 4x |
ref <- get_ref_info(ref_path, .spl_context) |
| 192 | ||
| 193 |
# Apply statistics function |
|
| 194 | 4x |
x_stats <- .apply_stat_functions( |
| 195 | 4x |
default_stat_fnc = s_rbmi_lsmeans, |
| 196 | 4x |
custom_stat_fnc_list = NULL, |
| 197 | 4x |
args_list = c(df = list(df), .in_ref_col = ref$in_ref_col, dots_extra_args) |
| 198 |
) |
|
| 199 | ||
| 200 |
# Format according to specifications |
|
| 201 | 4x |
format_stats( |
| 202 | 4x |
x_stats, |
| 203 | 4x |
method_groups = "tabulate_rbmi", |
| 204 | 4x |
stats_in = .stats, |
| 205 | 4x |
formats_in = .formats, |
| 206 | 4x |
labels_in = .labels, |
| 207 | 4x |
indents_in = .indent_mods |
| 208 |
) |
|
| 209 |
} |
| 1 |
#' Extract Estimates from Multivariate Cox Regression Model Fit Object |
|
| 2 |
#' |
|
| 3 |
#' @param x (`coxreg.multivar`)\cr from [tern::fit_coxreg_multivar()]. |
|
| 4 |
#' @export |
|
| 5 |
#' @return A data frame containing Cox regression results with columns for term, |
|
| 6 |
#' coef_se (coefficient and standard error), p.value, hr (hazard ratio), |
|
| 7 |
#' hr_ci (confidence interval for hazard ratio), and labels (formatted term labels). |
|
| 8 |
#' @keywords internal |
|
| 9 |
#' |
|
| 10 |
#' @examples |
|
| 11 |
#' anl <- tern::tern_ex_adtte |> |
|
| 12 |
#' dplyr::mutate(EVENT = 1 - CNSR) |
|
| 13 |
#' |
|
| 14 |
#' variables <- list( |
|
| 15 |
#' time = "AVAL", |
|
| 16 |
#' event = "EVENT", |
|
| 17 |
#' arm = "ARM", |
|
| 18 |
#' covariates = c("SEX", "AGE")
|
|
| 19 |
#' ) |
|
| 20 |
#' |
|
| 21 |
#' control <- tern::control_coxreg( |
|
| 22 |
#' conf_level = 0.9, |
|
| 23 |
#' ties = "efron" |
|
| 24 |
#' ) |
|
| 25 |
#' |
|
| 26 |
#' fit <- tern::fit_coxreg_multivar( |
|
| 27 |
#' data = anl, |
|
| 28 |
#' variables = variables, |
|
| 29 |
#' control = control |
|
| 30 |
#' ) |
|
| 31 |
#' |
|
| 32 |
#' h_extract_coxreg_multivar(fit) |
|
| 33 |
h_extract_coxreg_multivar <- function(x) {
|
|
| 34 | 22x |
checkmate::assert_class(x, "coxreg.multivar") |
| 35 | 22x |
vars <- c(x$vars$arm, x$vars$covariates) |
| 36 | ||
| 37 |
# Extract the table. |
|
| 38 | 22x |
tab <- broom::tidy(x$mod, exponentiate = FALSE, conf.int = TRUE, conf.level = x$control$conf_level) |
| 39 | 22x |
tab$hr <- exp(tab$estimate) |
| 40 | 22x |
tab$hr_ci <- Map(lcl = exp(tab$conf.low), ucl = exp(tab$conf.high), f = function(lcl, ucl) c(lcl, ucl)) |
| 41 | 22x |
tab$coef_se <- Map(coef = tab$estimate, se = tab$std.error, f = function(coef, se) c(coef, se)) |
| 42 | 22x |
colnms <- c("term", "coef_se", "p.value", "hr", "hr_ci")
|
| 43 | 22x |
tab <- tab[, colnms, drop = FALSE] |
| 44 | ||
| 45 |
# Format nicely the term labels. |
|
| 46 | 22x |
var_to_term <- sapply(vars, function(v) which(startsWith(tab$term, v)), USE.NAMES = TRUE, simplify = FALSE) |
| 47 | 22x |
tab$labels <- unlist(lapply(vars, function(v) {
|
| 48 | 67x |
inds <- var_to_term[[v]] |
| 49 | 67x |
strip_term <- gsub(paste0("^", v), "", tab$term[inds])
|
| 50 | 67x |
this_df <- x$dat[v] |
| 51 | 67x |
if (is.character(this_df[[1]])) {
|
| 52 | 1x |
this_df[[1]] <- as.factor(this_df[[1]]) |
| 53 |
} |
|
| 54 | 67x |
if (is.factor(this_df[[1]])) {
|
| 55 | 45x |
ref_level <- levels(this_df[[1]])[1] |
| 56 | 45x |
var_name <- if (v == x$vars$arm) "Treatment" else labels_or_names(this_df) |
| 57 | 45x |
paste0(var_name, " (", strip_term, " vs. ", ref_level, ")")
|
| 58 |
} else {
|
|
| 59 | 22x |
labels_or_names(this_df) |
| 60 |
} |
|
| 61 |
})) |
|
| 62 | 22x |
tab |
| 63 |
} |
|
| 64 | ||
| 65 |
#' First Level Column Split Function for TEFOS03 (mmy) Table Layout |
|
| 66 |
#' @seealso [rtables::make_split_fun()] for details. |
|
| 67 |
#' @keywords internal |
|
| 68 |
tefos03_first_post_fun <- function(ret, spl, fulldf, .spl_context) {
|
|
| 69 | 5x |
all_expr <- expression(TRUE) |
| 70 | 5x |
short_split_result(model_fit = "Model Fit", hazard_ratio = "Hazard Ratio", fulldf = fulldf) |
| 71 |
} |
|
| 72 |
tefos03_first_split_fun <- make_split_fun(post = list(tefos03_first_post_fun)) |
|
| 73 | ||
| 74 |
#' Second Level Column Split Function Factory for TEFOS03 (mmy) Table Layout |
|
| 75 |
#' |
|
| 76 |
#' @inheritParams proposal_argument_convention |
|
| 77 |
#' |
|
| 78 |
#' @return Split function to use in the TEFOS03 (mmy) and related table layouts. |
|
| 79 |
#' |
|
| 80 |
#' @seealso [tefos03_first_post_fun()] for the first level split. |
|
| 81 |
#' @keywords internal |
|
| 82 |
tefos03_second_split_fun_fct <- function(conf_level) {
|
|
| 83 | 4x |
post_fun <- function(ret, spl, fulldf, .spl_context) {
|
| 84 | 8x |
all_expr <- expression(TRUE) |
| 85 | 8x |
colset <- .spl_context[nrow(.spl_context), "value"][[1]] |
| 86 | 8x |
if (colset == "model_fit") {
|
| 87 | 4x |
short_split_result(coef_se = "Coeff. (SE)", pval = "p-value", fulldf = fulldf) |
| 88 |
} else {
|
|
| 89 | 4x |
short_split_result(hr_est = "Estimate", hr_ci = f_conf_level(conf_level), fulldf = fulldf) |
| 90 |
} |
|
| 91 |
} |
|
| 92 | 4x |
make_split_fun(post = list(post_fun)) |
| 93 |
} |
|
| 94 | ||
| 95 |
#' @importFrom tern fit_coxreg_multivar |
|
| 96 |
#' @keywords internal |
|
| 97 |
memoised_fit_coxreg_multivar <- memoise::memoise(fit_coxreg_multivar) |
|
| 98 | ||
| 99 |
#' Analysis Function for TEFOS03 and Related Table Layouts |
|
| 100 |
#' |
|
| 101 |
#' @inheritParams proposal_argument_convention |
|
| 102 |
#' @param control (`list`)\cr from [tern::control_coxreg()]. |
|
| 103 |
#' @param formats (`list`)\cr including `coef_se`, `hr_est`, `hr_ci` and `pval` formats. |
|
| 104 |
#' @param variables (`list`)\cr see [tern::fit_coxreg_multivar()] for required variable |
|
| 105 |
#' specifications. |
|
| 106 |
#' |
|
| 107 |
#' @keywords internal |
|
| 108 |
tefos03_afun <- function(df, .var, .spl_context, variables, control, formats) {
|
|
| 109 | 21x |
this_col_split <- .spl_context[nrow(.spl_context), "cur_col_split_val"][[1]] |
| 110 | 21x |
model_fit <- memoised_fit_coxreg_multivar(data = df, variables = variables, control = control) |
| 111 | 21x |
tab <- h_extract_coxreg_multivar(model_fit) |
| 112 | 21x |
if (this_col_split[1] == "model_fit") {
|
| 113 | 11x |
if (this_col_split[2] == "coef_se") {
|
| 114 | 5x |
in_rows(.list = tab$coef_se, .formats = formats$coef_se, .labels = tab$labels) |
| 115 |
} else {
|
|
| 116 | 6x |
in_rows(.list = tab$p.value, .formats = formats$pval, .labels = tab$labels) |
| 117 |
} |
|
| 118 |
} else {
|
|
| 119 | 10x |
if (this_col_split[2] == "hr_est") {
|
| 120 | 5x |
in_rows(.list = tab$hr, .formats = formats$hr_est, .labels = tab$labels) |
| 121 |
} else {
|
|
| 122 | 5x |
in_rows(.list = tab$hr_ci, .formats = formats$hr_ci, .labels = tab$labels) |
| 123 |
} |
|
| 124 |
} |
|
| 125 |
} |
|
| 126 | ||
| 127 |
#' Layout Generating Function for TEFOS03 and Related Cox Regression Layouts |
|
| 128 |
#' |
|
| 129 |
#' @inheritParams proposal_argument_convention |
|
| 130 |
#' @inheritParams tefos03_afun |
|
| 131 |
#' @param var (`string`)\cr any variable from the data, because this is not used. |
|
| 132 |
#' |
|
| 133 |
#' @return `lyt` modified to add the desired cox regression table section. |
|
| 134 |
#' @export |
|
| 135 |
#' @examples |
|
| 136 |
#' anl <- tern::tern_ex_adtte |> |
|
| 137 |
#' dplyr::mutate(EVENT = 1 - CNSR) |
|
| 138 |
#' |
|
| 139 |
#' variables <- list( |
|
| 140 |
#' time = "AVAL", |
|
| 141 |
#' event = "EVENT", |
|
| 142 |
#' arm = "ARM", |
|
| 143 |
#' covariates = c("SEX", "AGE")
|
|
| 144 |
#' ) |
|
| 145 |
#' |
|
| 146 |
#' basic_table() |> |
|
| 147 |
#' summarize_coxreg_multivar( |
|
| 148 |
#' var = "STUDYID", |
|
| 149 |
#' variables = variables |
|
| 150 |
#' ) |> |
|
| 151 |
#' build_table(df = anl) |
|
| 152 |
summarize_coxreg_multivar <- function( |
|
| 153 |
lyt, |
|
| 154 |
var, |
|
| 155 |
variables, |
|
| 156 |
control = control_coxreg(), |
|
| 157 |
formats = list( |
|
| 158 |
coef_se = jjcsformat_xx("xx.xx (xx.xx)"),
|
|
| 159 |
hr_est = jjcsformat_xx("xx.xx"),
|
|
| 160 |
hr_ci = jjcsformat_xx("(xx.xx, xx.xx)"),
|
|
| 161 |
pval = jjcsformat_pval_fct(0) |
|
| 162 |
) |
|
| 163 |
) {
|
|
| 164 | 3x |
second_split_fun <- tefos03_second_split_fun_fct(conf_level = control$conf_level) |
| 165 | 3x |
lyt |> |
| 166 | 3x |
split_cols_by(var = var, split_fun = tefos03_first_split_fun) |> |
| 167 | 3x |
split_cols_by(var = var, split_fun = second_split_fun) |> |
| 168 | 3x |
analyze( |
| 169 | 3x |
vars = var, |
| 170 | 3x |
var_labels = "Model Parameter", |
| 171 | 3x |
show_labels = "visible", |
| 172 | 3x |
afun = tefos03_afun, |
| 173 | 3x |
extra_args = list(variables = variables, control = control, formats = formats) |
| 174 |
) |
|
| 175 |
} |
| 1 |
#' Split Function for Compliance Columns |
|
| 2 |
#' |
|
| 3 |
#' Here we just split into 3 columns for expected, received and missing visits. |
|
| 4 |
#' |
|
| 5 |
#' @inheritParams proposal_argument_convention |
|
| 6 |
#' @param ret (`list`)\cr result from previous split function steps. |
|
| 7 |
#' @param spl (`split`)\cr split object. |
|
| 8 |
#' @param fulldf (`data.frame`)\cr full data frame. |
|
| 9 |
#' @param vals (`character`)\cr values to use for the split. |
|
| 10 |
#' @param trim (`logical`)\cr whether to trim the values. |
|
| 11 |
#' |
|
| 12 |
#' @seealso [rtables::make_split_fun()] describing the requirements for this kind of |
|
| 13 |
#' post-processing function. |
|
| 14 |
cmp_post_fun <- function(ret, spl, fulldf, .spl_context) {
|
|
| 15 | 1x |
short_split_result( |
| 16 | 1x |
expected = "Expected, N", |
| 17 | 1x |
received = "Received, n (%)", |
| 18 | 1x |
missing = "Missing, n (%)", |
| 19 | 1x |
fulldf = fulldf |
| 20 |
) |
|
| 21 |
} |
|
| 22 |
#' @rdname cmp_post_fun |
|
| 23 |
#' @return a split function for use with [rtables::split_rows_by] |
|
| 24 |
#' when creating proportion-based tables with compliance columns. |
|
| 25 |
#' @export |
|
| 26 |
cmp_split_fun <- make_split_fun(post = list(cmp_post_fun)) |
|
| 27 | ||
| 28 |
#' @title Summary Analysis Function for Compliance Columns |
|
| 29 |
#' @description A simple statistics function which prepares the numbers with percentages |
|
| 30 |
#' in the required format, for use in a split content row. The denominator here is |
|
| 31 |
#' from the expected visits column. |
|
| 32 |
#' @inheritParams proposal_argument_convention |
|
| 33 |
#' @param variables (`list`)\cr with variable names of logical columns for |
|
| 34 |
#' `expected`, `received` and `missing` visits. |
|
| 35 |
#' @param formats (`list`)\cr with the `count_percent` format to use for the received |
|
| 36 |
#' and missing visits columns. |
|
| 37 |
#' @return The [rtables::in_rows()] result with the counts and proportion statistics. |
|
| 38 |
#' @seealso [cmp_post_fun()] for the corresponding split function. |
|
| 39 |
#' @export |
|
| 40 |
cmp_cfun <- function(df, labelstr, .spl_context, variables, formats) {
|
|
| 41 | 3x |
this_col_split <- .spl_context[nrow(.spl_context), "cur_col_split_val"][[1]] |
| 42 | 3x |
this_afun_col <- this_col_split[2] |
| 43 | ||
| 44 | 3x |
exp <- df[[variables$expected]] |
| 45 | 3x |
checkmate::assert_logical(exp) |
| 46 | 3x |
n_exp <- sum(exp) |
| 47 | 3x |
rec <- df[[variables$received]] |
| 48 | 3x |
mis <- df[[variables$missing]] |
| 49 | ||
| 50 | 3x |
if (this_afun_col == "expected") {
|
| 51 | 1x |
in_rows(n_exp, .labels = labelstr, .formats = "xx.") |
| 52 | 2x |
} else if (this_afun_col == "received") {
|
| 53 | 1x |
checkmate::assert_logical(rec) |
| 54 | 1x |
checkmate::assert_true(all(exp[rec])) |
| 55 | ||
| 56 |
# Check if count_percent format is available |
|
| 57 | 1x |
if (!is.null(formats) && !("count_percent" %in% names(formats))) {
|
| 58 | ! |
stop("'count_percent' format must be provided in the formats argument")
|
| 59 |
} |
|
| 60 | ||
| 61 | 1x |
in_rows(sum(rec) * c(1, 1 / n_exp), .formats = formats$count_percent, .labels = labelstr) |
| 62 | 1x |
} else if (this_afun_col == "missing") {
|
| 63 | 1x |
checkmate::assert_logical(mis) |
| 64 | 1x |
checkmate::assert_true(all(exp[mis])) |
| 65 | 1x |
checkmate::assert_true(!any(rec[mis])) |
| 66 | ||
| 67 |
# Check if count_percent format is available |
|
| 68 | 1x |
if (!is.null(formats) && !("count_percent" %in% names(formats))) {
|
| 69 | ! |
stop("'count_percent' format must be provided in the formats argument")
|
| 70 |
} |
|
| 71 | ||
| 72 | 1x |
in_rows(sum(mis) * c(1, 1 / n_exp), .formats = formats$count_percent, .labels = labelstr) |
| 73 |
} |
|
| 74 |
} |
| 1 |
make_dps_regex <- function(keyword, type = c("[", "{")) {
|
|
| 2 | 1092x |
mustart <- match.arg(type) |
| 3 | 1092x |
muend <- ifelse(mustart == "[", "]", "}") |
| 4 | ||
| 5 | 1092x |
paste0("[~][", mustart, "]", keyword, " ([^", muend, "]+)[", muend, "]")
|
| 6 |
} |
|
| 7 | ||
| 8 |
handle_one_markup <- function(strs, keyword, rtfstart, rtfend) {
|
|
| 9 | 546x |
start <- ifelse(nzchar(rtfstart), paste0("\\", rtfstart, " "), "")
|
| 10 | 546x |
end <- ifelse(nzchar(rtfstart), paste0("\\", rtfend, " "), "")
|
| 11 | 546x |
out <- gsub(make_dps_regex(keyword), paste0(start, "\\1", end), strs) |
| 12 | 546x |
out <- gsub(make_dps_regex(keyword, "{"), paste0(start, "\\1", end), out)
|
| 13 | 546x |
out |
| 14 |
} |
|
| 15 | ||
| 16 | ||
| 17 |
dps_markup_df <- tibble::tribble( |
|
| 18 |
~keyword, |
|
| 19 |
~rtfstart, |
|
| 20 |
~rtfend, |
|
| 21 |
"super", |
|
| 22 |
"\\super", |
|
| 23 |
"\\nosupersub", |
|
| 24 |
"sub", |
|
| 25 |
"\\sub", |
|
| 26 |
"\\nosupersub", |
|
| 27 |
"optional", |
|
| 28 |
"", |
|
| 29 |
"" |
|
| 30 |
) |
|
| 31 | ||
| 32 | ||
| 33 |
#' Default String Mapping for Special Characters |
|
| 34 |
#' |
|
| 35 |
#' A tibble that maps special characters to their UTF-8 equivalents for use in RTF output. |
|
| 36 |
#' Currently it maps ">=" and "<=" to the Unicode characters. |
|
| 37 |
#' |
|
| 38 |
#' @return A tibble with columns 'pattern' and 'value', where 'pattern' contains |
|
| 39 |
#' the string to be replaced and 'value' contains the replacement. |
|
| 40 |
#' |
|
| 41 |
#' @export |
|
| 42 |
#' @keywords internal |
|
| 43 |
default_str_map <- tibble::tribble( |
|
| 44 |
~pattern, |
|
| 45 |
~value, |
|
| 46 |
">=", |
|
| 47 |
intToUtf8(strtoi(2265, base = 16)), |
|
| 48 |
"<=", |
|
| 49 |
intToUtf8(strtoi(2264, base = 16)) |
|
| 50 |
) |
|
| 51 | ||
| 52 |
convert_dps_markup <- function(strs, markup_df = dps_markup_df) {
|
|
| 53 | 182x |
for (i in seq_len(nrow(markup_df))) {
|
| 54 | 546x |
strs <- handle_one_markup(strs, markup_df$keyword[i], markup_df$rtfstart[i], markup_df$rtfend[i]) |
| 55 |
} |
|
| 56 | 182x |
strs |
| 57 |
} |
|
| 58 | ||
| 59 |
strmodify <- function(strs, replacement_str = default_str_map) {
|
|
| 60 | 182x |
map_tbl <- replacement_str |
| 61 | 182x |
if (!is.null(map_tbl) && nrow(map_tbl) > 0) {
|
| 62 | 178x |
for (i in seq_len(nrow(map_tbl))) {
|
| 63 | 356x |
pattern <- map_tbl$pattern[[i]] |
| 64 | 356x |
value <- map_tbl$value[i] |
| 65 | 356x |
strs <- gsub(pattern, value, strs, fixed = TRUE) |
| 66 |
} |
|
| 67 |
} |
|
| 68 | 182x |
return(strs) |
| 69 |
} |
|
| 70 | ||
| 71 |
prep_strs_for_rtf <- function(strs, string_map = default_str_map, markup_df = dps_markup_df) {
|
|
| 72 | 182x |
strs <- convert_dps_markup(strs, markup_df) |
| 73 | 182x |
strs <- strmodify(strs, string_map) |
| 74 | 182x |
return(strs) |
| 75 |
} |
|
| 76 | ||
| 77 | ||
| 78 |
#' Relabel Variables in a Dataset |
|
| 79 |
#' |
|
| 80 |
#' This function relabels variables in a dataset based on a provided list of labels. |
|
| 81 |
#' It can either replace existing labels or only add labels to variables without them. |
|
| 82 |
#' |
|
| 83 |
#' @param x (`data.frame`)\cr dataset containing variables to be relabeled. |
|
| 84 |
#' @param lbl_list (`list`)\cr named list of labels to apply to variables. |
|
| 85 |
#' @param replace_existing (`logical`)\cr if TRUE, existing labels will be replaced; |
|
| 86 |
#' if FALSE, only variables without labels will be updated. |
|
| 87 |
#' |
|
| 88 |
#' @return The dataset with updated variable labels. |
|
| 89 |
#' |
|
| 90 |
#' @export |
|
| 91 |
#' @keywords internal |
|
| 92 |
var_relabel_list <- function(x, lbl_list, replace_existing = TRUE) {
|
|
| 93 | ! |
if (replace_existing) {
|
| 94 | ! |
vars_to_relabel <- intersect(names(x), names(lbl_list)) |
| 95 |
} else {
|
|
| 96 | ! |
get_variables_with_empty_labels <- function(x) {
|
| 97 | ! |
labels <- var_labels(x) |
| 98 | ! |
mask <- lapply(labels, is.na) |> unlist() |
| 99 | ! |
return(names(labels[mask])) |
| 100 |
} |
|
| 101 | ||
| 102 | ! |
vars_without_labels <- get_variables_with_empty_labels(x) |
| 103 | ! |
vars_to_relabel <- intersect(vars_without_labels, names(lbl_list)) |
| 104 |
} |
|
| 105 | ! |
do.call(var_relabel, c(list(x = x), lbl_list[vars_to_relabel])) |
| 106 |
} |
| 1 |
#' Adding Labels To Variables For Model |
|
| 2 |
#' |
|
| 3 |
#' @param vars (`list`)\cr variables to use. |
|
| 4 |
#' @param data (`data.frame`)\cr data to use. |
|
| 5 |
#' @param x (`character`)\cr an element in vars. |
|
| 6 |
#' |
|
| 7 |
#' @name labels |
|
| 8 |
#' @keywords internal |
|
| 9 |
NULL |
|
| 10 | ||
| 11 |
#' @describeIn labels checks if element in `vars` is not `NULL` and not empty. |
|
| 12 |
h_is_specified <- function(x, vars) {
|
|
| 13 | 165x |
!is.null(vars[[x]]) && (length(vars[[x]]) > 0) |
| 14 |
} |
|
| 15 | ||
| 16 |
#' @describeIn labels checks if element in vars is not NULL and exists in dataset. |
|
| 17 |
h_is_specified_and_in_data <- function(x, vars, data) {
|
|
| 18 | 93x |
h_is_specified(x, vars) && all(vars[[x]] %in% names(data)) |
| 19 |
} |
|
| 20 | ||
| 21 |
#' @describeIn labels gets label for each element in vars. |
|
| 22 |
h_check_and_get_label <- function(x, vars, data) {
|
|
| 23 | 93x |
checkmate::assert_true(h_is_specified_and_in_data(x, vars, data)) |
| 24 | 93x |
res <- NULL |
| 25 | 93x |
for (v in vars[[x]]) {
|
| 26 | 108x |
label <- attr(data[[v]], "label") |
| 27 | 108x |
string <- ifelse(!is.null(label), label, v) |
| 28 | 108x |
res <- c(res, stats::setNames(string, v)) |
| 29 |
} |
|
| 30 | 93x |
res |
| 31 |
} |
|
| 32 | ||
| 33 |
#' Extraction of Covariate Parts from Character Vector |
|
| 34 |
#' |
|
| 35 |
#' @description |
|
| 36 |
#' Extraction of Covariate Parts from Character Vector. |
|
| 37 |
#' |
|
| 38 |
#' @param covariates (`character`)\cr specification in the usual way, see examples. |
|
| 39 |
#' |
|
| 40 |
#' @return Character vector of the covariates involved in `covariates` specification. |
|
| 41 |
#' @keywords internal |
|
| 42 |
h_get_covariate_parts <- function(covariates) {
|
|
| 43 | 18x |
checkmate::assert_character(covariates, null.ok = TRUE) |
| 44 | 18x |
if (is.null(covariates)) {
|
| 45 | ! |
NULL |
| 46 |
} else {
|
|
| 47 | 18x |
unique(unlist(strsplit(covariates, split = "\\*|:"))) |
| 48 |
} |
|
| 49 |
} |
|
| 50 | ||
| 51 |
#' @describeIn labels returns the list of variables with labels. |
|
| 52 |
h_labels <- function(vars, data) {
|
|
| 53 | 18x |
checkmate::assert_list(vars) |
| 54 | 18x |
checkmate::assert_data_frame(data) |
| 55 | 18x |
labels <- list() |
| 56 | 18x |
labels$response <- h_check_and_get_label("response", vars, data)
|
| 57 | 18x |
labels$id <- h_check_and_get_label("id", vars, data)
|
| 58 | 18x |
labels$visit <- h_check_and_get_label("visit", vars, data)
|
| 59 | 18x |
if (h_is_specified("arm", vars)) {
|
| 60 | 18x |
labels$arm <- h_check_and_get_label("arm", vars, data)
|
| 61 |
} |
|
| 62 | 18x |
if (h_is_specified("covariates", vars)) {
|
| 63 | 18x |
vars$parts <- h_get_covariate_parts(vars$covariates) |
| 64 | 18x |
labels$parts <- h_check_and_get_label("parts", vars, data)
|
| 65 |
} |
|
| 66 | 18x |
if (h_is_specified("weights", vars)) {
|
| 67 | ! |
labels$weights <- h_check_and_get_label("weights", vars, data)
|
| 68 |
} |
|
| 69 | 18x |
if (h_is_specified("subgroup", vars)) {
|
| 70 | 3x |
labels$subgroup <- h_check_and_get_label("subgroup", vars, data)
|
| 71 |
} |
|
| 72 | 18x |
return(labels) |
| 73 |
} |
|
| 74 | ||
| 75 |
#' Building Model Formula |
|
| 76 |
#' |
|
| 77 |
#' This builds the model formula which is used inside [fit_mmrm_j()] and provided |
|
| 78 |
#' to [mmrm::mmrm()] internally. It can be instructive to look at the resulting |
|
| 79 |
#' formula directly sometimes. In particular, if a `subgroup` variable is included |
|
| 80 |
#' in `vars`, then the formula will include the interaction of `subgroup` with |
|
| 81 |
#' `arm` and `visit`. |
|
| 82 |
#' |
|
| 83 |
#' @param vars (`list`)\cr variables to use in the model. |
|
| 84 |
#' @param cor_struct (`string`)\cr specify the covariance structure to use. |
|
| 85 |
#' @return Formula to use in [mmrm::mmrm()]. |
|
| 86 |
#' @export |
|
| 87 |
#' |
|
| 88 |
#' @examples |
|
| 89 |
#' vars <- list( |
|
| 90 |
#' response = "AVAL", |
|
| 91 |
#' covariates = c("RACE", "SEX"),
|
|
| 92 |
#' id = "USUBJID", |
|
| 93 |
#' arm = "ARMCD", |
|
| 94 |
#' visit = "AVISIT", |
|
| 95 |
#' subgroup = "REGION" |
|
| 96 |
#' ) |
|
| 97 |
#' build_formula(vars, "auto-regressive") |
|
| 98 |
#' build_formula(vars) |
|
| 99 |
build_formula <- function( |
|
| 100 |
vars, |
|
| 101 |
cor_struct = c( |
|
| 102 |
"unstructured", |
|
| 103 |
"toeplitz", |
|
| 104 |
"heterogeneous toeplitz", |
|
| 105 |
"ante-dependence", |
|
| 106 |
"heterogeneous ante-dependence", |
|
| 107 |
"auto-regressive", |
|
| 108 |
"heterogeneous auto-regressive", |
|
| 109 |
"compound symmetry", |
|
| 110 |
"heterogeneous compound symmetry" |
|
| 111 |
) |
|
| 112 |
) {
|
|
| 113 | 14x |
checkmate::assert_list(vars) |
| 114 | 14x |
cor_struct <- match.arg(cor_struct) |
| 115 | 14x |
covariates_part <- paste(vars$covariates, collapse = " + ") |
| 116 | 14x |
arm_visit_part <- if (is.null(vars$arm)) {
|
| 117 | ! |
vars$visit |
| 118 |
} else {
|
|
| 119 | 14x |
paste0(vars$arm, "*", vars$visit) |
| 120 |
} |
|
| 121 | 14x |
if (!is.null(vars$subgroup)) {
|
| 122 | 4x |
arm_visit_part <- paste0(arm_visit_part, "*", vars$subgroup) |
| 123 |
} |
|
| 124 | 14x |
random_effects_fun <- switch(cor_struct, |
| 125 | 14x |
unstructured = "us", |
| 126 | 14x |
toeplitz = "toep", |
| 127 | 14x |
`heterogeneous toeplitz` = "toeph", |
| 128 | 14x |
`ante-dependence` = "ad", |
| 129 | 14x |
`heterogeneous ante-dependence` = "adh", |
| 130 | 14x |
`auto-regressive` = "ar1", |
| 131 | 14x |
`heterogeneous auto-regressive` = "ar1h", |
| 132 | 14x |
`compound symmetry` = "cs", |
| 133 | 14x |
`heterogeneous compound symmetry` = "csh" |
| 134 |
) |
|
| 135 | 14x |
random_effects_part <- paste0(random_effects_fun, "(", vars$visit, " | ", vars$id, ")")
|
| 136 | 14x |
rhs_formula <- paste(arm_visit_part, "+", random_effects_part) |
| 137 | 14x |
if (covariates_part != "") {
|
| 138 | 14x |
rhs_formula <- paste(covariates_part, "+", rhs_formula) |
| 139 |
} |
|
| 140 | 14x |
stats::as.formula(paste(vars$response, "~", rhs_formula)) |
| 141 |
} |
|
| 142 | ||
| 143 |
#' Extract Least Square Means from `MMRM` |
|
| 144 |
#' |
|
| 145 |
#' Extracts the least square means from an `MMRM` fit. |
|
| 146 |
#' |
|
| 147 |
#' @inheritParams fit_mmrm_j |
|
| 148 |
#' @param fit (`mmrm`)\cr result of [mmrm::mmrm()]. |
|
| 149 |
#' @param averages (`list`)\cr named list of visit levels which should be averaged |
|
| 150 |
#' and reported along side the single visits. |
|
| 151 |
#' @param weights (`string`)\cr type of weights to be used for the least square means, |
|
| 152 |
#' see [emmeans::emmeans()] for details. |
|
| 153 |
#' @param mult_adj (`string`)\cr multiplicity adjustment within visits for the contrasts. |
|
| 154 |
#' @return A list with data frames `estimates` and `contrasts`. |
|
| 155 |
#' The attributes `averages`, `weights` and `mult_adj` save the settings used. |
|
| 156 |
#' |
|
| 157 |
#' @note This is modified from `tern.mmrm` and has the additional `mult_adj` argument. |
|
| 158 |
#' @export |
|
| 159 |
get_mmrm_lsmeans <- function( |
|
| 160 |
fit, vars, conf_level, weights, averages = list(), |
|
| 161 |
mult_adj = c("none", "dunnett", "step-down-dunnett")
|
|
| 162 |
) {
|
|
| 163 | 22x |
checkmate::assert_class(fit, "mmrm") |
| 164 | 22x |
checkmate::assert_list(averages, types = "character") |
| 165 | 22x |
mult_adj <- match.arg(mult_adj) |
| 166 | 22x |
emmeans_res <- h_get_emmeans_res(fit, vars, weights) |
| 167 | ||
| 168 |
# Get least square means estimates for single visits, and possibly averaged visits. |
|
| 169 | 22x |
estimates <- h_get_single_visit_estimates(emmeans_res, conf_level) |
| 170 | 22x |
if (length(averages)) {
|
| 171 | 7x |
average_specs <- h_get_average_visit_specs(emmeans_res, vars, averages, fit) |
| 172 | 7x |
average_estimates <- h_get_spec_visit_estimates(emmeans_res, average_specs, conf_level) |
| 173 | 7x |
estimates <- rbind(estimates, average_estimates) |
| 174 |
} |
|
| 175 | 22x |
has_arm_var <- !is.null(vars$arm) |
| 176 | 22x |
if (!has_arm_var) {
|
| 177 | 1x |
return(list(estimates = estimates)) |
| 178 |
} |
|
| 179 |
# Continue with contrasts when we have an arm variable. |
|
| 180 | 21x |
if (mult_adj != "none" && !identical(averages, list())) {
|
| 181 | ! |
stop("Multiplicity adjustment within visits cannot be combined with averages across visits")
|
| 182 |
} |
|
| 183 | ||
| 184 | 21x |
contrast_specs <- h_single_visit_contrast_specs(emmeans_res, vars) |
| 185 | 21x |
contrast_estimates <- h_get_spec_visit_estimates(emmeans_res, contrast_specs, conf_level, tests = TRUE) |
| 186 | ||
| 187 | 21x |
if (length(averages)) {
|
| 188 | 6x |
average_contrast_specs <- h_average_visit_contrast_specs(contrast_specs, averages, vars) |
| 189 | 6x |
average_contrasts <- h_get_spec_visit_estimates(emmeans_res, average_contrast_specs, conf_level, tests = TRUE) |
| 190 | 6x |
contrast_estimates <- rbind(contrast_estimates, average_contrasts) |
| 191 |
} |
|
| 192 | ||
| 193 | 21x |
if (mult_adj != "none") {
|
| 194 |
# We just calculate the adjusted p-values and confidence intervals here and replace them |
|
| 195 |
# in the contrast_estimates data frame. The estimates and t-stats remain unchanged. |
|
| 196 | 3x |
mult_adj_df <- h_get_mult_adj_estimates( |
| 197 | 3x |
emmeans_res = emmeans_res, |
| 198 | 3x |
vars = vars, |
| 199 | 3x |
mult_adj = mult_adj, |
| 200 | 3x |
conf_level = conf_level, |
| 201 | 3x |
contrast_df = contrast_estimates$df |
| 202 |
) |
|
| 203 | 3x |
contrast_estimates$p_value <- mult_adj_df$p_value |
| 204 | 3x |
contrast_estimates$p_value_less <- mult_adj_df$p_value_less |
| 205 | 3x |
contrast_estimates$p_value_greater <- mult_adj_df$p_value_greater |
| 206 | 3x |
contrast_estimates$lower_cl <- mult_adj_df$lower_cl |
| 207 | 3x |
contrast_estimates$upper_cl <- mult_adj_df$upper_cl |
| 208 |
} |
|
| 209 | ||
| 210 | 21x |
relative_reduc_df <- h_get_relative_reduc_df(estimates, vars) |
| 211 | 21x |
contrast_estimates <- merge( |
| 212 | 21x |
contrast_estimates, |
| 213 | 21x |
relative_reduc_df, |
| 214 | 21x |
by = c(vars$subgroup, vars$arm, vars$visit), |
| 215 | 21x |
sort = FALSE |
| 216 |
) |
|
| 217 | 21x |
if (!is.null(vars$subgroup)) {
|
| 218 | 6x |
contrast_estimates[[vars$subgroup]] <- factor( |
| 219 | 6x |
contrast_estimates[[vars$subgroup]], |
| 220 | 6x |
levels = intersect(levels(estimates[[vars$subgroup]]), contrast_estimates[[vars$subgroup]]) |
| 221 |
) |
|
| 222 |
} |
|
| 223 | 21x |
contrast_estimates[[vars$arm]] <- factor( |
| 224 | 21x |
contrast_estimates[[vars$arm]], |
| 225 | 21x |
levels = intersect(levels(estimates[[vars$arm]]), contrast_estimates[[vars$arm]]) |
| 226 |
) |
|
| 227 | 21x |
contrast_estimates[[vars$visit]] <- factor( |
| 228 | 21x |
contrast_estimates[[vars$visit]], |
| 229 | 21x |
levels = intersect(levels(estimates[[vars$visit]]), contrast_estimates[[vars$visit]]) |
| 230 |
) |
|
| 231 | 21x |
structure( |
| 232 | 21x |
list(estimates = estimates, contrasts = contrast_estimates), |
| 233 | 21x |
averages = averages, |
| 234 | 21x |
weights = weights, |
| 235 | 21x |
mult_adj = mult_adj |
| 236 |
) |
|
| 237 |
} |
|
| 238 | ||
| 239 |
#' `MMRM` Analysis |
|
| 240 |
#' |
|
| 241 |
#' Does the `MMRM` analysis. Multiple other functions can be called on the result to produce |
|
| 242 |
#' tables and graphs. |
|
| 243 |
#' |
|
| 244 |
#' @param vars (named `list` of `string` or `character`)\cr specifying the variables in the `MMRM`. |
|
| 245 |
#' The following elements need to be included as character vectors and match corresponding columns |
|
| 246 |
#' in `data`: |
|
| 247 |
#' |
|
| 248 |
#' - `response`: the response variable. |
|
| 249 |
#' - `covariates`: the additional covariate terms (might also include interactions). |
|
| 250 |
#' - `id`: the subject ID variable. |
|
| 251 |
#' - `arm`: the treatment group variable (factor). |
|
| 252 |
#' - `visit`: the visit variable (factor). |
|
| 253 |
#' - `weights`: optional weights variable (if `NULL` or omitted then no weights will be used). |
|
| 254 |
#' |
|
| 255 |
#' Note that the main effects and interaction of `arm` and `visit` are by default |
|
| 256 |
#' included in the model. |
|
| 257 |
#' @param data (`data.frame`)\cr with all the variables specified in |
|
| 258 |
#' `vars`. Records with missing values in any independent variables |
|
| 259 |
#' will be excluded. |
|
| 260 |
#' @param conf_level (`proportion`)\cr confidence level of the interval. |
|
| 261 |
#' @param cor_struct (`string`)\cr specifying the covariance structure, defaults to |
|
| 262 |
#' `'unstructured'`. See the details. |
|
| 263 |
#' @param averages_emmeans (`list`)\cr optional named list of visit levels which should be averaged |
|
| 264 |
#' and reported along side the single visits. |
|
| 265 |
#' @param weights_emmeans (`string`)\cr argument from [emmeans::emmeans()], `'counterfactual'` by default. |
|
| 266 |
#' @param mult_adj_emmeans (`string`)\cr whether to multiplicity adjust LS means contrast p-values and |
|
| 267 |
#' confidence intervals within visits when there are more than 2 treatment arms. Note that this cannot |
|
| 268 |
#' be combined with `averages_emmeans`. Either "none", "dunnett" or "step-down-dunnett". |
|
| 269 |
#' @param ... additional arguments for [mmrm::mmrm()], in particular `reml` and options listed in |
|
| 270 |
#' [mmrm::mmrm_control()]. |
|
| 271 |
#' |
|
| 272 |
#' @details Multiple different degree of freedom adjustments are available via the `method` argument |
|
| 273 |
#' for [mmrm::mmrm()]. In addition, covariance matrix adjustments are available via `vcov`. |
|
| 274 |
#' Please see [mmrm::mmrm_control()] for details and additional useful options. |
|
| 275 |
#' |
|
| 276 |
#' For the covariance structure (`cor_struct`), the user can choose among the following options. |
|
| 277 |
#' |
|
| 278 |
#' - `unstructured`: Unstructured covariance matrix. This is the most flexible choice and default. |
|
| 279 |
#' If there are `T` visits, then `T * (T+1) / 2` variance parameters are used. |
|
| 280 |
#' - `toeplitz`: Homogeneous Toeplitz covariance matrix, which uses `T` variance parameters. |
|
| 281 |
#' - `heterogeneous toeplitz`: Heterogeneous Toeplitz covariance matrix, |
|
| 282 |
#' which uses `2 * T - 1` variance parameters. |
|
| 283 |
#' - `ante-dependence`: Homogeneous Ante-Dependence covariance matrix, which uses `T` variance parameters. |
|
| 284 |
#' - `heterogeneous ante-dependence`: Heterogeneous Ante-Dependence covariance matrix, |
|
| 285 |
#' which uses `2 * T - 1` variance parameters. |
|
| 286 |
#' - `auto-regressive`: Homogeneous Auto-Regressive (order 1) covariance matrix, |
|
| 287 |
#' which uses 2 variance parameters. |
|
| 288 |
#' - `heterogeneous auto-regressive`: Heterogeneous Auto-Regressive (order 1) covariance matrix, |
|
| 289 |
#' which uses `T + 1` variance parameters. |
|
| 290 |
#' - `compound symmetry`: Homogeneous Compound Symmetry covariance matrix, which uses 2 |
|
| 291 |
#' variance parameters. |
|
| 292 |
#' - `heterogeneous compound symmetry`: Heterogeneous Compound Symmetry covariance matrix, which uses |
|
| 293 |
#' `T + 1` variance parameters. |
|
| 294 |
#' |
|
| 295 |
#' @return A `tern_model` object which is a list with model results: |
|
| 296 |
#' |
|
| 297 |
#' - `fit`: The `mmrm` object which was fitted to the data. Note that via `mmrm::component(fit, 'optimizer')` |
|
| 298 |
#' the finally used optimization algorithm can be obtained, which can be useful for refitting the model |
|
| 299 |
#' later on. |
|
| 300 |
#' - `cov_estimate`: The matrix with the covariance matrix estimate. |
|
| 301 |
#' - `diagnostics`: A list with model diagnostic statistics (REML criterion, AIC, corrected AIC, BIC). |
|
| 302 |
#' - `lsmeans`: This is a list with data frames `estimates` and `contrasts`. |
|
| 303 |
#' The attributes `averages`, `weights` and `mult_adj` save the settings used |
|
| 304 |
#' (`averages_emmeans`, `weights_emmeans` and `mult_adj_emmeans`). |
|
| 305 |
#' - `vars`: The variable list. |
|
| 306 |
#' - `labels`: Corresponding list with variable labels extracted from `data`. |
|
| 307 |
#' - `cor_struct`: input. |
|
| 308 |
#' - `ref_level`: The reference level for the arm variable, which is always the first level. |
|
| 309 |
#' - `treatment_levels`: The treatment levels for the arm variable. |
|
| 310 |
#' - `conf_level`: The confidence level which was used to construct the `lsmeans` confidence intervals. |
|
| 311 |
#' - `additional`: List with any additional inputs passed via `...` |
|
| 312 |
#' |
|
| 313 |
#' @export |
|
| 314 |
#' |
|
| 315 |
#' @note This function has the `_j` suffix to distinguish it from [mmrm::fit_mmrm()]. |
|
| 316 |
#' It is modified from the `tern.mmrm` package. |
|
| 317 |
#' The new features are: |
|
| 318 |
#' |
|
| 319 |
#' - the `mult_adj_emmeans` argument and its functionality |
|
| 320 |
#' - the `subgroup` variable in `vars` and its functionality |
|
| 321 |
#' |
|
| 322 |
#' These could later be contributed upstream in `tern.mmrm`. |
|
| 323 |
#' |
|
| 324 |
#' @examples |
|
| 325 |
#' mmrm_results <- fit_mmrm_j( |
|
| 326 |
#' vars = list( |
|
| 327 |
#' response = "FEV1", |
|
| 328 |
#' covariates = c("RACE", "SEX"),
|
|
| 329 |
#' id = "USUBJID", |
|
| 330 |
#' arm = "ARMCD", |
|
| 331 |
#' visit = "AVISIT" |
|
| 332 |
#' ), |
|
| 333 |
#' data = mmrm::fev_data, |
|
| 334 |
#' cor_struct = "unstructured", |
|
| 335 |
#' weights_emmeans = "equal", |
|
| 336 |
#' averages_emmeans = list( |
|
| 337 |
#' "VIS1+2" = c("VIS1", "VIS2")
|
|
| 338 |
#' ) |
|
| 339 |
#' ) |
|
| 340 |
fit_mmrm_j <- function( |
|
| 341 |
vars = list( |
|
| 342 |
response = "AVAL", |
|
| 343 |
covariates = c(), |
|
| 344 |
id = "USUBJID", |
|
| 345 |
arm = "ARM", |
|
| 346 |
visit = "AVISIT", |
|
| 347 |
subgroup = NULL |
|
| 348 |
), |
|
| 349 |
data, |
|
| 350 |
conf_level = 0.95, |
|
| 351 |
cor_struct = "unstructured", |
|
| 352 |
weights_emmeans = "counterfactual", |
|
| 353 |
averages_emmeans = list(), |
|
| 354 |
mult_adj_emmeans = c("none", "dunnett", "step-down-dunnett"),
|
|
| 355 |
... |
|
| 356 |
) {
|
|
| 357 | 13x |
labels <- h_labels(vars, data) |
| 358 | 13x |
formula <- build_formula(vars, cor_struct) |
| 359 | 13x |
weights <- if (!is.null(vars$weights)) data[[vars$weights]] else NULL |
| 360 | ||
| 361 | 13x |
fit <- mmrm::mmrm(formula = formula, data = data, weights = weights, reml = TRUE, ...) |
| 362 | 13x |
lsmeans <- get_mmrm_lsmeans( |
| 363 | 13x |
fit = fit, |
| 364 | 13x |
vars = vars, |
| 365 | 13x |
conf_level = conf_level, |
| 366 | 13x |
averages = averages_emmeans, |
| 367 | 13x |
weights = weights_emmeans, |
| 368 | 13x |
mult_adj = mult_adj_emmeans |
| 369 |
) |
|
| 370 | 13x |
cov_estimate <- mmrm::VarCorr(fit) |
| 371 | 13x |
visit_levels <- rownames(cov_estimate) |
| 372 | 13x |
contrasts_from_visits <- match(visit_levels, lsmeans$contrasts[[vars$visit]]) |
| 373 | 13x |
df <- stats::setNames(lsmeans$contrasts$df[contrasts_from_visits], visit_levels) |
| 374 | 13x |
results <- list( |
| 375 | 13x |
fit = fit, |
| 376 | 13x |
cov_estimate = cov_estimate, |
| 377 | 13x |
lsmeans = lsmeans, |
| 378 | 13x |
vars = vars, |
| 379 | 13x |
labels = labels, |
| 380 | 13x |
mse = diag(cov_estimate), |
| 381 | 13x |
df = df, |
| 382 | 13x |
cor_struct = cor_struct, |
| 383 | 13x |
ref_level = if (is.null(vars$arm)) NULL else levels(data[[vars$arm]])[1], |
| 384 | 13x |
treatment_levels = if (is.null(vars$arm)) NULL else levels(data[[vars$arm]])[-1], |
| 385 | 13x |
conf_level = conf_level, |
| 386 | 13x |
additional = list(...) |
| 387 |
) |
|
| 388 | 13x |
class(results) <- "tern_model" |
| 389 | 13x |
return(results) |
| 390 |
} |
| 1 |
#' Pool analysis results obtained from the imputed datasets |
|
| 2 |
#' |
|
| 3 |
#' @details This has been forked from the `rbmi` package, mainly to support in |
|
| 4 |
#' addition the pooling of variance estimates. See pool() for more details. |
|
| 5 |
#' |
|
| 6 |
#' @param results an analysis object created by analyse(). |
|
| 7 |
#' |
|
| 8 |
#' @param conf.level confidence level of the returned confidence interval. |
|
| 9 |
#' Must be a single number between 0 and 1. Default is 0.95. |
|
| 10 |
#' |
|
| 11 |
#' @param alternative a character string specifying the alternative hypothesis, |
|
| 12 |
#' must be one of `"two.sided"` (default), `"greater"` or `"less"`. |
|
| 13 |
#' |
|
| 14 |
#' @param type a character string of either `"percentile"` (default) or |
|
| 15 |
#' `"normal"`. Determines what method should be used to calculate the bootstrap |
|
| 16 |
#' confidence intervals. See details. |
|
| 17 |
#' Only used if `method_condmean(type = "bootstrap")` was specified |
|
| 18 |
#' in the original call to draws(). |
|
| 19 |
#' |
|
| 20 |
#' @returns A list of class `pool`. |
|
| 21 |
#' |
|
| 22 |
#' @export |
|
| 23 |
rbmi_pool <- function( |
|
| 24 |
results, |
|
| 25 |
conf.level = 0.95, |
|
| 26 |
alternative = c("two.sided", "less", "greater"),
|
|
| 27 |
type = c("percentile", "normal")
|
|
| 28 |
) {
|
|
| 29 | 6x |
rbmi::validate(results) |
| 30 | ||
| 31 | 6x |
alternative <- match.arg(alternative) |
| 32 | 6x |
type <- match.arg(type) |
| 33 | ||
| 34 | 6x |
pool_type <- class(results$results)[[1]] |
| 35 | 6x |
checkmate::assert_true(identical(pool_type, "rubin")) |
| 36 | ||
| 37 | 6x |
results_transpose <- (utils::getFromNamespace("transpose_results", "rbmi"))(
|
| 38 | 6x |
results$results, |
| 39 | 6x |
(utils::getFromNamespace("get_pool_components", "rbmi"))(pool_type)
|
| 40 |
) |
|
| 41 | ||
| 42 | 6x |
pars <- lapply( |
| 43 | 6x |
results_transpose, |
| 44 | 6x |
function(x, ...) mod_pool_internal_rubin(x, ...), |
| 45 | 6x |
conf.level = conf.level, |
| 46 | 6x |
alternative = alternative, |
| 47 | 6x |
type = type, |
| 48 | 6x |
D = results$method$D |
| 49 |
) |
|
| 50 | ||
| 51 | 6x |
method <- pool_type |
| 52 | ||
| 53 | 6x |
ret <- list( |
| 54 | 6x |
pars = pars, |
| 55 | 6x |
conf.level = conf.level, |
| 56 | 6x |
alternative = alternative, |
| 57 | 6x |
N = length(results$results), |
| 58 | 6x |
method = method |
| 59 |
) |
|
| 60 | 6x |
class(ret) <- "pool" |
| 61 | 6x |
return(ret) |
| 62 |
} |
|
| 63 | ||
| 64 |
mod_pool_internal_rubin <- function(results, conf.level, alternative, type, D) {
|
|
| 65 | 9x |
ests <- results$est |
| 66 | 9x |
ses <- results$se |
| 67 | 9x |
dfs <- results$df |
| 68 | 9x |
alpha <- 1 - conf.level |
| 69 | ||
| 70 |
# Note: Need to take median here, because in the MMRM case the d.f. will be slightly different for each imputed |
|
| 71 |
# data set analysis. |
|
| 72 | 9x |
v_com <- stats::median(dfs) |
| 73 | ||
| 74 | 9x |
res_rubin <- if (utils::packageVersion("rbmi") > "1.6.1") {
|
| 75 | 9x |
(utils::getFromNamespace("rubin_rules", "rbmi"))(
|
| 76 | 9x |
ests = ests, |
| 77 | 9x |
ses = ses, |
| 78 | 9x |
v_com = v_com, |
| 79 | 9x |
method = "barnard-rubin" |
| 80 |
) |
|
| 81 |
} else {
|
|
| 82 | ! |
(utils::getFromNamespace("rubin_rules", "rbmi"))(
|
| 83 | ! |
ests = ests, |
| 84 | ! |
ses = ses, |
| 85 | ! |
v_com = v_com |
| 86 |
) |
|
| 87 |
} |
|
| 88 | ||
| 89 | 9x |
ret <- (utils::getFromNamespace("parametric_ci", "rbmi"))(
|
| 90 | 9x |
point = res_rubin$est_point, |
| 91 | 9x |
se = sqrt(res_rubin$var_t), |
| 92 | 9x |
alpha = alpha, |
| 93 | 9x |
alternative = alternative, |
| 94 | 9x |
qfun = stats::qt, |
| 95 | 9x |
pfun = stats::pt, |
| 96 | 9x |
df = res_rubin$df |
| 97 |
) |
|
| 98 |
# Here also return the pooled d.f.: |
|
| 99 | 9x |
ret$df <- res_rubin$df |
| 100 | ||
| 101 | 9x |
return(ret) |
| 102 |
} |
| 1 |
#' Tabulation for Exposure Tables |
|
| 2 |
#' |
|
| 3 |
#' |
|
| 4 |
#' @details |
|
| 5 |
#' Creates statistics needed for standard exposure table. |
|
| 6 |
#' This includes differences and 95% CI and total treatment years. |
|
| 7 |
#' This is designed to be used as an analysis (afun in `analyze`) function. |
|
| 8 |
#' |
|
| 9 |
#' @name a_summarize_ex_j |
|
| 10 |
NULL |
|
| 11 | ||
| 12 |
#' @inheritParams proposal_argument_convention |
|
| 13 |
#' @describeIn a_summarize_ex_j Statistics function needed for the exposure tables. |
|
| 14 |
#' |
|
| 15 |
#' @param daysconv (`numeric`)\cr conversion required to get the values into days |
|
| 16 |
#' (i.e 1 if original PARAMCD unit is days, 30.4375 if original PARAMCD unit is in months) |
|
| 17 |
#' @param ancova (`logical`)\cr If FALSE, only descriptive methods will be used. \cr |
|
| 18 |
#' If TRUE, ANCOVA methods will be used for each of the columns : AVAL, CHG, DIFF. \cr |
|
| 19 |
#' @param comp_btw_group (`logical`)\cr If TRUE, comparison between groups will be performed. |
|
| 20 |
#' \cr When ancova = FALSE, the estimate of between group difference (on CHG) will be based upon two-sample t-test. |
|
| 21 |
#' \cr When ancova = TRUE, the same ANCOVA model will be used for the estimate of between group difference (on CHG). |
|
| 22 |
#' @param interaction_y (`character`)\cr Will be passed onto the `tern` function `s_ancova`, when ancova = TRUE. |
|
| 23 |
#' @param interaction_item (`character`)\cr Will be passed onto the `tern` function `s_ancova`, when ancova = TRUE. |
|
| 24 |
#' @param conf_level (`proportion`)\cr Confidence level of the interval |
|
| 25 |
#' @param variables (named list of strings)\cr |
|
| 26 |
#' list of additional analysis variables, with expected elements: |
|
| 27 |
#' * arm (string)\cr |
|
| 28 |
#' group variable, for which the covariate adjusted means of multiple groups will be summarized. |
|
| 29 |
#' Specifically, the first level of arm variable is taken as the reference group. |
|
| 30 |
#' * covariates (character)\cr |
|
| 31 |
#' a vector that can contain single variable names (such as 'X1'), and/or interaction terms indicated by 'X1 * X2'. |
|
| 32 |
s_summarize_ex_j <- function( |
|
| 33 |
df, |
|
| 34 |
.var, |
|
| 35 |
.df_row, |
|
| 36 |
.spl_context, |
|
| 37 |
comp_btw_group = TRUE, |
|
| 38 |
ref_path = NULL, |
|
| 39 |
ancova = FALSE, |
|
| 40 |
interaction_y, |
|
| 41 |
interaction_item, |
|
| 42 |
conf_level, |
|
| 43 |
daysconv, |
|
| 44 |
variables |
|
| 45 |
) {
|
|
| 46 | 5x |
control <- control_analyze_vars() |
| 47 | 5x |
control$conf_level <- conf_level |
| 48 | 5x |
x_stats <- s_summary(df[[.var]], na.rm = TRUE, .var, control = control) |
| 49 |
## add extra for subject years |
|
| 50 | 5x |
subj_years <- x_stats[["sum"]] * daysconv / 365.25 |
| 51 | 5x |
x_stats[["total_subject_years"]] <- c(x_stats[["sum"]], subj_years) |
| 52 | 5x |
names(x_stats[["total_subject_years"]]) <- c("total", "subject_years")
|
| 53 | ||
| 54 | 5x |
cur_col_id <- .spl_context$cur_col_id[[length(.spl_context$split)]] |
| 55 | 5x |
indiffcol <- grepl("difference", tolower(cur_col_id), fixed = TRUE)
|
| 56 | ||
| 57 | 5x |
if (indiffcol) {
|
| 58 |
# blank out all stats |
|
| 59 | 2x |
x_stats <- sapply( |
| 60 | 2x |
names(x_stats), |
| 61 | 2x |
FUN = function(x) {
|
| 62 | 60x |
x_stats[[x]] <- NULL |
| 63 |
}, |
|
| 64 | 2x |
simplify = FALSE, |
| 65 | 2x |
USE.NAMES = TRUE |
| 66 |
) |
|
| 67 |
# diff between group will be updated in mean_sd stat |
|
| 68 | 2x |
if (comp_btw_group) {
|
| 69 | 2x |
trt_var <- ref_path[length(ref_path) - 1L] |
| 70 | 2x |
ctrl_grp <- ref_path[length(ref_path)] |
| 71 | 2x |
cur_trt_grp <- h_get_cur_trt_grp(trt_var, .spl_context) |
| 72 | ||
| 73 | 2x |
.in_ref_col <- FALSE |
| 74 | ! |
if (cur_trt_grp == ctrl_grp) .in_ref_col <- TRUE |
| 75 | ||
| 76 | 2x |
.ref_group <- .df_row[.df_row[[trt_var]] == ctrl_grp, ] |
| 77 | ||
| 78 | 2x |
if (ancova) {
|
| 79 |
# ancova method for diff between group |
|
| 80 | 2x |
x_stats2 <- s_summarize_ancova_j( |
| 81 | 2x |
df = df, |
| 82 | 2x |
.var = .var, |
| 83 | 2x |
.ref_group = .ref_group, |
| 84 | 2x |
.in_ref_col = .in_ref_col, |
| 85 | 2x |
.df_row = .df_row, |
| 86 | 2x |
conf_level = conf_level, |
| 87 | 2x |
interaction_y = interaction_y, |
| 88 | 2x |
interaction_item = interaction_item, |
| 89 | 2x |
variables = variables |
| 90 |
) |
|
| 91 | 2x |
diffstat <- x_stats2[["lsmean_diff_with_ci"]] |
| 92 |
} else {
|
|
| 93 |
# descriptive method for diff between group |
|
| 94 | ! |
x_stats2 <- s_summarize_desc_j( |
| 95 | ! |
df = df, |
| 96 | ! |
.var = .var, |
| 97 | ! |
.ref_group = .ref_group, |
| 98 | ! |
.in_ref_col = .in_ref_col, |
| 99 | ! |
control = control |
| 100 |
) |
|
| 101 | ! |
diffstat <- x_stats2[["mean_diff_with_ci"]] |
| 102 |
} |
|
| 103 |
# actual update with the diffstat |
|
| 104 | 2x |
x_stats[["mean_sd"]] <- diffstat |
| 105 |
} |
|
| 106 |
} |
|
| 107 | ||
| 108 | 5x |
return(x_stats) |
| 109 |
} |
|
| 110 | ||
| 111 |
#' @title Analysis Function For Exposure Tables |
|
| 112 |
#' @description |
|
| 113 |
#' A function to create the appropriate statistics needed for exposure table |
|
| 114 |
#' @inheritParams proposal_argument_convention |
|
| 115 |
#' |
|
| 116 |
#' @describeIn a_summarize_ex_j Formatted analysis function which is used as `afun`. |
|
| 117 |
#' |
|
| 118 |
#' @return |
|
| 119 |
#' * `a_summarize_ex_j()` returns the corresponding list with formatted [rtables::CellValue()]. |
|
| 120 |
#' |
|
| 121 |
#' @aliases a_summarize_ex_j |
|
| 122 |
#' @examples |
|
| 123 |
#' library(dplyr) |
|
| 124 |
#' ADEX <- ex_adsl |> select(USUBJID, ARM, TRTSDTM, EOSSTT, EOSDY) |
|
| 125 |
#' |
|
| 126 |
#' trtvar <- "ARM" |
|
| 127 |
#' ctrl_grp <- "B: Placebo" |
|
| 128 |
#' cutoffd <- as.Date("2023-09-24")
|
|
| 129 |
#' |
|
| 130 |
#' ADEX <- ADEX |> |
|
| 131 |
#' create_colspan_var( |
|
| 132 |
#' non_active_grp = ctrl_grp, |
|
| 133 |
#' non_active_grp_span_lbl = " ", |
|
| 134 |
#' active_grp_span_lbl = "Active Study Agent", |
|
| 135 |
#' colspan_var = "colspan_trt", |
|
| 136 |
#' trt_var = trtvar |
|
| 137 |
#' ) |> |
|
| 138 |
#' mutate( |
|
| 139 |
#' diff_header = "Difference in Means (95% CI)", |
|
| 140 |
#' diff_label = paste(!!rlang::sym(trtvar), "vs", ctrl_grp), |
|
| 141 |
#' TRTDURY = case_when( |
|
| 142 |
#' !is.na(EOSDY) ~ EOSDY, |
|
| 143 |
#' TRUE ~ as.integer(cutoffd - as.Date(TRTSDTM) + 1) |
|
| 144 |
#' ) |
|
| 145 |
#' ) |
|
| 146 |
#' |
|
| 147 |
#' colspan_trt_map <- create_colspan_map(ADEX, |
|
| 148 |
#' non_active_grp = ctrl_grp, |
|
| 149 |
#' non_active_grp_span_lbl = " ", |
|
| 150 |
#' active_grp_span_lbl = "Active Study Agent", |
|
| 151 |
#' colspan_var = "colspan_trt", |
|
| 152 |
#' trt_var = trtvar |
|
| 153 |
#' ) |
|
| 154 |
#' |
|
| 155 |
#' ref_path <- c("colspan_trt", "", trtvar, ctrl_grp)
|
|
| 156 |
#' |
|
| 157 |
#' lyt <- basic_table() |> |
|
| 158 |
#' split_cols_by( |
|
| 159 |
#' "colspan_trt", |
|
| 160 |
#' split_fun = trim_levels_to_map(map = colspan_trt_map) |
|
| 161 |
#' ) |> |
|
| 162 |
#' split_cols_by(trtvar) |> |
|
| 163 |
#' split_cols_by("diff_header", nested = FALSE) |>
|
|
| 164 |
#' split_cols_by( |
|
| 165 |
#' trtvar, |
|
| 166 |
#' split_fun = remove_split_levels(ctrl_grp), |
|
| 167 |
#' labels_var = "diff_label" |
|
| 168 |
#' ) |> |
|
| 169 |
#' analyze("EOSDY",
|
|
| 170 |
#' afun = a_summarize_ex_j, var_labels = "Duration of treatment (Days)", |
|
| 171 |
#' show_labels = "visible", |
|
| 172 |
#' indent_mod = 0L, |
|
| 173 |
#' extra_args = list( |
|
| 174 |
#' daysconv = 1, |
|
| 175 |
#' ref_path = ref_path, |
|
| 176 |
#' variables = list(arm = trtvar, covariates = NULL), |
|
| 177 |
#' ancova = TRUE, |
|
| 178 |
#' comp_btw_group = TRUE |
|
| 179 |
#' ) |
|
| 180 |
#' ) |
|
| 181 |
#' |
|
| 182 |
#' result <- build_table(lyt, ADEX, alt_counts_df = ADEX) |
|
| 183 |
#' result |
|
| 184 |
#' @export |
|
| 185 |
a_summarize_ex_j <- function( |
|
| 186 |
df, |
|
| 187 |
.var, |
|
| 188 |
.df_row, |
|
| 189 |
.spl_context, |
|
| 190 |
comp_btw_group = TRUE, |
|
| 191 |
ref_path = NULL, |
|
| 192 |
ancova = FALSE, |
|
| 193 |
interaction_y = FALSE, |
|
| 194 |
interaction_item = NULL, |
|
| 195 |
conf_level = 0.95, |
|
| 196 |
variables, |
|
| 197 |
.stats = c("mean_sd", "median", "range", "quantiles", "total_subject_years"),
|
|
| 198 |
.formats = c(diff_mean_est_ci = jjcsformat_xx("xx.xx (xx.xx, xx.xx)")),
|
|
| 199 |
.labels = c(quantiles = "Interquartile range"), |
|
| 200 |
.indent_mods = NULL, |
|
| 201 |
na_str = rep("NA", 3),
|
|
| 202 |
daysconv = 1 |
|
| 203 |
) {
|
|
| 204 | 5x |
checkmate::check_character(ref_path, min.len = 2L) |
| 205 | 5x |
checkmate::assert_true(length(ref_path) %% 2L == 0L) |
| 206 | ||
| 207 | 5x |
if (!is.numeric(df[[.var]])) {
|
| 208 | ! |
stop("a_summarize_ex_j issue: input variable must be numeric.")
|
| 209 |
} |
|
| 210 | ||
| 211 | 5x |
if (comp_btw_group && is.null(ref_path)) {
|
| 212 | ! |
stop("a_summarize_ex_j issue: argument ref_path cannot be NULL.")
|
| 213 |
} |
|
| 214 | ||
| 215 | 5x |
if (comp_btw_group && ancova && is.null(variables)) {
|
| 216 | ! |
stop("a_summarize_ex_j issue: argument variables must be defined when ancova is requested.")
|
| 217 |
} |
|
| 218 | ||
| 219 | 5x |
x_stats <- s_summarize_ex_j( |
| 220 | 5x |
df = df, |
| 221 | 5x |
.var = .var, |
| 222 | 5x |
.df_row = .df_row, |
| 223 | 5x |
.spl_context = .spl_context, |
| 224 | 5x |
comp_btw_group = comp_btw_group, |
| 225 | 5x |
ref_path = ref_path, |
| 226 | 5x |
ancova = ancova, |
| 227 | 5x |
interaction_y = interaction_y, |
| 228 | 5x |
interaction_item = interaction_item, |
| 229 | 5x |
conf_level = conf_level, |
| 230 | 5x |
daysconv = daysconv, |
| 231 | 5x |
variables = variables |
| 232 |
) |
|
| 233 | ||
| 234 |
# Fill in formatting defaults |
|
| 235 | 5x |
.stats_in <- .stats |
| 236 | 5x |
.stats <- tern_get_stats("analyze_vars_numeric", stats_in = .stats, custom_stats_in = NULL)
|
| 237 | 5x |
if ("total_subject_years" %in% .stats_in) {
|
| 238 |
# place the extra statistic at the appropriate place within .stats vector |
|
| 239 | 5x |
i <- match("total_subject_years", .stats_in)
|
| 240 | 5x |
x <- .stats_in[i:length(.stats_in)] |
| 241 | 5x |
if (length(x) == 1) {
|
| 242 | 5x |
.stats <- c(.stats, "total_subject_years") |
| 243 |
} else {
|
|
| 244 | ! |
i2 <- min(match(x, .stats), na.rm = TRUE) |
| 245 | ! |
if (i2 == 1) {
|
| 246 | ! |
.stats <- c("total_subject_years", .stats)
|
| 247 |
} else {
|
|
| 248 | ! |
.stats <- c(.stats[1:(i2 - 1)], "total_subject_years", .stats[i2:length(.stats)]) |
| 249 |
} |
|
| 250 |
} |
|
| 251 |
} |
|
| 252 | ||
| 253 | 5x |
.stats_ext <- c(.stats, "diff_mean_est_ci") |
| 254 | ||
| 255 | 5x |
.formats <- junco_get_formats_from_stats(.stats_ext, .formats) |
| 256 | 5x |
.labels <- junco_get_labels_from_stats(.stats, .labels, label_attr_from_stats = get_label_attr_from_stats(x_stats)) |
| 257 | 5x |
.indent_mods <- junco_get_indents_from_stats(.stats, .indent_mods) |
| 258 | ||
| 259 | 5x |
.names <- names(.labels) |
| 260 | 5x |
.labels <- .unlist_keep_nulls(.labels) |
| 261 | 5x |
.indent_mods <- .unlist_keep_nulls(.indent_mods) |
| 262 | ||
| 263 | 5x |
cur_col_id <- .spl_context$cur_col_id[[length(.spl_context$split)]] |
| 264 | 5x |
indiffcol <- grepl("difference", tolower(cur_col_id), fixed = TRUE)
|
| 265 | ||
| 266 | 5x |
if (indiffcol && comp_btw_group) {
|
| 267 | 2x |
.formats[["mean_sd"]] <- .formats[["diff_mean_est_ci"]] |
| 268 |
} |
|
| 269 | 5x |
.formats[["diff_mean_est_ci"]] <- NULL |
| 270 | ||
| 271 | 5x |
if (!is.null(na_str)) {
|
| 272 | 5x |
.format_na_strs <- lapply(names(.formats), FUN = function(x) {
|
| 273 | 25x |
na_str |
| 274 |
}) |
|
| 275 |
} else {
|
|
| 276 | ! |
.format_na_strs <- NULL |
| 277 |
} |
|
| 278 | ||
| 279 | 5x |
x_stats <- x_stats[.stats] |
| 280 | 5x |
ret <- in_rows( |
| 281 | 5x |
.list = x_stats, |
| 282 | 5x |
.formats = .formats, |
| 283 | 5x |
.names = .names, |
| 284 | 5x |
.labels = .labels, |
| 285 | 5x |
.indent_mods = .indent_mods, |
| 286 | 5x |
.format_na_strs = .format_na_strs |
| 287 |
) |
|
| 288 | 5x |
return(ret) |
| 289 |
} |
| 1 |
s_summarize_desc_j <- function(df, .var, .ref_group, .in_ref_col, control = control_analyze_vars()) {
|
|
| 2 | 175x |
x <- df[[.var]] |
| 3 | 175x |
y1 <- s_summary(x) |
| 4 | 175x |
y2 <- NULL |
| 5 | ||
| 6 |
# diff in means versus control group, based upon 2 sample t.test |
|
| 7 | 175x |
y2$mean_diff_with_ci <- numeric() |
| 8 | 175x |
if (!is.null(.ref_group) && !.in_ref_col) {
|
| 9 | 101x |
x1 <- df[[.var]] |
| 10 | 101x |
x2 <- .ref_group[[.var]] |
| 11 | ||
| 12 | 101x |
x1 <- x1[!is.na(x1)] |
| 13 | 101x |
x2 <- x2[!is.na(x2)] |
| 14 | ||
| 15 | 101x |
ttest_stat <- safe_t_test(x1, x2, conf.level = control$conf_level) |
| 16 | ||
| 17 | 101x |
stat <- ttest_stat[c("estimate", "conf.int")]
|
| 18 | 101x |
stat$diff <- stat$estimate[1] - stat$estimate[2] |
| 19 | 101x |
stat <- c(stat$diff, stat$conf.int) |
| 20 | ||
| 21 | 101x |
y2$mean_diff_with_ci <- with_label( |
| 22 | 101x |
c(mean_diff_with_ci = stat), |
| 23 | 101x |
paste("Difference in Mean + ", f_conf_level(control$conf_level))
|
| 24 |
) |
|
| 25 |
} |
|
| 26 | 175x |
y <- c(y1, y2) |
| 27 | ||
| 28 | 175x |
return(y) |
| 29 |
} |
|
| 30 | ||
| 31 |
s_aval_chg_col1 <- function(df, .var, denom, .N_col, id, indatavar) {
|
|
| 32 |
## First column AVAL - show n/N (%) |
|
| 33 | 100x |
mystat <- "n" |
| 34 | ||
| 35 | 100x |
if (!is.null(indatavar)) {
|
| 36 | ! |
df <- subset(df, !is.na(df[[indatavar]])) |
| 37 |
} |
|
| 38 | ||
| 39 | 100x |
x <- df[[.var]] |
| 40 | ||
| 41 | 100x |
x_stats <- s_summary(x) |
| 42 | 100x |
x_stats <- x_stats[mystat] |
| 43 | ||
| 44 |
### Ndenom derivation in case denom = N |
|
| 45 | 100x |
Ndenom <- .N_col |
| 46 | 100x |
if (denom == "N") {
|
| 47 |
### as our input dataset has ensured we have unique subjects we can just use the length of x here still safer |
|
| 48 |
### to use id variable |
|
| 49 | 100x |
nsub <- length(unique(df[[id]])) |
| 50 | 100x |
Ndenom <- nsub |
| 51 |
} |
|
| 52 | ||
| 53 | 100x |
count_denom_frac <- c(x_stats$n, Ndenom, x_stats$n / Ndenom) |
| 54 | 100x |
names(count_denom_frac) <- c("n", "N", "fraction")
|
| 55 | ||
| 56 | 100x |
count_frac <- count_denom_frac[c("n", "fraction")]
|
| 57 | 100x |
count <- count_denom_frac[c("n")]
|
| 58 | ||
| 59 | 100x |
y <- list() |
| 60 | 100x |
y$count_denom_frac <- count_denom_frac |
| 61 | 100x |
y$count_frac <- count_frac |
| 62 | 100x |
y$count <- count |
| 63 | ||
| 64 | 100x |
return(y) |
| 65 |
} |
|
| 66 | ||
| 67 |
s_aval_chg_col23_diff <- function( |
|
| 68 |
df, |
|
| 69 |
.var, |
|
| 70 |
.df_row, |
|
| 71 |
.ref_group, |
|
| 72 |
.in_ref_col, |
|
| 73 |
ancova, |
|
| 74 |
interaction_y, |
|
| 75 |
interaction_item, |
|
| 76 |
conf_level, |
|
| 77 |
variables, |
|
| 78 |
trt_var, |
|
| 79 |
ctrl_grp, |
|
| 80 |
cur_param, |
|
| 81 |
cur_lvl, |
|
| 82 |
weights_emmeans, |
|
| 83 |
method_combo, |
|
| 84 |
weights_combo |
|
| 85 |
) {
|
|
| 86 | 244x |
.df_row <- subset(.df_row, !is.na(.df_row[[.var]])) |
| 87 | 244x |
df <- subset(df, !is.na(df[[.var]])) |
| 88 | 244x |
.ref_group <- subset(.ref_group, !is.na(.ref_group[[.var]])) |
| 89 | ||
| 90 | 244x |
if (nrow(.df_row) == 0) {
|
| 91 |
#### this is only when input row no non-missing records in any of the columns expected to occur for baseline |
|
| 92 |
#### timepoint for analysis variable change only here we want a blank cell, not a cell with all NA's NULL is |
|
| 93 |
#### generating a blank cell |
|
| 94 | ! |
x_stats <- NULL |
| 95 | ! |
mystat1 <- c("mean_ci_3d", "mean_diff_with_ci")
|
| 96 | 244x |
} else if (!ancova) {
|
| 97 | 173x |
mystat1 <- c("mean_ci_3d", "mean_diff_with_ci")
|
| 98 | ||
| 99 | 173x |
control <- control_analyze_vars() |
| 100 | 173x |
control$conf_level <- conf_level |
| 101 | 173x |
x_stats <- s_summarize_desc_j( |
| 102 | 173x |
df = df, |
| 103 | 173x |
.var = .var, |
| 104 | 173x |
.ref_group = .ref_group, |
| 105 | 173x |
.in_ref_col = .in_ref_col, |
| 106 | 173x |
control = control |
| 107 |
) |
|
| 108 |
} else {
|
|
| 109 | 71x |
mystat1 <- c("lsmean_ci", "lsmean_diff_with_ci")
|
| 110 | ||
| 111 |
### sparse data problems with underlying ancova function 1/ if nrow(.df_row) = 0 NULL (blank columns) |
|
| 112 | ||
| 113 |
### 2/ if nrow(df) = 0 no ancova - lsmean and lsmean diff should be na |
|
| 114 | ||
| 115 |
### 3/ if nrow(df) > 0, & nrow(.ref_group) = 0 ancova for ls mean only, NULL .ref_group -- lsmean diff should |
|
| 116 |
### be na |
|
| 117 | ||
| 118 |
### 4/ if nrow(df) > 0, & nrow(.ref_group) > 0 and at least one group with 0 data update levels in |
|
| 119 |
### .df_row/df/.ref_group to avoid problems for contrast |
|
| 120 | ||
| 121 |
#### by making updates to .df_row/df/.ref_group, situation 3 and 4 could be used together with general case |
|
| 122 | ||
| 123 | 71x |
.df_row_trtlevels <- unique(.df_row[[trt_var]]) |
| 124 | ||
| 125 | 71x |
if (NROW(.df_row_trtlevels) == 0) {
|
| 126 |
### No data at all |
|
| 127 | ! |
x_stats <- NULL |
| 128 | 71x |
} else if (NROW(df) == 0 || NROW(.df_row_trtlevels) == 1) {
|
| 129 |
### current column no data/less than 2 treatment levels |
|
| 130 | ||
| 131 | ! |
x_stats <- list() |
| 132 | ! |
x_stats[[mystat1[1]]] <- rep(NA, 3) |
| 133 | ! |
x_stats[[mystat1[2]]] <- rep(NA, 3) |
| 134 |
} else {
|
|
| 135 |
### ancova situation, and some updates to prevent problems with sparse data |
|
| 136 | 71x |
if ((!ctrl_grp %in% .df_row_trtlevels)) {
|
| 137 |
# LS means for current group can be estimated from model, but not difference in LS means set .ref_group |
|
| 138 |
# to NULL to proceed with s_summarize_ancova_j function update to underlying s_ancova_j to cover NULL |
|
| 139 |
# ref_group in call |
|
| 140 | ! |
.ref_group <- NULL |
| 141 |
} |
|
| 142 | 71x |
if (NROW(.df_row_trtlevels) < length(levels(.df_row[[trt_var]]))) {
|
| 143 |
# missing levels need to be removed from the factor, in order to have the correct estimates, and avoid |
|
| 144 |
# errors with underlying tern:::s_ancova function |
|
| 145 | ! |
.df_row[[trt_var]] <- droplevels(.df_row[[trt_var]]) |
| 146 | ! |
df[[trt_var]] <- factor(as.character(df[[trt_var]]), levels = levels(.df_row[[trt_var]])) |
| 147 | ! |
.ref_group[[trt_var]] <- factor(as.character(.ref_group[[trt_var]]), levels = levels(.df_row[[trt_var]])) |
| 148 |
} |
|
| 149 | ||
| 150 | 71x |
x_stats <- s_summarize_ancova_j( |
| 151 | 71x |
df = df, |
| 152 | 71x |
.var = .var, |
| 153 | 71x |
.ref_group = .ref_group, |
| 154 | 71x |
.in_ref_col = .in_ref_col, |
| 155 | 71x |
.df_row = .df_row, |
| 156 | 71x |
conf_level = conf_level, |
| 157 | 71x |
interaction_y = interaction_y, |
| 158 | 71x |
interaction_item = interaction_item, |
| 159 | 71x |
variables = variables, |
| 160 | 71x |
weights_emmeans = weights_emmeans, |
| 161 | 71x |
method_combo = method_combo, |
| 162 | 71x |
weights_combo = weights_combo |
| 163 |
) |
|
| 164 |
} |
|
| 165 |
} |
|
| 166 | ||
| 167 | 244x |
y <- list(mean_ci_3d = x_stats[[mystat1[1]]], mean_diff_with_ci = x_stats[[mystat1[2]]]) |
| 168 | 244x |
return(y) |
| 169 |
} |
|
| 170 | ||
| 171 | ||
| 172 |
#' @name a_summarize_aval_chg_diff_j |
|
| 173 |
#' |
|
| 174 |
#' @title Analysis function 3-column presentation |
|
| 175 |
#' |
|
| 176 |
#' @description Analysis functions to produce a 1-row summary presented in |
|
| 177 |
#' a 3-column layout in the columns (column 1 = N, column 2 = Value, column 3 = Change).\cr |
|
| 178 |
#' In the difference columns, only 1 column will be presented : difference + CI\cr |
|
| 179 |
#' When ancova = `TRUE`, the presented statistics will be based on ANCOVA method (`s_summarize_ancova_j`).\cr |
|
| 180 |
#' mean and ci (both for Value (column 2) and CHG (column 3)) using statistic `lsmean_ci`\cr |
|
| 181 |
#' mean and ci for the difference column are based on same ANCOVA model using statistic `lsmean_diff_with_ci`\cr |
|
| 182 |
#' When ancova = `FALSE`, descriptive statistics will be used instead.\cr |
|
| 183 |
#' In the difference column, the 2-sample t-test will be used. |
|
| 184 |
#' |
|
| 185 |
#' @details See Description |
|
| 186 |
#' |
|
| 187 |
#' @inheritParams proposal_argument_convention |
|
| 188 |
#' @inheritParams s_ancova_j |
|
| 189 |
#' @param denom (`string`)\cr choice of denominator for proportions. Options are: |
|
| 190 |
#' * `N`: number of records in this column/row split. |
|
| 191 |
#' \cr There is no check in place that the current split only has one record per subject. |
|
| 192 |
#' Users should be careful with this. |
|
| 193 |
#' * `.N_col`: number of records in this column intersection (based on alt_counts_df dataset) |
|
| 194 |
#' \cr (when alt_counts_df is a single record per subjects, this will match number of subjects) |
|
| 195 |
#' |
|
| 196 |
#' @param d (default = 1) \cr choice of Decimal precision. |
|
| 197 |
#' Note that one extra precision will be added, as means are presented. |
|
| 198 |
#' \cr Options are: |
|
| 199 |
#' * numerical(1) |
|
| 200 |
#' * variable name containing information on the precision, this variable |
|
| 201 |
#' should be available on input dataset. The content of this variable should |
|
| 202 |
#' then be an integer. |
|
| 203 |
#' |
|
| 204 |
#' @param ancova (`logical`)\cr If FALSE, only descriptive methods will be used. \cr |
|
| 205 |
#' If TRUE, ANCOVA methods will be used for each of the columns : AVAL, CHG, DIFF. \cr |
|
| 206 |
#' @param comp_btw_group (`logical`)\cr If TRUE, comparison between groups will be performed. |
|
| 207 |
#' \cr When ancova = FALSE, the estimate of between group difference (on CHG) |
|
| 208 |
#' will be based upon a two-sample t-test. |
|
| 209 |
#' \cr When ancova = TRUE, the same ANCOVA model will be used for the estimate of between group difference (on CHG). |
|
| 210 |
#' |
|
| 211 |
#' @param interaction_y (`character`)\cr Will be passed onto the `tern` function `s_ancova`, when ancova = TRUE. |
|
| 212 |
#' @param interaction_item (`character`)\cr Will be passed onto the `tern` function `s_ancova`, when ancova = TRUE. |
|
| 213 |
#' @param conf_level (`proportion`)\cr Confidence level of the interval |
|
| 214 |
#' @param variables (named list of strings)\cr |
|
| 215 |
#' list of additional analysis variables, with expected elements: |
|
| 216 |
#' * arm (string)\cr |
|
| 217 |
#' group variable, for which the covariate adjusted means of multiple groups will be summarized. |
|
| 218 |
#' Specifically, the first level of arm variable is taken as the reference group. |
|
| 219 |
#' * covariates (character)\cr |
|
| 220 |
#' a vector that can contain single variable names (such as 'X1'), and/or interaction terms indicated by 'X1 * X2'. |
|
| 221 |
#' |
|
| 222 |
#' |
|
| 223 |
#' @param format_na_str (`string`)\cr |
|
| 224 |
#' |
|
| 225 |
#' @param indatavar (`string`)\cr If not null, variable name to extra subset |
|
| 226 |
#' incoming df to non-missing values of this variable. |
|
| 227 |
#' @param multivars (`string(3)`)\cr Variables names to use in 3-col layout. |
|
| 228 |
#' |
|
| 229 |
#' @param .stats (named `list`)\cr column statistics to select for the table. |
|
| 230 |
#' The following column names are to be used: `col1`, `col23`, `coldiff`.\cr |
|
| 231 |
#' For `col1`, the following stats can be specified.\cr |
|
| 232 |
#' For `col23`, only `mean_ci_3d` is available. When ancova = `TRUE` these are LS Means, otherwise, arithmetic means.\cr |
|
| 233 |
#' For `coldiff`, only `mean_diff_with_ci` is available. When ancova = `TRUE` these |
|
| 234 |
#' are LS difference in means, otherwise, difference in means based upon 2-sample t-test.\cr |
|
| 235 |
#' @param .formats (named `list`)\cr formats for the column statistics. `xx.d` style formats can be used. |
|
| 236 |
#' @param .formats_fun (named `list`)\cr formatting functions for the column |
|
| 237 |
#' statistics, to be applied after the conversion of `xx.d` style to the |
|
| 238 |
#' appropriate precision. |
|
| 239 |
#' |
|
| 240 |
#' @rdname a_summarize_aval_chg_diff_j |
|
| 241 |
#' @return A function that can be used in an analyze function call |
|
| 242 |
#' @export |
|
| 243 |
#' |
|
| 244 |
#' |
|
| 245 |
#' @examples |
|
| 246 |
#' library(dplyr) |
|
| 247 |
#' |
|
| 248 |
#' ADEG <- data.frame( |
|
| 249 |
#' STUDYID = c( |
|
| 250 |
#' "DUMMY", "DUMMY", "DUMMY", "DUMMY", "DUMMY", |
|
| 251 |
#' "DUMMY", "DUMMY", "DUMMY", "DUMMY", "DUMMY" |
|
| 252 |
#' ), |
|
| 253 |
#' USUBJID = c( |
|
| 254 |
#' "XXXXX01", "XXXXX02", "XXXXX03", "XXXXX04", "XXXXX05", |
|
| 255 |
#' "XXXXX06", "XXXXX07", "XXXXX08", "XXXXX09", "XXXXX10" |
|
| 256 |
#' ), |
|
| 257 |
#' TRT01A = c( |
|
| 258 |
#' "ARMA", "ARMA", "ARMA", "ARMA", "ARMA", "Placebo", |
|
| 259 |
#' "Placebo", "Placebo", "ARMA", "ARMA" |
|
| 260 |
#' ), |
|
| 261 |
#' PARAM = c("BP", "BP", "BP", "BP", "BP", "BP", "BP", "BP", "BP", "BP"),
|
|
| 262 |
#' AVISIT = c( |
|
| 263 |
#' "Visit 1", "Visit 1", "Visit 1", "Visit 1", "Visit 1", |
|
| 264 |
#' "Visit 1", "Visit 1", "Visit 1", "Visit 1", "Visit 1" |
|
| 265 |
#' ), |
|
| 266 |
#' AVAL = c(56, 78, 67, 87, 88, 93, 39, 87, 65, 55), |
|
| 267 |
#' CHG = c(2, 3, -1, 9, -2, 0, 6, -2, 5, 2) |
|
| 268 |
#' ) |
|
| 269 |
#' |
|
| 270 |
#' ADEG <- ADEG |> |
|
| 271 |
#' mutate( |
|
| 272 |
#' TRT01A = as.factor(TRT01A), |
|
| 273 |
#' STUDYID = as.factor(STUDYID) |
|
| 274 |
#' ) |
|
| 275 |
#' |
|
| 276 |
#' ADEG$colspan_trt <- factor(ifelse(ADEG$TRT01A == "Placebo", " ", "Active Study Agent"), |
|
| 277 |
#' levels = c("Active Study Agent", " ")
|
|
| 278 |
#' ) |
|
| 279 |
#' ADEG$rrisk_header <- "Risk Difference (%) (95% CI)" |
|
| 280 |
#' ADEG$rrisk_label <- paste(ADEG$TRT01A, paste("vs", "Placebo"))
|
|
| 281 |
#' |
|
| 282 |
#' colspan_trt_map <- create_colspan_map(ADEG, |
|
| 283 |
#' non_active_grp = "Placebo", |
|
| 284 |
#' non_active_grp_span_lbl = " ", |
|
| 285 |
#' active_grp_span_lbl = "Active Study Agent", |
|
| 286 |
#' colspan_var = "colspan_trt", |
|
| 287 |
#' trt_var = "TRT01A" |
|
| 288 |
#' ) |
|
| 289 |
#' ref_path <- c("colspan_trt", " ", "TRT01A", "Placebo")
|
|
| 290 |
#' |
|
| 291 |
#' lyt <- basic_table() |> |
|
| 292 |
#' split_cols_by( |
|
| 293 |
#' "colspan_trt", |
|
| 294 |
#' split_fun = trim_levels_to_map(map = colspan_trt_map) |
|
| 295 |
#' ) |> |
|
| 296 |
#' split_cols_by("TRT01A") |>
|
|
| 297 |
#' split_rows_by( |
|
| 298 |
#' "PARAM", |
|
| 299 |
#' label_pos = "topleft", |
|
| 300 |
#' split_label = "Blood Pressure", |
|
| 301 |
#' section_div = " ", |
|
| 302 |
#' split_fun = drop_split_levels |
|
| 303 |
#' ) |> |
|
| 304 |
#' split_rows_by( |
|
| 305 |
#' "AVISIT", |
|
| 306 |
#' label_pos = "topleft", |
|
| 307 |
#' split_label = "Study Visit", |
|
| 308 |
#' split_fun = drop_split_levels, |
|
| 309 |
#' child_labels = "hidden" |
|
| 310 |
#' ) |> |
|
| 311 |
#' split_cols_by_multivar( |
|
| 312 |
#' c("AVAL", "AVAL", "CHG"),
|
|
| 313 |
#' varlabels = c("n/N (%)", "Mean (CI)", "CFB (CI)")
|
|
| 314 |
#' ) |> |
|
| 315 |
#' split_cols_by("rrisk_header", nested = FALSE) |>
|
|
| 316 |
#' split_cols_by( |
|
| 317 |
#' "TRT01A", |
|
| 318 |
#' split_fun = remove_split_levels("Placebo"),
|
|
| 319 |
#' labels_var = "rrisk_label" |
|
| 320 |
#' ) |> |
|
| 321 |
#' split_cols_by_multivar(c("CHG"), varlabels = c(" ")) |>
|
|
| 322 |
#' analyze("STUDYID",
|
|
| 323 |
#' afun = a_summarize_aval_chg_diff_j, |
|
| 324 |
#' extra_args = list( |
|
| 325 |
#' format_na_str = "-", d = 0, |
|
| 326 |
#' ref_path = ref_path, variables = list(arm = "TRT01A", covariates = NULL) |
|
| 327 |
#' ) |
|
| 328 |
#' ) |
|
| 329 |
#' |
|
| 330 |
#' result <- build_table(lyt, ADEG) |
|
| 331 |
#' |
|
| 332 |
#' result |
|
| 333 |
#' @family Inclusion of ANCOVA Functions |
|
| 334 |
a_summarize_aval_chg_diff_j <- function( |
|
| 335 |
df, |
|
| 336 |
.df_row, |
|
| 337 |
.spl_context, |
|
| 338 |
ancova = FALSE, |
|
| 339 |
comp_btw_group = TRUE, |
|
| 340 |
ref_path = NULL, |
|
| 341 |
.N_col, |
|
| 342 |
denom = c("N", ".N_col"),
|
|
| 343 |
indatavar = NULL, |
|
| 344 |
d = 0, |
|
| 345 |
id = "USUBJID", |
|
| 346 |
interaction_y = FALSE, |
|
| 347 |
interaction_item = NULL, |
|
| 348 |
conf_level = 0.95, |
|
| 349 |
variables = list(arm = "TRT01A", covariates = NULL), |
|
| 350 |
format_na_str = "", |
|
| 351 |
.stats = list(col1 = "count_denom_frac", col23 = "mean_ci_3d", coldiff = "mean_diff_with_ci"), |
|
| 352 |
.formats = list(col1 = NULL, col23 = "xx.dx (xx.dx, xx.dx)", coldiff = "xx.dx (xx.dx, xx.dx)"), |
|
| 353 |
.formats_fun = list(col1 = jjcsformat_count_denom_fraction, col23 = jjcsformat_xx, coldiff = jjcsformat_xx), |
|
| 354 |
multivars = c("AVAL", "AVAL", "CHG"),
|
|
| 355 |
weights_emmeans = NULL, |
|
| 356 |
method_combo = c("contrasts", "collapse"),
|
|
| 357 |
weights_combo = NULL |
|
| 358 |
) {
|
|
| 359 | 344x |
checkmate::check_character(ref_path, min.len = 2L) |
| 360 | 344x |
checkmate::assert_true(length(ref_path) %% 2L == 0L) |
| 361 | ||
| 362 | 344x |
denom <- match.arg(denom) |
| 363 | 344x |
method_combo <- match.arg(method_combo) |
| 364 | ||
| 365 | 344x |
if (comp_btw_group && is.null(ref_path)) {
|
| 366 | ! |
stop("ref_path cannot be NULL, please specify it. See ?get_ref_info for details.")
|
| 367 |
} |
|
| 368 | ||
| 369 | 344x |
if (!(length(multivars) == 3)) {
|
| 370 | ! |
stop("argument multivars must be of length 3.")
|
| 371 |
} |
|
| 372 | ||
| 373 | 344x |
if (!(all(multivars %in% names(df)))) {
|
| 374 | ! |
stop("all variables specified in multivars must be available on input dataset df.")
|
| 375 |
} |
|
| 376 | ||
| 377 | 344x |
if (multivars[1] == multivars[2]) {
|
| 378 | 344x |
multivars[2] <- paste0(multivars[2], "._[[2]]_.") |
| 379 |
} |
|
| 380 | ||
| 381 |
if ( |
|
| 382 | 344x |
!is.list(.stats) || |
| 383 | 344x |
!all(c("col1", "col23", "coldiff") %in% names(.stats)) ||
|
| 384 | 344x |
!is.list(.formats) || |
| 385 | 344x |
!all(c("col1", "col23", "coldiff") %in% names(.formats)) ||
|
| 386 | 344x |
!is.list(.formats_fun) || |
| 387 | 344x |
!all(c("col1", "col23", "coldiff") %in% names(.formats_fun))
|
| 388 |
) {
|
|
| 389 | ! |
stop(paste( |
| 390 | ! |
"Issue a_summarize_aval_chg_diff_j \n.stats/.formats/.formats_fun must", |
| 391 | ! |
"be a list with names c(\"col1\", \"col23\", \"coldiff\")." |
| 392 |
)) |
|
| 393 |
} |
|
| 394 | ||
| 395 | 344x |
row_vars <- .spl_context$split |
| 396 | 344x |
row_val <- .spl_context$cur_col_split_val |
| 397 | ||
| 398 | 344x |
col_split <- .spl_context$cur_col_split |
| 399 | ||
| 400 |
## treatment group value/variable KEY assumption : treatment variable is the last split_col prior to |
|
| 401 |
## split_cols_by_multivar |
|
| 402 | 344x |
colvars <- col_split[[NROW(.spl_context)]] |
| 403 | 344x |
colvars_multivars <- which(colvars == "multivars") |
| 404 | 344x |
if (identical(colvars_multivars, integer(0))) {
|
| 405 | ! |
stop("Layout must at least contain a split_cols_by_multivar call.")
|
| 406 |
} |
|
| 407 | 344x |
if (colvars_multivars == 1) {
|
| 408 | ! |
stop("Layout must at least contain a split_cols_by prior to split_cols_by_multivar call.")
|
| 409 |
} |
|
| 410 |
## KEY assumption : treatment variable is the last split_col prior to split_cols_by_multivar |
|
| 411 | 344x |
trt_var <- .spl_context$cur_col_split[[NROW(.spl_context)]][colvars_multivars - 1] |
| 412 | 344x |
trt_val <- .spl_context$cur_col_split_val[[NROW(.spl_context)]][colvars_multivars - 1] |
| 413 | ||
| 414 | 344x |
mysplitlevel <- length(.spl_context$split) |
| 415 | 344x |
cur_lvl <- .spl_context$value[[mysplitlevel]] |
| 416 | 344x |
cur_col_id <- .spl_context$cur_col_id[[mysplitlevel]] |
| 417 | ||
| 418 | 344x |
cur_param <- .spl_context$value[[mysplitlevel - 1]] |
| 419 | ||
| 420 | 344x |
indiffcol <- grepl("difference", tolower(cur_col_id), fixed = TRUE)
|
| 421 | ||
| 422 |
# Early return if we're not comparing between groups but we're in a difference column |
|
| 423 | 344x |
if (!comp_btw_group && indiffcol) {
|
| 424 | ! |
return(rcell(NULL, format = NULL, label = cur_lvl, format_na_str = format_na_str)) |
| 425 |
} |
|
| 426 | ||
| 427 |
## variable that is analyzed |
|
| 428 | 344x |
last_val <- utils::tail(.spl_context$cur_col_split_val[[NROW(.spl_context)]], 1) |
| 429 | ||
| 430 |
# as AVAL will be utilized twice, the second call results in last_val == 'AVAL._[[2]]_.' take the first part of it |
|
| 431 |
# in flast_val (will become AVAL) flast_val <- stringr::str_split_1(last_val, pattern = '._')[1] |
|
| 432 | 344x |
flast_val <- unlist(strsplit(last_val, "._"))[1] |
| 433 | ||
| 434 | 344x |
x_stats <- NULL |
| 435 | 344x |
fmt_d <- NULL |
| 436 | ||
| 437 | 344x |
.in_ref_col <- FALSE |
| 438 | 344x |
.ref_group <- NULL |
| 439 | 344x |
ctrl_grp <- NULL |
| 440 | 344x |
if (comp_btw_group) {
|
| 441 | 290x |
checkmate::assert_true(identical(trt_var, ref_path[length(ref_path) - 1L])) |
| 442 | 290x |
ctrl_grp <- ref_path[length(ref_path)] |
| 443 | 290x |
stopifnot(ctrl_grp %in% levels(df[[trt_var]])) |
| 444 | ||
| 445 | 78x |
if (trt_val == ctrl_grp) .in_ref_col <- TRUE |
| 446 | ||
| 447 | 290x |
.ref_group <- .df_row[.df_row[[trt_var]] == ctrl_grp, ] |
| 448 |
} |
|
| 449 | ||
| 450 | 344x |
if (last_val == multivars[1] && !indiffcol) {
|
| 451 | 100x |
mystat <- .stats[["col1"]] |
| 452 | 100x |
if (is.null(.formats[["col1"]])) {
|
| 453 | 100x |
fmt <- .formats_fun[["col1"]] |
| 454 |
} else {
|
|
| 455 | ! |
fmt <- .formats[["col1"]] |
| 456 |
} |
|
| 457 | ||
| 458 | 100x |
x_stats <- s_aval_chg_col1( |
| 459 | 100x |
df = df, |
| 460 | 100x |
.var = flast_val, |
| 461 | 100x |
denom = denom, |
| 462 | 100x |
.N_col = .N_col, |
| 463 | 100x |
id = id, |
| 464 | 100x |
indatavar = indatavar |
| 465 |
) |
|
| 466 | 244x |
} else if (last_val %in% multivars[2:3]) {
|
| 467 | 244x |
x_stats <- s_aval_chg_col23_diff( |
| 468 | 244x |
df = df, |
| 469 | 244x |
.var = flast_val, |
| 470 | 244x |
.df_row = .df_row, |
| 471 | 244x |
.ref_group = .ref_group, |
| 472 | 244x |
.in_ref_col = .in_ref_col, |
| 473 | 244x |
ancova = ancova, |
| 474 | 244x |
interaction_y = interaction_y, |
| 475 | 244x |
interaction_item = interaction_item, |
| 476 | 244x |
conf_level = conf_level, |
| 477 | 244x |
variables = variables, |
| 478 | 244x |
trt_var = trt_var, |
| 479 | 244x |
ctrl_grp = ctrl_grp, |
| 480 | 244x |
cur_param = cur_param, |
| 481 | 244x |
cur_lvl = cur_lvl, |
| 482 | 244x |
weights_emmeans = weights_emmeans, |
| 483 | 244x |
method_combo = method_combo, |
| 484 | 244x |
weights_combo = weights_combo |
| 485 |
) |
|
| 486 | ||
| 487 | 244x |
if (comp_btw_group && indiffcol) {
|
| 488 | 44x |
mystat1 <- "coldiff" |
| 489 |
} else {
|
|
| 490 | 200x |
mystat1 <- "col23" |
| 491 |
} |
|
| 492 | 244x |
mystat <- .stats[[mystat1]] |
| 493 | ||
| 494 | 244x |
fmt_d <- .formats[[mystat1]] |
| 495 | 244x |
formatting_fun <- .formats_fun[[mystat1]] |
| 496 | ||
| 497 | 244x |
fmt <- format_xxd(fmt_d, d = d, .df_row = .df_row, formatting_fun = formatting_fun) |
| 498 |
} |
|
| 499 | 344x |
x_stats <- x_stats[[mystat]] |
| 500 | ||
| 501 | 344x |
ret <- rcell(x_stats, format = fmt, label = cur_lvl, format_na_str = format_na_str) |
| 502 | 344x |
return(ret) |
| 503 |
} |
| 1 |
#' Calculate Count and Percentage of the Maximum Level of an Ordered Factor per |
|
| 2 |
#' Subject. |
|
| 3 |
#' |
|
| 4 |
#' @description A formatted analysis function used as an `afun` in |
|
| 5 |
#' \link[rtables]{analyze} and as a `cfun` in \link[rtables]{summarize_row_groups}.
|
|
| 6 |
#' |
|
| 7 |
#' It computes count and proportion statistics for the maximum level of an |
|
| 8 |
#' ordered factor, `df[[.var]]`, for each unique subject in `df[[id]]`. |
|
| 9 |
#' Specifically, for each subject, the function identifies the highest level |
|
| 10 |
#' of `df[[.var]]`, producing one value per subject. |
|
| 11 |
#' Then, if `any_level = TRUE`, the function reports the total number of |
|
| 12 |
#' maximum values, excluding those specified in `any_level_exclude`. |
|
| 13 |
#' Otherwise, it tabulates the frequency of each maximum level across all |
|
| 14 |
#' subjects. |
|
| 15 |
#' |
|
| 16 |
#' This function is particularly useful for identifying the maximum severity of |
|
| 17 |
#' adverse events in a treatment sequence, where the most severe event |
|
| 18 |
#' experienced by a subject is used for reporting. |
|
| 19 |
#' |
|
| 20 |
#' @note The denominator for proportions is computed using the `denom_df` |
|
| 21 |
#' argument. This serves as a temporary workaround until the next version of |
|
| 22 |
#' `rtables` is released, which will support `.alt_count_df` for use in |
|
| 23 |
#' `afun`/`cfun`. |
|
| 24 |
#' |
|
| 25 |
#' @details For each unique subject, only the maximum level of the ordered factor |
|
| 26 |
#' `df[[.var]]` is included in the final count and percentage statistics. |
|
| 27 |
#' |
|
| 28 |
#' @inheritParams proposal_argument_convention |
|
| 29 |
#' @param .alt_df_full (`dataframe`)\cr A dataset used to compute the |
|
| 30 |
#' denominator for proportions. This is required when the same subject appears |
|
| 31 |
#' multiple times in the dataset due to treatment sequences. |
|
| 32 |
#' `colnames(.alt_df_full)` must be a superset of `id`. |
|
| 33 |
#' This argument gets populated by the rtables split machinery |
|
| 34 |
#' (see [rtables::additional_fun_params]). |
|
| 35 |
#' @param any_level (`flag`)\cr |
|
| 36 |
#' Should be set to `TRUE` when the function is used as a `cfun`. |
|
| 37 |
#' @param any_level_exclude (`character`)\cr |
|
| 38 |
#' Applicable only when `any_level = TRUE`. Specifies levels of `df[[.var]]` |
|
| 39 |
#' to exclude from the statistic (default = "Missing"). |
|
| 40 |
#' |
|
| 41 |
#' @returns A `RowsVerticalSection` object. |
|
| 42 |
#' |
|
| 43 |
#' @author WW |
|
| 44 |
#' |
|
| 45 |
#' @export |
|
| 46 |
#' |
|
| 47 |
#' @examples |
|
| 48 |
#' treatments <- factor(c("a", "b", "c"))
|
|
| 49 |
#' ae_severities <- c("Missing", "Mild", "Moderate", "Severe")
|
|
| 50 |
#' ae_severities <- ordered(ae_severities, levels = ae_severities) |
|
| 51 |
#' my_adae <- data.frame( |
|
| 52 |
#' ID = c(1, 1, 1, 2, 2, 3, 3, 3, 3, 4), |
|
| 53 |
#' TRT = factor(c("a", "b", "b", "b", "c", "c", "a", "c", "b", "b")),
|
|
| 54 |
#' AESEV = ae_severities[c(4L, 1L, 2L, 1L, 2L, 1L, 2L, 3L, 1L, 2L)] |
|
| 55 |
#' ) |
|
| 56 |
#' my_adsl <- data.frame( |
|
| 57 |
#' ID = rep(1:5, each = 3), |
|
| 58 |
#' TRT = factor(rep(c("a", "b", "c"), times = 5))
|
|
| 59 |
#' ) |
|
| 60 |
#' |
|
| 61 |
#' aesevall_spf <- make_combo_splitfun( |
|
| 62 |
#' nm = "AESEV_ALL", |
|
| 63 |
#' label = "Any AE", |
|
| 64 |
#' levels = NULL, |
|
| 65 |
#' ) |
|
| 66 |
#' |
|
| 67 |
#' lyt <- basic_table() |> |
|
| 68 |
#' split_cols_by("TRT") |>
|
|
| 69 |
#' add_overall_col("Total") |>
|
|
| 70 |
#' split_rows_by("AESEV", split_fun = aesevall_spf) |>
|
|
| 71 |
#' summarize_row_groups( |
|
| 72 |
#' "AESEV", |
|
| 73 |
#' cfun = a_maxlev, |
|
| 74 |
#' extra_args = list(id = "ID", any_level = TRUE) |
|
| 75 |
#' ) |> |
|
| 76 |
#' analyze( |
|
| 77 |
#' "AESEV", |
|
| 78 |
#' afun = a_maxlev, |
|
| 79 |
#' extra_args = list(id = "ID") |
|
| 80 |
#' ) |
|
| 81 |
#' build_table(lyt, my_adae, alt_counts_df = my_adsl) |
|
| 82 |
a_maxlev <- function(df, |
|
| 83 |
labelstr = NULL, |
|
| 84 |
.var, |
|
| 85 |
.spl_context, |
|
| 86 |
id = "USUBJID", |
|
| 87 |
.alt_df_full = NULL, |
|
| 88 |
any_level = FALSE, |
|
| 89 |
any_level_exclude = "Missing", |
|
| 90 |
...) {
|
|
| 91 | 29x |
checkmate::assert_factor(df[[.var]], ordered = TRUE, any.missing = FALSE) |
| 92 | 28x |
checkmate::assert_string(id) |
| 93 | 28x |
checkmate::assert_subset(id, choices = colnames(df)) |
| 94 | 28x |
checkmate::assert_subset(id, choices = colnames(.alt_df_full)) |
| 95 | 28x |
checkmate::assert_flag(any_level) |
| 96 | 28x |
checkmate::assert_character(any_level_exclude, any.missing = FALSE, unique = TRUE) |
| 97 | ||
| 98 | 28x |
varvec <- df[[.var]] |
| 99 | 28x |
levs <- levels(varvec) |
| 100 | 28x |
levs_lab <- if (any_level) {
|
| 101 | 16x |
labelstr |
| 102 |
} else {
|
|
| 103 | 12x |
levs |
| 104 |
} |
|
| 105 | ||
| 106 | 28x |
N_row <- nrow(df) |
| 107 | 28x |
count_pct <- if (N_row != 0) {
|
| 108 | 28x |
max_per_id <- tapply(varvec, df[[id]], max) |
| 109 | 28x |
max_per_id <- ordered(levs[max_per_id], levels = levs) # tapply loses levels of varvec. |
| 110 | ||
| 111 | 28x |
count_max <- if (any_level) {
|
| 112 | 16x |
sum(max_per_id != any_level_exclude) |
| 113 |
} else {
|
|
| 114 | 12x |
table(max_per_id) |
| 115 |
} |
|
| 116 | ||
| 117 | 28x |
cur_col <- subset(.alt_df_full, eval(.spl_context$cur_col_expr[[1]])) |
| 118 | 28x |
N <- length(unique(cur_col[[id]])) |
| 119 | 28x |
as.list(data.frame(rbind(count_max, count_max / N))) |
| 120 |
} else {
|
|
| 121 | ! |
rep(list(NA), length(levs_lab)) |
| 122 |
} |
|
| 123 | ||
| 124 | 28x |
count_pct <- setNames(count_pct, levs_lab) |
| 125 | ||
| 126 | 28x |
in_rows( |
| 127 | 28x |
.list = count_pct, |
| 128 | 28x |
.formats = jjcsformat_count_fraction, |
| 129 | 28x |
.format_na_strs = "-" |
| 130 |
) |
|
| 131 |
} |
| 1 |
#' @title Descriptive Statistics for Univariate Data with Optional Reference |
|
| 2 |
#' Comparison |
|
| 3 |
#' |
|
| 4 |
#' @description `r lifecycle::badge("experimental")`
|
|
| 5 |
#' |
|
| 6 |
#' Computes descriptive statistics for a single variable `df[[.var]]` using |
|
| 7 |
#' [tern::s_summary()], which dispatches type-specific methods depending on the |
|
| 8 |
#' S3 class of the input (e.g., character, factor, logical, numeric). |
|
| 9 |
#' |
|
| 10 |
#' Optionally, it computes estimates and associated inferential statistics for |
|
| 11 |
#' the difference in population means between two underlying distributions, |
|
| 12 |
#' based on observed samples `df[[.var]]` and `.ref_group[[.var]]` using |
|
| 13 |
#' [s_diff_means()]. |
|
| 14 |
#' These statistics are applicable only to numeric data. |
|
| 15 |
#' |
|
| 16 |
#' @inheritParams proposal_argument_convention |
|
| 17 |
#' @param .var (`character(1)`)\cr Name of the column in `df` containing the |
|
| 18 |
#' values for which statistics are computed. The variable type is handled by |
|
| 19 |
#' the corresponding methods of [tern::s_summary()]. |
|
| 20 |
#' If difference-in-means statistics are requested, `df[[.var]]` must be |
|
| 21 |
#' numeric. |
|
| 22 |
#' @param .stats (`character` or `NULL`)\cr Names of statistics to be computed. |
|
| 23 |
#' For numerical data, the names of supported statistics are listed via |
|
| 24 |
#' [tern::get_stats()] for statistics for a single variable and |
|
| 25 |
#' `junco_get_stats("diff_means")` for difference-in-means statistics.
|
|
| 26 |
#' If `NULL`, all available statistics for numerical data are computed. |
|
| 27 |
#' @param control (`list`)\cr List of control options passed to |
|
| 28 |
#' [tern::s_summary()]. |
|
| 29 |
#' If difference-in-means statistics are requested, `control$conf_level` |
|
| 30 |
#' specifies the confidence level of the interval. |
|
| 31 |
#' @param ... Additional named arguments passed to [s_diff_means()], except |
|
| 32 |
#' `df1`, `df2`, `.var`, and `conf.level`, which are already passed. |
|
| 33 |
#' |
|
| 34 |
#' @importFrom tern control_analyze_vars s_summary get_stats |
|
| 35 |
#' |
|
| 36 |
#' @returns A named `list` with the requested statistics. |
|
| 37 |
#' |
|
| 38 |
#' @author WW |
|
| 39 |
#' |
|
| 40 |
#' @export |
|
| 41 |
#' |
|
| 42 |
#' @examples |
|
| 43 |
#' df <- data.frame( |
|
| 44 |
#' USUBJID = c("X01", "X02", "X03", "X04", "X05"),
|
|
| 45 |
#' TRT01A = rep("ARM_A", 5),
|
|
| 46 |
#' PARAMCD = rep("SYSBP", 5),
|
|
| 47 |
#' AVISIT = rep("Visit 1", 5),
|
|
| 48 |
#' CHG = c(4, 1, -1, 9, -2) |
|
| 49 |
#' ) |
|
| 50 |
#' df |
|
| 51 |
#' |
|
| 52 |
#' rg <- data.frame( |
|
| 53 |
#' USUBJID = c("X06", "X07", "X08", "X09", "X10"),
|
|
| 54 |
#' TRT01A = rep("Placebo", 5),
|
|
| 55 |
#' PARAMCD = rep("SYSBP", 5),
|
|
| 56 |
#' AVISIT = rep("Visit 1", 5),
|
|
| 57 |
#' CHG = c(-2, 6, -2, 5, 2) |
|
| 58 |
#' ) |
|
| 59 |
#' rg |
|
| 60 |
#' |
|
| 61 |
#' .stats <- c("n", "mean_sd", "diff_means_n1", "diff_means_n2", "diff_means_est_ci")
|
|
| 62 |
#' |
|
| 63 |
#' # With reference group. |
|
| 64 |
#' s_summary_diff(df, "CHG", .stats, rg) |
|
| 65 |
#' |
|
| 66 |
#' # Using df as reference. |
|
| 67 |
#' s_summary_diff(df, "CHG", .stats, df, .in_ref_col = TRUE) |
|
| 68 |
#' |
|
| 69 |
s_summary_diff <- function(df, |
|
| 70 |
.var, |
|
| 71 |
.stats = NULL, |
|
| 72 |
.ref_group = NULL, |
|
| 73 |
.in_ref_col = FALSE, |
|
| 74 |
control = tern::control_analyze_vars(), |
|
| 75 |
...) {
|
|
| 76 | ! |
checkmate::assert_data_frame(df, null.ok = FALSE) |
| 77 | ! |
checkmate::assert_string(.var) |
| 78 | ! |
checkmate::assert_names(colnames(df), must.include = .var) |
| 79 | ! |
checkmate::assert_character( |
| 80 | ! |
x = .stats, any.missing = FALSE, min.len = 1, unique = TRUE, null.ok = TRUE |
| 81 |
) |
|
| 82 | ! |
checkmate::assert_flag(.in_ref_col, null.ok = FALSE) |
| 83 | ! |
checkmate::assert_list(control, null.ok = FALSE) |
| 84 | ||
| 85 | ! |
if (is.null(.stats)) {
|
| 86 | ! |
.stats <- unique(c(tern::get_stats(), junco_get_stats("diff_means")))
|
| 87 |
} |
|
| 88 | ||
| 89 | ! |
y <- tern::s_summary(df[[.var]], control = control) |
| 90 | ||
| 91 | ! |
.stats_diff <- .stats[startsWith(.stats, "diff_means_")] |
| 92 | ! |
if (length(.stats_diff) >= 1) {
|
| 93 | ! |
dm <- if (.in_ref_col) {
|
| 94 | ! |
setNames(vector(mode = "list", length = length(.stats_diff)), .stats_diff) |
| 95 |
} else {
|
|
| 96 | ! |
if (!checkmate::test_data_frame(.ref_group, null.ok = FALSE)) {
|
| 97 | ! |
stop(".ref_group must be a data.frame for diff_means_* stats when .in_ref_col = FALSE")
|
| 98 |
} |
|
| 99 | ! |
checkmate::assert_names(names(control), must.include = "conf_level") |
| 100 | ! |
s_diff_means(df, .ref_group, .var, conf.level = control$conf_level, ...) |
| 101 |
} |
|
| 102 | ! |
y <- c(y, dm) |
| 103 |
} |
|
| 104 | ||
| 105 | ! |
y[.stats] |
| 106 |
} |
| 1 |
#' Analysis of Covariance |
|
| 2 |
#' |
|
| 3 |
#' Performs an analysis of covariance between two groups returning the estimated |
|
| 4 |
#' "treatment effect" (i.e. the contrast between the two treatment groups) and |
|
| 5 |
#' the least square means estimates in each group. |
|
| 6 |
#' |
|
| 7 |
#' @param data A `data.frame` containing the data to be used in the model. |
|
| 8 |
#' @param vars A `vars` object as generated by the set_vars() function from the rbmi package. Only the `group`, |
|
| 9 |
#' `visit`, `outcome` and `covariates` elements are required. See details. |
|
| 10 |
#' @param visits An optional character vector specifying which visits to |
|
| 11 |
#' fit the ancova model at. If `NULL`, a separate ancova model will be fit to the |
|
| 12 |
#' outcomes for each visit (as determined by `unique(data[[vars$visit]])`). |
|
| 13 |
#' See details. |
|
| 14 |
#' @param weights Character, either `"counterfactual"` (default), `"equal"`, |
|
| 15 |
#' `"proportional_em"` or `"proportional"`. |
|
| 16 |
#' Specifies the weighting strategy to be used when calculating the lsmeans. |
|
| 17 |
#' See the weighting section for more details. |
|
| 18 |
#' |
|
| 19 |
#' @details |
|
| 20 |
#' The function works as follows: |
|
| 21 |
#' |
|
| 22 |
#' 1. Select the first value from `visits`. |
|
| 23 |
#' 2. Subset the data to only the observations that occurred on this visit. |
|
| 24 |
#' 3. Fit a linear model as `vars$outcome ~ vars$group + vars$covariates`. |
|
| 25 |
#' 4. Extract the "treatment effect" & least square means for each treatment group. |
|
| 26 |
#' 5. Repeat points 2-3 for all other values in `visits`. |
|
| 27 |
#' |
|
| 28 |
#' If no value for `visits` is provided then it will be set to |
|
| 29 |
#' `unique(data[[vars$visit]])`. |
|
| 30 |
#' |
|
| 31 |
#' In order to meet the formatting standards set by [rbmi_analyse()] the results will be collapsed |
|
| 32 |
#' into a single list suffixed by the visit name, e.g.: |
|
| 33 |
#' ``` |
|
| 34 |
#' list( |
|
| 35 |
#' var_visit_1 = list(est = ...), |
|
| 36 |
#' trt_B_visit_1 = list(est = ...), |
|
| 37 |
#' lsm_A_visit_1 = list(est = ...), |
|
| 38 |
#' lsm_B_visit_1 = list(est = ...), |
|
| 39 |
#' var_visit_2 = list(est = ...), |
|
| 40 |
#' trt_B_visit_2 = list(est = ...), |
|
| 41 |
#' lsm_A_visit_2 = list(est = ...), |
|
| 42 |
#' lsm_B_visit_2 = list(est = ...), |
|
| 43 |
#' ... |
|
| 44 |
#' ) |
|
| 45 |
#' ``` |
|
| 46 |
#' Please note that "trt" refers to the treatment effects, and "lsm" refers to the least |
|
| 47 |
#' square mean results. In the above example `vars$group` has two factor levels A and B. |
|
| 48 |
#' The new "var" refers to the model estimated variance of the residuals. |
|
| 49 |
#' |
|
| 50 |
#' If you want to include interaction terms in your model this can be done |
|
| 51 |
#' by providing them to the `covariates` argument of the set_vars() function from the rbmi package |
|
| 52 |
#' e.g. `set_vars(covariates = c("sex*age"))`.
|
|
| 53 |
#' |
|
| 54 |
#' @return a list of variance (`var_*`), treatment effect (`trt_*`), and |
|
| 55 |
#' least square mean (`lsm_*`) estimates for each visit, organized as |
|
| 56 |
#' described in Details above. |
|
| 57 |
#' |
|
| 58 |
#' @note These functions have the `rbmi_` prefix to distinguish them from the corresponding |
|
| 59 |
#' `rbmi` package functions, from which they were copied from. Additional features here |
|
| 60 |
#' include: |
|
| 61 |
#' |
|
| 62 |
#' * Support for more than two treatment groups. |
|
| 63 |
#' * Variance estimates are returned. |
|
| 64 |
#' |
|
| 65 |
#' @seealso [rbmi_analyse()] |
|
| 66 |
#' @seealso [stats::lm()] |
|
| 67 |
#' @seealso The set_vars() function from the rbmi package |
|
| 68 |
#' @export |
|
| 69 |
rbmi_ancova <- function( |
|
| 70 |
data, |
|
| 71 |
vars, |
|
| 72 |
visits = NULL, |
|
| 73 |
weights = c("counterfactual", "equal", "proportional_em", "proportional")
|
|
| 74 |
) {
|
|
| 75 | 4x |
outcome <- vars[["outcome"]] |
| 76 | 4x |
group <- vars[["group"]] |
| 77 | 4x |
covariates <- vars[["covariates"]] |
| 78 | 4x |
visit <- vars[["visit"]] |
| 79 | 4x |
weights <- match.arg(weights) |
| 80 | ||
| 81 | 4x |
expected_vars <- c( |
| 82 | 4x |
utils::getFromNamespace("extract_covariates", "rbmi")(covariates),
|
| 83 | 4x |
outcome, |
| 84 | 4x |
group |
| 85 |
) |
|
| 86 | ||
| 87 | 4x |
if (is.null(visits)) {
|
| 88 | 1x |
visits <- unique(data[[visit]]) |
| 89 |
} |
|
| 90 | ||
| 91 | 4x |
res <- lapply( |
| 92 | 4x |
visits, |
| 93 | 4x |
function(x) {
|
| 94 | 6x |
data2 <- data[data[[visit]] == x, ] |
| 95 | 6x |
res <- rbmi_ancova_single(data2, outcome, group, covariates, weights) |
| 96 | 6x |
names(res) <- paste0(names(res), "_", x) |
| 97 | 6x |
return(res) |
| 98 |
} |
|
| 99 |
) |
|
| 100 | 4x |
return(unlist(res, recursive = FALSE)) |
| 101 |
} |
|
| 102 | ||
| 103 | ||
| 104 |
#' Implements an Analysis of Covariance (ANCOVA) |
|
| 105 |
#' |
|
| 106 |
#' @description |
|
| 107 |
#' Performance analysis of covariance. See [rbmi_ancova()] for full details. |
|
| 108 |
#' |
|
| 109 |
#' @param outcome string, the name of the outcome variable in `data`. |
|
| 110 |
#' @param group string, the name of the group variable in `data`. |
|
| 111 |
#' @param covariates character vector containing the name of any additional covariates |
|
| 112 |
#' to be included in the model as well as any interaction terms. |
|
| 113 |
#' |
|
| 114 |
#' @inheritParams rbmi_ancova |
|
| 115 |
#' |
|
| 116 |
#' @details |
|
| 117 |
#' - `group` must be a factor variable with only 2 levels. |
|
| 118 |
#' - `outcome` must be a continuous numeric variable. |
|
| 119 |
#' @export |
|
| 120 |
#' @return a list containing `var` with variance estimates as well as |
|
| 121 |
#' `trt_*` and `lsm_*` entries. See [rbmi_ancova()] for full details. |
|
| 122 |
#' @examples |
|
| 123 |
#' |
|
| 124 |
#' iris2 <- iris[iris$Species %in% c("versicolor", "virginica"), ]
|
|
| 125 |
#' iris2$Species <- factor(iris2$Species) |
|
| 126 |
#' rbmi_ancova_single(iris2, "Sepal.Length", "Species", c("Petal.Length * Petal.Width"))
|
|
| 127 |
#' |
|
| 128 |
#' |
|
| 129 |
#' @seealso [rbmi_ancova()] |
|
| 130 |
rbmi_ancova_single <- function( |
|
| 131 |
data, |
|
| 132 |
outcome, |
|
| 133 |
group, |
|
| 134 |
covariates, |
|
| 135 |
weights = c("counterfactual", "equal", "proportional_em", "proportional")
|
|
| 136 |
) {
|
|
| 137 | 7x |
checkmate::assert_string(outcome) |
| 138 | 7x |
checkmate::assert_string(group) |
| 139 | 7x |
weights <- match.arg(weights) |
| 140 | ||
| 141 | 7x |
checkmate::assert_factor(data[[group]], n.levels = 2L) |
| 142 | 7x |
checkmate::assert_numeric(data[[outcome]]) |
| 143 | 7x |
data2 <- data[, c( |
| 144 | 7x |
utils::getFromNamespace("extract_covariates", "rbmi")(covariates),
|
| 145 | 7x |
outcome, |
| 146 | 7x |
group |
| 147 |
)] |
|
| 148 | ||
| 149 | 7x |
frm <- utils::getFromNamespace("as_simple_formula", "rbmi")(
|
| 150 | 7x |
outcome, |
| 151 | 7x |
c(group, covariates) |
| 152 |
) |
|
| 153 | ||
| 154 | 7x |
mod <- stats::lm(formula = frm, data = data2) |
| 155 | ||
| 156 | 7x |
args <- list(model = mod, .weights = weights) |
| 157 | ||
| 158 | 7x |
grp_levels <- levels(data2[[group]]) |
| 159 | ||
| 160 | 7x |
all_lsm <- lapply( |
| 161 | 7x |
grp_levels, |
| 162 | 7x |
\(x) {
|
| 163 | 14x |
do.call( |
| 164 | 14x |
utils::getFromNamespace("lsmeans", "rbmi"),
|
| 165 | 14x |
c(args, stats::setNames(x, group)) |
| 166 |
) |
|
| 167 |
} |
|
| 168 |
) |
|
| 169 | 7x |
names(all_lsm) <- paste0("lsm_", grp_levels)
|
| 170 | ||
| 171 | 7x |
var_est <- summary(mod)$sigma^2 |
| 172 | 7x |
var_df <- stats::df.residual(mod) |
| 173 | ||
| 174 | 7x |
all_trt <- lapply( |
| 175 | 7x |
paste0(group, grp_levels[-1]), |
| 176 | 7x |
\(x) {
|
| 177 | 7x |
list( |
| 178 | 7x |
est = stats::coef(mod)[x], |
| 179 | 7x |
se = sqrt(stats::vcov(mod)[x, x]), |
| 180 | 7x |
df = var_df |
| 181 |
) |
|
| 182 |
} |
|
| 183 |
) |
|
| 184 | 7x |
names(all_trt) <- paste0("trt_", grp_levels[-1])
|
| 185 | ||
| 186 | 7x |
var_res <- list( |
| 187 | 7x |
var = list( |
| 188 | 7x |
est = var_est, |
| 189 |
# Reference: Davison, Statistical Methods, p. 371 (just above 8.3.2) |
|
| 190 | 7x |
se = var_est / sqrt(var_df / 2), |
| 191 | 7x |
df = var_df |
| 192 |
) |
|
| 193 |
) |
|
| 194 | ||
| 195 | 7x |
c(var_res, all_trt, all_lsm) |
| 196 |
} |
| 1 |
#' Get default statistical methods and their associated formats, labels, and indent modifiers |
|
| 2 |
#' |
|
| 3 |
#' @description `r lifecycle::badge("experimental")`
|
|
| 4 |
#' |
|
| 5 |
#' @note These functions have been copied from the `tern` package file |
|
| 6 |
#' `utils_default_stats_formats_labels.R` from GitHub development version 0.9.7.9017. |
|
| 7 |
#' Slight modifications have been applied to enhance functionality: |
|
| 8 |
#' |
|
| 9 |
#' * `tern_get_stats` added the `tern_stats` argument to avoid hardcoding within the function's body. |
|
| 10 |
#' * `tern_get_labels_from_stats` is more careful when receiving partial `labels_in` |
|
| 11 |
#' and partial `label_attr_from_stats`. |
|
| 12 |
#' |
|
| 13 |
#' Once these features are included in the `tern` package, this file could be removed from |
|
| 14 |
#' the `junco` package, and the functions could be used from the `tern` namespace directly. |
|
| 15 |
#' |
|
| 16 |
#' @name tern_default_stats_formats_labels |
|
| 17 |
NULL |
|
| 18 | ||
| 19 | ||
| 20 |
#' @keywords internal |
|
| 21 |
.split_std_from_custom_stats <- function(stats_in) {
|
|
| 22 | 290x |
out <- list(default_stats = NULL, custom_stats = NULL, all_stats = NULL) |
| 23 | 290x |
if (is.list(stats_in)) {
|
| 24 | 1x |
is_custom_fnc <- sapply(stats_in, is.function) |
| 25 | 1x |
checkmate::assert_list( |
| 26 | 1x |
stats_in[is_custom_fnc], |
| 27 | 1x |
types = "function", |
| 28 | 1x |
names = "named" |
| 29 |
) |
|
| 30 | 1x |
out[["custom_stats"]] <- stats_in[is_custom_fnc] |
| 31 | 1x |
out[["default_stats"]] <- unlist(stats_in[!is_custom_fnc]) |
| 32 | 1x |
all_stats <- names(stats_in) # to keep the order |
| 33 | 1x |
all_stats[!is_custom_fnc] <- out[["default_stats"]] |
| 34 | 1x |
out[["all_stats"]] <- all_stats |
| 35 |
} else {
|
|
| 36 | 289x |
out[["default_stats"]] <- out[["all_stats"]] <- stats_in |
| 37 |
} |
|
| 38 | 290x |
out |
| 39 |
} |
|
| 40 | ||
| 41 |
#' @keywords internal |
|
| 42 |
.apply_stat_functions <- function( |
|
| 43 |
default_stat_fnc, |
|
| 44 |
custom_stat_fnc_list, |
|
| 45 |
args_list) {
|
|
| 46 |
# Default checks |
|
| 47 | 294x |
checkmate::assert_function(default_stat_fnc) |
| 48 | 294x |
checkmate::assert_list( |
| 49 | 294x |
custom_stat_fnc_list, |
| 50 | 294x |
types = "function", |
| 51 | 294x |
null.ok = TRUE, |
| 52 | 294x |
names = "named" |
| 53 |
) |
|
| 54 | 294x |
checkmate::assert_list(args_list) |
| 55 | ||
| 56 |
# Checking custom stats have same formals |
|
| 57 | 294x |
if (!is.null(custom_stat_fnc_list)) {
|
| 58 | 1x |
fundamental_call_to_data <- names(formals(default_stat_fnc))[[1]] |
| 59 | 1x |
for (fnc in custom_stat_fnc_list) {
|
| 60 | 1x |
if (!identical(names(formals(fnc))[[1]], fundamental_call_to_data)) {
|
| 61 | ! |
stop( |
| 62 | ! |
"The first parameter of a custom statistical function needs to be the same (it can be `df` or `x`) ", |
| 63 | ! |
"as the default statistical function. In this case your custom function has ", |
| 64 | ! |
names(formals(fnc))[[1]], |
| 65 | ! |
" as first parameter, while the default function has ", |
| 66 | ! |
fundamental_call_to_data, |
| 67 |
"." |
|
| 68 |
) |
|
| 69 |
} |
|
| 70 | 1x |
if (!any(names(formals(fnc)) == "...")) {
|
| 71 | ! |
stop( |
| 72 | ! |
"The custom statistical function needs to have `...` as a parameter to accept additional arguments. ", |
| 73 | ! |
"In this case your custom function does not have `...`." |
| 74 |
) |
|
| 75 |
} |
|
| 76 |
} |
|
| 77 |
} |
|
| 78 | ||
| 79 |
# Applying |
|
| 80 | 294x |
out_default <- do.call(default_stat_fnc, args = args_list) |
| 81 | 290x |
out_custom <- lapply( |
| 82 | 290x |
custom_stat_fnc_list, |
| 83 | 290x |
function(fnc) do.call(fnc, args = args_list) |
| 84 |
) |
|
| 85 | ||
| 86 |
# Merging |
|
| 87 | 290x |
c(out_default, out_custom) |
| 88 |
} |
|
| 89 | ||
| 90 |
#' @describeIn tern_default_stats_formats_labels Get statistics available for a given method |
|
| 91 |
#' group (analyze function). |
|
| 92 |
#' @keywords internal |
|
| 93 |
tern_get_stats <- function( |
|
| 94 |
method_groups = "analyze_vars_numeric", |
|
| 95 |
stats_in = NULL, |
|
| 96 |
custom_stats_in = NULL, |
|
| 97 |
add_pval = FALSE, |
|
| 98 |
tern_defaults = tern_default_stats) {
|
|
| 99 | 1747x |
checkmate::assert_character(method_groups) |
| 100 | 1747x |
checkmate::assert_character(stats_in, null.ok = TRUE) |
| 101 | 1747x |
checkmate::assert_character(custom_stats_in, null.ok = TRUE) |
| 102 | 1747x |
checkmate::assert_flag(add_pval) |
| 103 | ||
| 104 |
# Default is still numeric |
|
| 105 | 1747x |
if (any(method_groups == "analyze_vars")) {
|
| 106 | 3x |
method_groups[method_groups == "analyze_vars"] <- "analyze_vars_numeric" |
| 107 |
} |
|
| 108 | ||
| 109 | 1747x |
type_tmp <- ifelse(any(grepl("counts$", method_groups)), "counts", "numeric") # for pval checks
|
| 110 | ||
| 111 |
# Defaults for loop |
|
| 112 | 1747x |
out <- NULL |
| 113 | ||
| 114 |
# Loop for multiple method groups |
|
| 115 | 1747x |
for (mgi in method_groups) {
|
| 116 | 1748x |
if (mgi %in% names(tern_defaults)) {
|
| 117 | 1747x |
out_tmp <- tern_defaults[[mgi]] |
| 118 |
} else {
|
|
| 119 | 1x |
stop( |
| 120 | 1x |
"The selected method group (",
|
| 121 | 1x |
mgi, |
| 122 | 1x |
") has no default statistical method." |
| 123 |
) |
|
| 124 |
} |
|
| 125 | 1747x |
out <- unique(c(out, out_tmp)) |
| 126 |
} |
|
| 127 | ||
| 128 |
# Add custom stats |
|
| 129 | 1746x |
out <- c(out, custom_stats_in) |
| 130 | ||
| 131 |
# If you added pval to the stats_in you certainly want it |
|
| 132 | 1746x |
if (!is.null(stats_in) && any(grepl("^pval", stats_in))) {
|
| 133 | 34x |
stats_in_pval_value <- stats_in[grepl("^pval", stats_in)]
|
| 134 | ||
| 135 |
# Must be only one value between choices |
|
| 136 | 34x |
checkmate::assert_choice( |
| 137 | 34x |
stats_in_pval_value, |
| 138 | 34x |
c("pval", "pval_counts", "pvalue")
|
| 139 |
) |
|
| 140 | ||
| 141 |
# Mismatch with counts and numeric |
|
| 142 |
if ( |
|
| 143 | 33x |
any(grepl("counts", method_groups)) &&
|
| 144 | 33x |
stats_in_pval_value != "pval_counts" || |
| 145 | 33x |
any(grepl("numeric", method_groups)) && stats_in_pval_value != "pval"
|
| 146 |
) {
|
|
| 147 |
# nolint |
|
| 148 | 2x |
stop( |
| 149 | 2x |
"Inserted p-value (",
|
| 150 | 2x |
stats_in_pval_value, |
| 151 | 2x |
") is not valid for type ", |
| 152 | 2x |
type_tmp, |
| 153 | 2x |
". Use ", |
| 154 | 2x |
paste(ifelse(stats_in_pval_value == "pval", "pval_counts", "pval")), |
| 155 | 2x |
" instead." |
| 156 |
) |
|
| 157 |
} |
|
| 158 | ||
| 159 |
# Lets add it even if present (thanks to unique) |
|
| 160 | 31x |
add_pval <- TRUE |
| 161 |
} |
|
| 162 | ||
| 163 |
# Mainly used in "analyze_vars" but it could be necessary elsewhere |
|
| 164 | 1743x |
if (isTRUE(add_pval)) {
|
| 165 | 34x |
if (any(grepl("counts", method_groups))) {
|
| 166 | 1x |
out <- unique(c(out, "pval_counts")) |
| 167 |
} else {
|
|
| 168 | 33x |
out <- unique(c(out, "pval")) |
| 169 |
} |
|
| 170 |
} |
|
| 171 | ||
| 172 |
# Filtering for stats_in (character vector) |
|
| 173 | 1743x |
if (!is.null(stats_in)) {
|
| 174 | 908x |
out <- intersect(stats_in, out) # It orders them too |
| 175 |
} |
|
| 176 | ||
| 177 |
# If intersect did not find matches (and no pval?) -> error |
|
| 178 | 1743x |
if (length(out) == 0) {
|
| 179 | 2x |
stop( |
| 180 | 2x |
"The selected method group(s) (",
|
| 181 | 2x |
paste0(method_groups, collapse = ", "), |
| 182 |
")", |
|
| 183 | 2x |
" do not have the required default statistical methods:\n", |
| 184 | 2x |
paste0(stats_in, collapse = " ") |
| 185 |
) |
|
| 186 |
} |
|
| 187 | ||
| 188 | 1741x |
out |
| 189 |
} |
|
| 190 | ||
| 191 |
#' @describeIn tern_default_stats_formats_labels Get formats corresponding to a list of statistics. |
|
| 192 |
#' @keywords internal |
|
| 193 |
tern_get_formats_from_stats <- function( |
|
| 194 |
stats, |
|
| 195 |
formats_in = NULL, |
|
| 196 |
levels_per_stats = NULL, |
|
| 197 |
tern_defaults = tern_default_formats) {
|
|
| 198 | 1051x |
checkmate::assert_character(stats, min.len = 1) |
| 199 |
# It may be a list if there is a function in the formats |
|
| 200 | 1051x |
if (checkmate::test_list(formats_in, null.ok = TRUE)) {
|
| 201 | 1043x |
checkmate::assert_list(formats_in, null.ok = TRUE) |
| 202 |
# Or it may be a vector of characters |
|
| 203 |
} else {
|
|
| 204 | 8x |
checkmate::assert_character(formats_in, null.ok = TRUE) |
| 205 |
} |
|
| 206 | 1051x |
checkmate::assert_list(levels_per_stats, null.ok = TRUE) |
| 207 | ||
| 208 |
# If unnamed formats given as formats_in and same number of stats, use one format per stat |
|
| 209 |
if ( |
|
| 210 | 1051x |
!is.null(formats_in) && |
| 211 | 1051x |
length(formats_in) == length(stats) && |
| 212 | 1051x |
is.null(names(formats_in)) && |
| 213 | 1051x |
is.null(levels_per_stats) |
| 214 |
) {
|
|
| 215 | 3x |
out <- as.list(formats_in) |> stats::setNames(stats) |
| 216 | 3x |
return(out) |
| 217 |
} |
|
| 218 | ||
| 219 |
# If levels_per_stats not given, assume one row per statistic |
|
| 220 | 356x |
if (is.null(levels_per_stats)) levels_per_stats <- as.list(stats) |> stats::setNames(stats) |
| 221 | ||
| 222 |
# Apply custom formats |
|
| 223 | 1048x |
out <- .fill_in_vals_by_stats(levels_per_stats, formats_in, tern_defaults) |
| 224 | ||
| 225 |
# Default to NULL if no format |
|
| 226 | 1048x |
case_input_is_not_stat <- unlist(out, use.names = FALSE) == unlist(levels_per_stats, use.names = FALSE) |
| 227 | 1048x |
out[names(out) == out | case_input_is_not_stat] <- list(NULL) |
| 228 | ||
| 229 | 1048x |
out |
| 230 |
} |
|
| 231 | ||
| 232 |
#' @describeIn tern_default_stats_formats_labels Get labels corresponding to a list of statistics. |
|
| 233 |
#' @keywords internal |
|
| 234 |
tern_get_labels_from_stats <- function( |
|
| 235 |
stats, |
|
| 236 |
labels_in = NULL, |
|
| 237 |
levels_per_stats = NULL, |
|
| 238 |
label_attr_from_stats = NULL, |
|
| 239 |
tern_defaults = tern_default_labels) {
|
|
| 240 | 1115x |
checkmate::assert_character(stats, min.len = 1) |
| 241 | ||
| 242 |
# Modification: |
|
| 243 |
# If any label_attr_from_stats is available and valid, save in labels_in |
|
| 244 |
# if not specified there already (so don't overwrite labels_in). |
|
| 245 | 1115x |
if (!is.null(label_attr_from_stats)) {
|
| 246 | 344x |
valid_label_attr_from_stats <- label_attr_from_stats[ |
| 247 | 344x |
nzchar(label_attr_from_stats) & |
| 248 | 344x |
!sapply(label_attr_from_stats, is.null) & |
| 249 | 344x |
!is.na(label_attr_from_stats) |
| 250 |
] |
|
| 251 | 344x |
if (length(valid_label_attr_from_stats)) {
|
| 252 | 316x |
do_save <- setdiff( |
| 253 | 316x |
names(valid_label_attr_from_stats), |
| 254 | 316x |
names(labels_in) |
| 255 |
) |
|
| 256 | 316x |
labels_in <- c(labels_in, valid_label_attr_from_stats[do_save]) |
| 257 |
} |
|
| 258 |
} |
|
| 259 | ||
| 260 |
# It may be a list |
|
| 261 | 1115x |
if (checkmate::test_list(labels_in, null.ok = TRUE)) {
|
| 262 | 845x |
checkmate::assert_list(labels_in, null.ok = TRUE) |
| 263 |
# Or it may be a vector of characters |
|
| 264 |
} else {
|
|
| 265 | 270x |
checkmate::assert_character(labels_in, null.ok = TRUE) |
| 266 |
} |
|
| 267 | 1115x |
checkmate::assert_list(levels_per_stats, null.ok = TRUE) |
| 268 | ||
| 269 |
# If unnamed labels given as labels_in and same number of stats, use one label per stat |
|
| 270 |
if ( |
|
| 271 | 1115x |
!is.null(labels_in) && |
| 272 | 1115x |
length(labels_in) == length(stats) && |
| 273 | 1115x |
is.null(names(labels_in)) && |
| 274 | 1115x |
is.null(levels_per_stats) |
| 275 |
) {
|
|
| 276 | ! |
out <- as.list(labels_in) |> stats::setNames(stats) |
| 277 | ! |
return(out) |
| 278 |
} |
|
| 279 | ||
| 280 |
# If levels_per_stats not given, assume one row per statistic |
|
| 281 | 352x |
if (is.null(levels_per_stats)) levels_per_stats <- as.list(stats) |> stats::setNames(stats) |
| 282 | ||
| 283 |
# Apply custom labels |
|
| 284 | 1115x |
out <- .fill_in_vals_by_stats(levels_per_stats, labels_in, tern_defaults) |
| 285 | 1115x |
out |
| 286 |
} |
|
| 287 | ||
| 288 |
# Function to loop over each stat and levels to set correct values |
|
| 289 |
.fill_in_vals_by_stats <- function(levels_per_stats, user_in, tern_defaults) {
|
|
| 290 | 3171x |
out <- list() |
| 291 | ||
| 292 | 3171x |
levels_per_stats_list <- NULL |
| 293 | 3171x |
for (stat_i in names(levels_per_stats)) {
|
| 294 |
# Get all levels of the statistic |
|
| 295 | 7344x |
all_lvls <- levels_per_stats[[stat_i]] |
| 296 | ||
| 297 | 7344x |
if ((length(all_lvls) == 1 && all_lvls == stat_i) || is.null(all_lvls)) {
|
| 298 |
# One row per statistic |
|
| 299 | 4641x |
out[[stat_i]] <- if (stat_i %in% names(user_in)) {
|
| 300 |
# 1. Check for stat_i in user input |
|
| 301 | 1519x |
user_in[[stat_i]] |
| 302 | 4641x |
} else if (stat_i %in% names(tern_defaults)) {
|
| 303 |
# 2. Check for stat_i in tern defaults |
|
| 304 | 2941x |
tern_defaults[[stat_i]] |
| 305 |
} else {
|
|
| 306 |
# 3. Otherwise stat_i |
|
| 307 | 181x |
stat_i |
| 308 |
} |
|
| 309 | 4641x |
levels_per_stats_list <- c(levels_per_stats_list, stat_i) |
| 310 |
} else {
|
|
| 311 |
# One row per combination of variable level and statistic |
|
| 312 |
# Loop over levels for each statistic |
|
| 313 | 2703x |
for (lev_i in all_lvls) {
|
| 314 |
# Construct row name (stat_i.lev_i) |
|
| 315 | 9047x |
row_nm <- paste(stat_i, lev_i, sep = ".") |
| 316 | ||
| 317 | 9047x |
out[[row_nm]] <- if (row_nm %in% names(user_in)) {
|
| 318 |
# 1. Check for stat_i.lev_i in user input |
|
| 319 | 1x |
user_in[[row_nm]] |
| 320 | 9047x |
} else if (lev_i %in% names(user_in)) {
|
| 321 |
# 2. Check for lev_i in user input |
|
| 322 | 626x |
user_in[[lev_i]] |
| 323 | 9047x |
} else if (stat_i %in% names(user_in)) {
|
| 324 |
# 3. Check for stat_i in user input |
|
| 325 | 1x |
user_in[[stat_i]] |
| 326 | 9047x |
} else if (lev_i %in% names(tern_defaults)) {
|
| 327 |
# 4. Check for lev_i in tern defaults (only used for labels) |
|
| 328 | 1200x |
tern_defaults[[lev_i]] |
| 329 | 9047x |
} else if (stat_i %in% names(tern_defaults)) {
|
| 330 |
# 5. Check for stat_i in tern defaults |
|
| 331 | 4786x |
tern_defaults[[stat_i]] |
| 332 |
} else {
|
|
| 333 |
# 6. Otherwise lev_i |
|
| 334 | 2433x |
lev_i |
| 335 |
} |
|
| 336 | 9047x |
levels_per_stats_list <- c(levels_per_stats_list, row_nm) |
| 337 |
} |
|
| 338 |
} |
|
| 339 |
} |
|
| 340 | ||
| 341 | 3171x |
if (any(duplicated(levels_per_stats_list))) {
|
| 342 |
# this is to cover the same stat more than once |
|
| 343 |
# in risk diff column more than one stat transformed into same riskdiff stat |
|
| 344 | 72x |
out <- out[levels_per_stats_list] |
| 345 |
} |
|
| 346 | ||
| 347 | 3171x |
out |
| 348 |
} |
|
| 349 | ||
| 350 |
#' @describeIn tern_default_stats_formats_labels Get row indent modifiers corresponding to a list of statistics/rows. |
|
| 351 |
#' @keywords internal |
|
| 352 |
tern_get_indents_from_stats <- function(stats, |
|
| 353 |
indents_in = NULL, |
|
| 354 |
levels_per_stats = NULL, |
|
| 355 |
tern_defaults = as.list(rep(0L, length(stats))) |> stats::setNames(stats)) {
|
|
| 356 | 1028x |
checkmate::assert_character(stats, min.len = 1) |
| 357 |
# It may be a list |
|
| 358 | 1028x |
if (checkmate::test_list(indents_in, null.ok = TRUE)) {
|
| 359 | 990x |
checkmate::assert_list(indents_in, null.ok = TRUE) |
| 360 |
# Or it may be a vector of integers |
|
| 361 |
} else {
|
|
| 362 | 38x |
checkmate::assert_integerish(indents_in, null.ok = TRUE) |
| 363 |
} |
|
| 364 | 1028x |
checkmate::assert_list(levels_per_stats, null.ok = TRUE) |
| 365 | ||
| 366 |
# If levels_per_stats not given, assume one row per statistic |
|
| 367 | 348x |
if (is.null(levels_per_stats)) levels_per_stats <- as.list(stats) |> stats::setNames(stats) |
| 368 | ||
| 369 |
# Single indentation level for all rows |
|
| 370 | 1028x |
if (is.null(names(indents_in)) && length(indents_in) == 1) {
|
| 371 | 20x |
out <- rep(indents_in, length(levels_per_stats |> unlist())) |
| 372 | 20x |
return(out) |
| 373 |
} |
|
| 374 | ||
| 375 |
# Apply custom indentation |
|
| 376 | 1008x |
out <- .fill_in_vals_by_stats(levels_per_stats, indents_in, tern_defaults) |
| 377 | 1008x |
out |
| 378 |
} |
|
| 379 | ||
| 380 |
# tern_default_labels ---- |
|
| 381 | ||
| 382 |
#' @describeIn tern_default_stats_formats_labels Named `character` vector of default labels for `tern`. |
|
| 383 |
#' This is only copied here from the latest GitHub version, because otherwise a tern test fails. |
|
| 384 |
#' |
|
| 385 |
#' @keywords internal |
|
| 386 |
tern_default_labels <- c( |
|
| 387 |
cv = "CV (%)", |
|
| 388 |
iqr = "IQR", |
|
| 389 |
geom_cv = "CV % Geometric Mean", |
|
| 390 |
geom_mean = "Geometric Mean", |
|
| 391 |
geom_mean_sd = "Geometric Mean (SD)", |
|
| 392 |
geom_mean_ci = "Geometric Mean 95% CI", |
|
| 393 |
geom_mean_ci_3d = "Geometric Mean (95% CI)", |
|
| 394 |
geom_sd = "Geometric SD", |
|
| 395 |
mad = "Median Absolute Deviation", |
|
| 396 |
max = "Maximum", |
|
| 397 |
mean = "Mean", |
|
| 398 |
mean_ci = "Mean 95% CI", |
|
| 399 |
mean_ci_3d = "Mean (95% CI)", |
|
| 400 |
mean_pval = "Mean p-value (H0: mean = 0)", |
|
| 401 |
mean_sd = "Mean (SD)", |
|
| 402 |
mean_sdi = "Mean -/+ 1xSD", |
|
| 403 |
mean_se = "Mean (SE)", |
|
| 404 |
mean_sei = "Mean -/+ 1xSE", |
|
| 405 |
median = "Median", |
|
| 406 |
median_ci = "Median 95% CI", |
|
| 407 |
median_ci_3d = "Median (95% CI)", |
|
| 408 |
median_range = "Median (Min - Max)", |
|
| 409 |
min = "Minimum", |
|
| 410 |
n = "n", |
|
| 411 |
n_blq = "n_blq", |
|
| 412 |
nonunique = "Number of events", |
|
| 413 |
pval = "p-value (t-test)", # Default for numeric |
|
| 414 |
pval_counts = "p-value (chi-squared test)", # Default for counts |
|
| 415 |
quantiles = "25% and 75%-ile", |
|
| 416 |
quantiles_lower = "25%-ile (95% CI)", |
|
| 417 |
quantiles_upper = "75%-ile (95% CI)", |
|
| 418 |
range = "Min - Max", |
|
| 419 |
range_censor = "Range (censored)", |
|
| 420 |
range_event = "Range (event)", |
|
| 421 |
rate = "Adjusted Rate", |
|
| 422 |
rate_ratio = "Adjusted Rate Ratio", |
|
| 423 |
sd = "SD", |
|
| 424 |
se = "SE", |
|
| 425 |
sum = "Sum", |
|
| 426 |
unique = "Number of patients with at least one event" |
|
| 427 |
) |
|
| 428 | ||
| 429 | ||
| 430 |
# utility function to get labels from x_stats label attribute (or name if no label present) |
|
| 431 |
#' @keywords internal |
|
| 432 |
junco_get_labels_from_attrib <- function(.stats, .labels, x_stats) {
|
|
| 433 | 114x |
.labels_out <- unlist(junco_get_labels_from_stats(.stats, .labels, setNames(as.list(.stats), .stats))) |
| 434 | 114x |
.labels_out <- .labels_out[.stats] |
| 435 | ||
| 436 | 114x |
if (!all(names(x_stats) %in% .stats)) {
|
| 437 |
# assumption: incoming x_stats has .stats in inner list |
|
| 438 |
# key assumption, all inner lists have same structure |
|
| 439 | 114x |
inner_names <- unique(lapply(x_stats, names)) |
| 440 | 114x |
if (length(inner_names) > 1 || !(all(inner_names[[1]] %in% .stats))) {
|
| 441 | ! |
stop("incorrect .stats/x_stats structure assumption")
|
| 442 |
} |
|
| 443 |
# now we can more safely take the first element to retrieve labels |
|
| 444 | 114x |
nms_stats_pre <- x_stats[[1]] |
| 445 |
} else {
|
|
| 446 | ! |
nms_stats_pre <- x_stats |
| 447 |
} |
|
| 448 | ||
| 449 | 114x |
nms_stats <- vapply(seq_along(nms_stats_pre), function(i) {
|
| 450 | 317x |
el <- nms_stats_pre[[i]] |
| 451 | 317x |
attr(el, "label") %||% names(nms_stats_pre)[i] |
| 452 | 114x |
}, character(1)) |
| 453 | ||
| 454 | 114x |
names(nms_stats) <- .stats |
| 455 | ||
| 456 | 114x |
.labels_out[.labels_out == .stats] <- nms_stats[.labels_out == .stats] |
| 457 | ||
| 458 | 114x |
.labels_out |
| 459 |
} |
| 1 |
#' @title Safe Wrapper for `stats::t.test()` |
|
| 2 |
#' |
|
| 3 |
#' @noRd |
|
| 4 |
#' |
|
| 5 |
#' @description `r lifecycle::badge("stable")`
|
|
| 6 |
#' |
|
| 7 |
#' A robust wrapper around [stats::t.test()] that prevents errors from |
|
| 8 |
#' interrupting execution. |
|
| 9 |
#' |
|
| 10 |
#' Instead of failing, the function returns an object of class `"htest"` |
|
| 11 |
#' containing `NA` values for inferential statistics together with the computed |
|
| 12 |
#' sample mean(s) and an informative error message embedded in the `method` |
|
| 13 |
#' field. |
|
| 14 |
#' |
|
| 15 |
#' This is particularly useful in pipelines, simulations, or batch analyses |
|
| 16 |
#' where occasional invalid inputs (e.g., constant vectors or insufficient |
|
| 17 |
#' observations) would otherwise stop execution. |
|
| 18 |
#' |
|
| 19 |
#' @details |
|
| 20 |
#' Inputs `x` and `y` must be numeric vectors containing only finite, |
|
| 21 |
#' non-missing values. In particular, `NA`, `NaN`, `Inf`, and `-Inf` values are |
|
| 22 |
#' not allowed and will trigger an error before [stats::t.test()] is evaluated. |
|
| 23 |
#' |
|
| 24 |
#' When [stats::t.test()] succeeds, the resulting `"htest"` object is returned |
|
| 25 |
#' unchanged except that `data.name` is reconstructed from the original |
|
| 26 |
#' expressions supplied to `x` and `y`. |
|
| 27 |
#' |
|
| 28 |
#' When an error occurs, the function returns an object of class `"htest"` with: |
|
| 29 |
#' |
|
| 30 |
#' \itemize{
|
|
| 31 |
#' \item `NA_real` values for `statistic`, `parameter`, `p.value`, `stderr`, |
|
| 32 |
#' and `conf.int` |
|
| 33 |
#' \item computed sample mean(s) stored in `estimate` |
|
| 34 |
#' \item the original null hypothesis value stored in `null.value` |
|
| 35 |
#' \item the original test type and error message appended to `method`. |
|
| 36 |
#' } |
|
| 37 |
#' |
|
| 38 |
#' @inheritParams stats::t.test |
|
| 39 |
#' @param x (`numeric`)\cr A numeric vector containing only finite, non-missing |
|
| 40 |
#' values. |
|
| 41 |
#' @param y (`numeric`)\cr An optional numeric vector containing only finite, |
|
| 42 |
#' non-missing values. |
|
| 43 |
#' |
|
| 44 |
#' @return |
|
| 45 |
#' An object of class `"htest"`. |
|
| 46 |
#' |
|
| 47 |
#' If [stats::t.test()] succeeds, the standard `"htest"` object is returned. |
|
| 48 |
#' If an error occurs, an `"htest"` object with `NA` inferential statistics and |
|
| 49 |
#' computed sample mean(s) in `estimate` is returned instead. |
|
| 50 |
#' |
|
| 51 |
#' @author WW |
|
| 52 |
#' |
|
| 53 |
#' @importFrom stats t.test |
|
| 54 |
#' |
|
| 55 |
#' @examples |
|
| 56 |
#' # Standard usage |
|
| 57 |
#' x <- 1:10 |
|
| 58 |
#' y <- 11:20 |
|
| 59 |
#' |
|
| 60 |
#' t.test(x, y) |
|
| 61 |
#' safe_t_test(x, y) |
|
| 62 |
#' |
|
| 63 |
#' # Paired test with constant differences |
|
| 64 |
#' x <- rep(10, 5) |
|
| 65 |
#' |
|
| 66 |
#' t.test(x, x, paired = TRUE) |
|
| 67 |
#' safe_t_test(x, x, paired = TRUE) |
|
| 68 |
#' |
|
| 69 |
#' # Example triggering an error (constant data) |
|
| 70 |
#' t.test(x, x) |
|
| 71 |
#' safe_t_test(x, x) |
|
| 72 |
safe_t_test <- function( |
|
| 73 |
x, |
|
| 74 |
y = NULL, |
|
| 75 |
alternative = c("two.sided", "less", "greater"),
|
|
| 76 |
mu = 0, |
|
| 77 |
paired = FALSE, |
|
| 78 |
var.equal = FALSE, |
|
| 79 |
conf.level = 0.95, |
|
| 80 |
... |
|
| 81 |
) {
|
|
| 82 | 188x |
x_expr <- substitute(x) |
| 83 | 188x |
y_expr <- substitute(y) |
| 84 | 188x |
alternative <- match.arg(alternative) |
| 85 | ||
| 86 | 188x |
checkmate::assert_numeric(x, any.missing = FALSE, finite = TRUE) |
| 87 | 173x |
if (paired) {
|
| 88 | 20x |
checkmate::assert_numeric(y, finite = TRUE, any.missing = FALSE, len = length(x)) |
| 89 |
} else {
|
|
| 90 | 153x |
checkmate::assert_numeric(y, finite = TRUE, any.missing = FALSE, null.ok = TRUE) |
| 91 |
} |
|
| 92 | 171x |
checkmate::assert_choice(alternative, choices = c("two.sided", "less", "greater"))
|
| 93 | 171x |
checkmate::assert_number(mu, finite = TRUE) |
| 94 | 171x |
checkmate::assert_flag(paired) |
| 95 | 171x |
checkmate::assert_flag(var.equal) |
| 96 | 171x |
checkmate::assert_number(conf.level, lower = 0, upper = 1, finite = TRUE) |
| 97 | ||
| 98 | 171x |
tryCatch( |
| 99 |
{
|
|
| 100 | 171x |
res <- stats::t.test( |
| 101 | 171x |
x = x, |
| 102 | 171x |
y = y, |
| 103 | 171x |
alternative = alternative, |
| 104 | 171x |
mu = mu, |
| 105 | 171x |
paired = paired, |
| 106 | 171x |
var.equal = var.equal, |
| 107 | 171x |
conf.level = conf.level, |
| 108 |
... |
|
| 109 |
) |
|
| 110 | ||
| 111 | 146x |
res$data.name <- if (is.null(y)) {
|
| 112 | 5x |
deparse1(x_expr) |
| 113 |
} else {
|
|
| 114 | 141x |
paste(deparse1(x_expr), "and", deparse1(y_expr)) |
| 115 |
} |
|
| 116 | ||
| 117 | 146x |
res |
| 118 |
}, |
|
| 119 | 171x |
error = function(e) {
|
| 120 | 25x |
if (paired) {
|
| 121 | 5x |
x <- x - y |
| 122 | 5x |
y <- NULL |
| 123 |
} |
|
| 124 | ||
| 125 | 25x |
if (is.null(y)) {
|
| 126 | 6x |
estimate <- mean(x) |
| 127 | 6x |
dname <- deparse1(x_expr) |
| 128 | 6x |
method <- ifelse(paired, "Paired t-test", "One Sample t-test") |
| 129 | 6x |
names(estimate) <- ifelse(paired, "mean difference", "mean of x") |
| 130 |
} else {
|
|
| 131 | 19x |
estimate <- c(mean(x), mean(y)) |
| 132 | 19x |
dname <- paste(deparse1(x_expr), "and", deparse1(y_expr)) |
| 133 | 19x |
method <- paste(if (!var.equal) "Welch", "Two Sample t-test") |
| 134 | 19x |
names(estimate) <- c("mean of x", "mean of y")
|
| 135 |
} |
|
| 136 | 25x |
estimate[is.nan(estimate)] <- NA_real_ |
| 137 | ||
| 138 | 25x |
names(mu) <- if (paired) {
|
| 139 | 5x |
"mean difference" |
| 140 | 25x |
} else if (!is.null(y)) {
|
| 141 | 19x |
"difference in means" |
| 142 |
} else {
|
|
| 143 | 1x |
"mean" |
| 144 |
} |
|
| 145 | ||
| 146 | 25x |
out <- list( |
| 147 | 25x |
statistic = setNames(NA_real_, "t"), |
| 148 | 25x |
parameter = setNames(NA_real_, "df"), |
| 149 | 25x |
p.value = NA_real_, |
| 150 | 25x |
conf.int = c(NA_real_, NA_real_), |
| 151 | 25x |
estimate = estimate, |
| 152 | 25x |
null.value = mu, |
| 153 | 25x |
stderr = NA_real_, |
| 154 | 25x |
alternative = alternative, |
| 155 | 25x |
method = paste0(method, " (failed: ", e$message, ")"), |
| 156 | 25x |
data.name = dname |
| 157 |
) |
|
| 158 | 25x |
attr(out$conf.int, "conf.level") <- conf.level |
| 159 | ||
| 160 | 25x |
class(out) <- "htest" |
| 161 | 25x |
out |
| 162 |
} |
|
| 163 |
) |
|
| 164 |
} |
| 1 |
#' @name a_freq_combos_j |
|
| 2 |
#' |
|
| 3 |
#' @title Analysis function count and percentage in column design controlled by combosdf |
|
| 4 |
#' |
|
| 5 |
#' @inheritParams proposal_argument_convention |
|
| 6 |
#' @inheritParams a_freq_j |
|
| 7 |
#' |
|
| 8 |
#' @param combosdf The df which provides the mapping of column facets to produce cumulative counts for .N_col.\cr |
|
| 9 |
#' In the cell facet, these cumulative records must then be removed from the numerator, |
|
| 10 |
#' which can be done via the filter_var parameter |
|
| 11 |
#' to avoid unwanted counting of events. |
|
| 12 |
#' @param do_not_filter A vector of facets (i.e., column headers), identifying headers for which |
|
| 13 |
#' no filtering of records should occur. |
|
| 14 |
#' That is, the numerator should contain cumulative counts. Generally, this will be used for a |
|
| 15 |
#' "Total" column, or something similar. |
|
| 16 |
#' @param filter_var The variable which identifies the records to count in the numerator for any given column. |
|
| 17 |
#' Generally, this will contain text matching the column header for the column associated with a given record. |
|
| 18 |
#' @param flag_var Variable which identifies the occurrence (or first occurrence) of an event. |
|
| 19 |
#' The flag variable is expected to have a value of "Y" identifying that the event should be counted, or NA otherwise. |
|
| 20 |
#' @param denom (`string`)\cr |
|
| 21 |
#' One of \cr |
|
| 22 |
#' \itemize{
|
|
| 23 |
#' \item \strong{N_col} Column count, \cr
|
|
| 24 |
#' \item \strong{n_df} Number of patients (based upon the main input dataframe `df`),\cr
|
|
| 25 |
#' \item \strong{n_altdf} Number of patients from the secondary dataframe (`.alt_df_full`),\cr
|
|
| 26 |
#' Note that argument `denom_by` will perform a row-split on the `.alt_df_full` dataframe.\cr |
|
| 27 |
#' It is a requirement that variables specified in `denom_by` are part of the row split specifications. \cr |
|
| 28 |
#' \item \strong{n_rowdf} Number of patients from the current row-level dataframe
|
|
| 29 |
#' (`.row_df` from the rtables splitting machinery).\cr |
|
| 30 |
#' \item \strong{n_parentdf} Number of patients from a higher row-level split than the current split.\cr
|
|
| 31 |
#' This higher row-level split is specified in the argument `denom_by`.\cr |
|
| 32 |
#' } |
|
| 33 |
#' @param .formats (named 'character' or 'list')\cr |
|
| 34 |
#' formats for the statistics. |
|
| 35 |
#' @seealso [a_freq_j()] for extensive `label_map` usage examples. |
|
| 36 |
#' @return list of requested statistics with formatted `rtables::CellValue()`.\cr |
|
| 37 |
#' |
|
| 38 |
#' @examples |
|
| 39 |
#' library(dplyr) |
|
| 40 |
#' ADSL <- ex_adsl |> select(USUBJID, ARM, EOSSTT, EOSDT, EOSDY, TRTSDTM) |
|
| 41 |
#' |
|
| 42 |
#' cutoffd <- as.Date("2023-09-24")
|
|
| 43 |
#' |
|
| 44 |
#' ADSL <- ADSL |> |
|
| 45 |
#' mutate( |
|
| 46 |
#' TRTDURY = case_when( |
|
| 47 |
#' !is.na(EOSDY) ~ EOSDY, |
|
| 48 |
#' TRUE ~ as.integer(cutoffd - as.Date(TRTSDTM) + 1) |
|
| 49 |
#' ) |
|
| 50 |
#' ) |> |
|
| 51 |
#' mutate(ACAT1 = case_when( |
|
| 52 |
#' TRTDURY < 183 ~ "0-6 Months", |
|
| 53 |
#' TRTDURY < 366 ~ "6-12 Months", |
|
| 54 |
#' TRUE ~ "+12 Months" |
|
| 55 |
#' )) |> |
|
| 56 |
#' mutate(ACAT1 = factor(ACAT1, levels = c("0-6 Months", "6-12 Months", "+12 Months")))
|
|
| 57 |
#' |
|
| 58 |
#' |
|
| 59 |
#' ADAE <- ex_adae |> select(USUBJID, ARM, AEBODSYS, AEDECOD, ASTDY) |
|
| 60 |
#' |
|
| 61 |
#' ADAE <- ADAE |> |
|
| 62 |
#' mutate(TRTEMFL = "Y") |> |
|
| 63 |
#' mutate(ACAT1 = case_when( |
|
| 64 |
#' ASTDY < 183 ~ "0-6 Months", |
|
| 65 |
#' ASTDY < 366 ~ "6-12 Months", |
|
| 66 |
#' TRUE ~ "+12 Months" |
|
| 67 |
#' )) |> |
|
| 68 |
#' mutate(ACAT1 = factor(ACAT1, levels = c("0-6 Months", "6-12 Months", "+12 Months")))
|
|
| 69 |
#' |
|
| 70 |
#' combodf <- tribble( |
|
| 71 |
#' ~valname, ~label, ~levelcombo, ~exargs, |
|
| 72 |
#' "Tot", "Total", c("0-6 Months", "6-12 Months", "+12 Months"), list(),
|
|
| 73 |
#' "A_0-6 Months", "0-6 Months", c("0-6 Months", "6-12 Months", "+12 Months"), list(),
|
|
| 74 |
#' "B_6-12 Months", "6-12 Months", c("6-12 Months", "+12 Months"), list(),
|
|
| 75 |
#' "C_+12 Months", "+12 Months", c("+12 Months"), list()
|
|
| 76 |
#' ) |
|
| 77 |
#' |
|
| 78 |
#' |
|
| 79 |
#' lyt <- basic_table(show_colcounts = TRUE) |> |
|
| 80 |
#' split_cols_by("ARM") |>
|
|
| 81 |
#' split_cols_by("ACAT1",
|
|
| 82 |
#' split_fun = rtables::add_combo_levels( |
|
| 83 |
#' combosdf = combodf, |
|
| 84 |
#' trim = FALSE, keep_levels = combodf$valname |
|
| 85 |
#' ) |
|
| 86 |
#' ) |> |
|
| 87 |
#' analyze("TRTEMFL",
|
|
| 88 |
#' show_labels = "hidden", |
|
| 89 |
#' afun = a_freq_combos_j, |
|
| 90 |
#' extra_args = list( |
|
| 91 |
#' val = "Y", |
|
| 92 |
#' label = "Subjects with >= 1 AE", |
|
| 93 |
#' combosdf = combodf, |
|
| 94 |
#' filter_var = "ACAT1", |
|
| 95 |
#' do_not_filter = "Tot" |
|
| 96 |
#' ) |
|
| 97 |
#' ) |
|
| 98 |
#' |
|
| 99 |
#' |
|
| 100 |
#' result <- build_table(lyt, df = ADAE, alt_counts_df = ADSL) |
|
| 101 |
#' |
|
| 102 |
#' result |
|
| 103 |
#' @export |
|
| 104 |
a_freq_combos_j <- function( |
|
| 105 |
df, |
|
| 106 |
labelstr = NULL, |
|
| 107 |
.var = NA, |
|
| 108 |
val = NULL, |
|
| 109 |
# arguments specific to a_freq_combos_j |
|
| 110 |
combosdf = NULL, |
|
| 111 |
do_not_filter = NULL, |
|
| 112 |
filter_var = NULL, |
|
| 113 |
flag_var = NULL, |
|
| 114 |
# arguments specific to a_freq_combos_j till here |
|
| 115 |
.df_row, |
|
| 116 |
.spl_context, |
|
| 117 |
.N_col, |
|
| 118 |
id = "USUBJID", |
|
| 119 |
denom = c("N_col", "n_df", "n_altdf", "n_rowdf", "n_parentdf"),
|
|
| 120 |
label = NULL, |
|
| 121 |
label_fstr = NULL, |
|
| 122 |
label_map = NULL, |
|
| 123 |
.alt_df_full = NULL, |
|
| 124 |
denom_by = NULL, |
|
| 125 |
.stats = "count_unique_denom_fraction", |
|
| 126 |
.formats = NULL, |
|
| 127 |
.labels_n = NULL, |
|
| 128 |
.indent_mods = NULL, |
|
| 129 |
na_str = rep("NA", 3)
|
|
| 130 |
) {
|
|
| 131 | 12x |
denom <- match.arg(denom) |
| 132 | ||
| 133 | 12x |
check_alt_df_full(denom, "n_altdf", .alt_df_full) |
| 134 | ||
| 135 | 12x |
if (!is.null(combosdf) && !all(c("valname", "label") %in% names(combosdf))) {
|
| 136 | ! |
stop("a_freq_combos_j: combosdf must have variables valname and label.")
|
| 137 |
} |
|
| 138 | ||
| 139 | 12x |
res_dataprep <- h_a_freq_dataprep( |
| 140 | 12x |
df = df, |
| 141 | 12x |
labelstr = labelstr, |
| 142 | 12x |
.var = .var, |
| 143 | 12x |
val = val, |
| 144 | 12x |
drop_levels = FALSE, |
| 145 | 12x |
excl_levels = NULL, |
| 146 | 12x |
new_levels = NULL, |
| 147 | 12x |
new_levels_after = FALSE, |
| 148 | 12x |
.df_row = .df_row, |
| 149 | 12x |
.spl_context = .spl_context, |
| 150 | 12x |
.N_col = .N_col, |
| 151 | 12x |
id = id, |
| 152 | 12x |
denom = denom, |
| 153 | 12x |
variables = NULL, |
| 154 | 12x |
label = label, |
| 155 | 12x |
label_fstr = label_fstr, |
| 156 | 12x |
label_map = label_map, |
| 157 | 12x |
.alt_df_full = .alt_df_full, |
| 158 | 12x |
denom_by = denom_by, |
| 159 | 12x |
.stats = .stats |
| 160 |
) |
|
| 161 |
# res_dataprep is list with elements |
|
| 162 |
# df .df_row val |
|
| 163 |
# drop_levels excl_levels |
|
| 164 |
# alt_df parentdf new_denomdf |
|
| 165 |
# .stats |
|
| 166 |
# make these elements available in current environment |
|
| 167 | 12x |
df <- res_dataprep$df |
| 168 | 12x |
.df_row <- res_dataprep$.df_row |
| 169 | 12x |
val <- res_dataprep$val |
| 170 | 12x |
drop_levels <- res_dataprep$drop_levels |
| 171 | 12x |
excl_levels <- res_dataprep$excl_levels |
| 172 | 12x |
alt_df <- res_dataprep$alt_df |
| 173 | 12x |
parentdf <- res_dataprep$parentdf |
| 174 | 12x |
new_denomdf <- res_dataprep$new_denomdf |
| 175 | 12x |
.stats <- res_dataprep$.stats |
| 176 | ||
| 177 |
## colid can be used to figure out if we're in the combo column or not |
|
| 178 | 12x |
colid <- .spl_context$cur_col_id[[1]] |
| 179 | ||
| 180 |
### this is the core code for subsetting to appropriate combo level |
|
| 181 | 12x |
df <- h_subset_combo( |
| 182 | 12x |
df = df, |
| 183 | 12x |
combosdf = combosdf, |
| 184 | 12x |
do_not_filter = do_not_filter, |
| 185 | 12x |
filter_var = filter_var, |
| 186 | 12x |
flag_var = flag_var, |
| 187 | 12x |
colid = colid |
| 188 |
) |
|
| 189 | ||
| 190 |
## the same s-function can be used as in a_freq_j |
|
| 191 | 12x |
x_stats <- s_freq_j( |
| 192 | 12x |
df, |
| 193 | 12x |
.var = .var, |
| 194 | 12x |
.df_row = .df_row, |
| 195 | 12x |
val = val, |
| 196 | 12x |
alt_df = new_denomdf, |
| 197 | 12x |
parent_df = new_denomdf, |
| 198 | 12x |
id = id, |
| 199 | 12x |
denom = denom, |
| 200 | 12x |
.N_col = .N_col, |
| 201 | 12x |
countsource = "df" |
| 202 |
) |
|
| 203 | ||
| 204 | 12x |
.stats_adj <- .stats |
| 205 | ||
| 206 | 12x |
res_prepinrows <- h_a_freq_prepinrows( |
| 207 | 12x |
x_stats, |
| 208 | 12x |
.stats_adj, |
| 209 | 12x |
.formats, |
| 210 | 12x |
labelstr, |
| 211 | 12x |
label_fstr, |
| 212 | 12x |
label, |
| 213 | 12x |
.indent_mods, |
| 214 | 12x |
.labels_n, |
| 215 | 12x |
na_str |
| 216 |
) |
|
| 217 |
# res_prepinrows is list with elements |
|
| 218 |
# x_stats .formats .labels .indent_mods .format_na_strs |
|
| 219 |
# make these elements available in current environment |
|
| 220 | 12x |
x_stats <- res_prepinrows$x_stats |
| 221 | 12x |
.formats <- res_prepinrows$.formats |
| 222 | 12x |
.labels <- res_prepinrows$.labels |
| 223 | 12x |
.indent_mods <- res_prepinrows$.indent_mods |
| 224 | 12x |
.format_na_strs <- res_prepinrows$.format_na_strs |
| 225 | ||
| 226 |
### final step: turn requested stats into rtables rows |
|
| 227 | 12x |
in_rows( |
| 228 | 12x |
.list = x_stats, |
| 229 | 12x |
.formats = .formats, |
| 230 | 12x |
.labels = .labels, |
| 231 | 12x |
.indent_mods = .indent_mods, |
| 232 | 12x |
.format_na_strs = .format_na_strs |
| 233 |
) |
|
| 234 |
} |
| 1 |
#' Assert a Response Contingency Table |
|
| 2 |
#' |
|
| 3 |
#' Checks that a response table contains finite, non-negative integer-like counts |
|
| 4 |
#' in a 2 by 2 table, optionally with a third dimension for strata. |
|
| 5 |
#' |
|
| 6 |
#' @param tbl (`array`)\cr A 2 by 2 response table, or a 2 by 2 by strata array, |
|
| 7 |
#' containing finite, non-negative integer-like counts without missing values. |
|
| 8 |
#' @param stratified (`flag`)\cr Whether `tbl` must have a third dimension for strata. |
|
| 9 |
#' |
|
| 10 |
#' @return Invisibly returns `NULL`. |
|
| 11 |
#' @keywords internal |
|
| 12 |
#' @noRd |
|
| 13 |
assert_response_table <- function(tbl, stratified = FALSE) {
|
|
| 14 | 53x |
checkmate::assert_flag(stratified) |
| 15 | 52x |
d <- if (stratified) 3L else 2L |
| 16 | 52x |
checkmate::assert_array(tbl, mode = "integerish", d = d) |
| 17 | 45x |
checkmate::assert_true(identical(dim(tbl)[1:2], c(2L, 2L))) |
| 18 | 42x |
checkmate::assert_numeric(tbl, lower = 0, finite = TRUE, any.missing = FALSE) |
| 19 | 37x |
invisible(NULL) |
| 20 |
} |
|
| 21 | ||
| 22 |
#' Extract the left-hand side of a formula |
|
| 23 |
#' |
|
| 24 |
#' @param x (`formula`)\cr A two-sided formula, e.g., `y ~ x1 + x2`. |
|
| 25 |
#' |
|
| 26 |
#' @return (`character(1)`) The name of the left-hand side of the formula. |
|
| 27 |
#' |
|
| 28 |
#' @examples |
|
| 29 |
#' leftside(y ~ x) |
|
| 30 |
#' |
|
| 31 |
#' @export |
|
| 32 |
leftside <- function(x) {
|
|
| 33 | ! |
checkmate::assert_formula(x) |
| 34 | ! |
res <- x[[2L]] |
| 35 | ! |
res <- as.character(res) |
| 36 | ! |
checkmate::assert_string(res) |
| 37 | ! |
res |
| 38 |
} |
|
| 39 | ||
| 40 |
#' Extract the right-hand side of a formula |
|
| 41 |
#' |
|
| 42 |
#' @param x (`formula`)\cr A two-sided formula, e.g., `y ~ x1 + x2`. |
|
| 43 |
#' |
|
| 44 |
#' @return (`character(1)`) The right-hand side of the formula. |
|
| 45 |
#' |
|
| 46 |
#' @examples |
|
| 47 |
#' rightside(y ~ x1 + x2) |
|
| 48 |
#' |
|
| 49 |
#' @export |
|
| 50 |
rightside <- function(x) {
|
|
| 51 | 3x |
checkmate::assert_formula(x) |
| 52 | 3x |
res <- x[[3L]] |
| 53 | 3x |
if (is.character(res) && length(res) == 1L) {
|
| 54 | 1x |
res <- as.character(res) |
| 55 |
} else {
|
|
| 56 | 2x |
res <- paste(deparse(res), collapse = "") |
| 57 |
} |
|
| 58 | 3x |
checkmate::assert_string(res) |
| 59 | 3x |
res |
| 60 |
} |
|
| 61 | ||
| 62 |
#' Custom unlist function |
|
| 63 |
#' |
|
| 64 |
#' Unlist a list, but retain `NULL` as `'NULL'` or `NA`. |
|
| 65 |
#' |
|
| 66 |
#' @keywords internal |
|
| 67 |
.unlist_keep_nulls <- function(lst, null_placeholder = "NULL", recursive = FALSE) {
|
|
| 68 | 2753x |
lapply(lst, function(x) if (is.null(x)) null_placeholder else x) |> |
| 69 | 2753x |
unlist(recursive = recursive) |
| 70 |
} |
|
| 71 | ||
| 72 |
#' @title Title Case Conversion |
|
| 73 |
#' |
|
| 74 |
#' @description `r lifecycle::badge("stable")`
|
|
| 75 |
#' |
|
| 76 |
#' Convert strings to title case (first letter of each word capitalized), |
|
| 77 |
#' with the option to keep specified words, such as conjunctions, lowercase. |
|
| 78 |
#' |
|
| 79 |
#' @param x (`character` or `factor`)\cr |
|
| 80 |
#' Input string. |
|
| 81 |
#' @param lowercase_words (`character` or `NULL`)\cr |
|
| 82 |
#' Words that should remain lowercase when converting to title case. |
|
| 83 |
#' Matching is case-insensitive. Specified words are only converted to |
|
| 84 |
#' lowercase when they are not the first word in the string. The first word is |
|
| 85 |
#' always capitalized, even if it is included in `lowercase_words`. |
|
| 86 |
#' @return `x` converted to title case, with words specified in |
|
| 87 |
#' `lowercase_words` kept lowercase. |
|
| 88 |
#' |
|
| 89 |
#' @export |
|
| 90 |
#' |
|
| 91 |
#' @examples |
|
| 92 |
#' x <- c("THIS IS an eXaMple", "statement TO CAPItaliZe")
|
|
| 93 |
#' string_to_title(x) |
|
| 94 |
#' string_to_title(x, c("is", "an", "to"))
|
|
| 95 |
#' |
|
| 96 |
#' x <- factor( |
|
| 97 |
#' c("OPTIMAL DOSE", "UNDERDOSE"),
|
|
| 98 |
#' levels = c("OPTIMAL DOSE", "UNDERDOSE", "OVERDOSE")
|
|
| 99 |
#' ) |
|
| 100 |
#' string_to_title(x) |
|
| 101 |
#' |
|
| 102 |
string_to_title <- function(x, lowercase_words = NULL) {
|
|
| 103 | 17x |
checkmate::assert( |
| 104 | 17x |
checkmate::check_character(x, null.ok = TRUE), |
| 105 | 17x |
checkmate::check_factor(x, null.ok = TRUE) |
| 106 |
) |
|
| 107 | 17x |
checkmate::assert_character(lowercase_words, min.chars = 1L, any.missing = FALSE, null.ok = TRUE) |
| 108 | ||
| 109 | 17x |
if (is.null(x)) {
|
| 110 | ! |
return(character()) |
| 111 |
} |
|
| 112 | ||
| 113 |
# Step 1. Apply initial title case (capitalize the first letter of every word). |
|
| 114 | 17x |
y <- if (is.factor(x)) {
|
| 115 | 9x |
levels(x) |
| 116 |
} else {
|
|
| 117 | 8x |
x |
| 118 |
} |
|
| 119 | ||
| 120 | 17x |
pattern <- "(^|\\s)(\\w)" |
| 121 | 17x |
replacement <- "\\1\\U\\2" |
| 122 | 17x |
y <- gsub(pattern, replacement, tolower(y), perl = TRUE) |
| 123 | ||
| 124 |
# Step 2. Apply lowercase exceptions for specified words. |
|
| 125 | 17x |
if (!is.null(lowercase_words)) {
|
| 126 |
# Match specified words only when preceded by whitespace that follows |
|
| 127 |
# a non-whitespace character, ensuring the first word is excluded. |
|
| 128 | 10x |
pattern_lw <- paste0( |
| 129 | 10x |
"(?<=\\S)(\\s+)(",
|
| 130 | 10x |
paste(lowercase_words, collapse = "|"), |
| 131 | 10x |
")(?=\\s|$)" |
| 132 |
) |
|
| 133 | ||
| 134 |
# Preserve the preceding whitespace and convert the word to lowercase. |
|
| 135 | 10x |
replacement_lw <- "\\1\\L\\2" |
| 136 | ||
| 137 | 10x |
y <- gsub(pattern_lw, replacement_lw, y, ignore.case = TRUE, perl = TRUE) |
| 138 |
} |
|
| 139 | ||
| 140 | 17x |
ret <- if (is.factor(x)) {
|
| 141 | 9x |
levels(x) <- y |
| 142 | 9x |
x |
| 143 |
} else {
|
|
| 144 | 8x |
y |
| 145 |
} |
|
| 146 | ||
| 147 | 17x |
ret |
| 148 |
} |
|
| 149 | ||
| 150 |
#' Check If `.alt_df_full` Is `NULL` |
|
| 151 |
#' |
|
| 152 |
#' For example, in `a_patyrs_j()`, if `source` is `"alt_df"`, we need to |
|
| 153 |
#' check if `.alt_df_full` is `NULL`. |
|
| 154 |
#' |
|
| 155 |
#' @noRd |
|
| 156 |
check_alt_df_full <- function(argument, values, .alt_df_full) {
|
|
| 157 | 579x |
if (!argument %in% values || !is.null(.alt_df_full)) {
|
| 158 | 579x |
return(invisible()) |
| 159 |
} |
|
| 160 | ||
| 161 | ! |
name <- deparse(substitute(argument)) |
| 162 | ||
| 163 | ! |
stop(sprintf( |
| 164 | ! |
'`.alt_df_full` cannot be `NULL` when `%s` is `"%s"`', |
| 165 | ! |
name, |
| 166 | ! |
argument |
| 167 |
)) |
|
| 168 |
} |
|
| 169 | ||
| 170 |
#' @title Extract Vectors for Two-Sample Analysis |
|
| 171 |
#' |
|
| 172 |
#' @noRd |
|
| 173 |
#' |
|
| 174 |
#' @description `r lifecycle::badge("stable")`
|
|
| 175 |
#' |
|
| 176 |
#' Extract (aligned) vectors from two data frames for two-sample statistical |
|
| 177 |
#' analysis using complete-case (non-missing) observations. |
|
| 178 |
#' |
|
| 179 |
#' For unpaired data, values are extracted directly from each dataset with |
|
| 180 |
#' missing values (`NA`) removed independently. |
|
| 181 |
#' |
|
| 182 |
#' For paired data, observations are first matched using the key variable(s) |
|
| 183 |
#' specified in `paired_by`, and only complete pairs are retained. |
|
| 184 |
#' |
|
| 185 |
#' The function validates that `paired_by` uniquely identifies rows in each |
|
| 186 |
#' dataset (after removing rows with incomplete values in `paired_by` columns) |
|
| 187 |
#' and raises an error if duplicates are detected. |
|
| 188 |
#' |
|
| 189 |
#' @details |
|
| 190 |
#' The function performs the following steps depending on the `paired` flag: |
|
| 191 |
#' |
|
| 192 |
#' \strong{Unpaired case (`paired = FALSE`):}
|
|
| 193 |
#' \enumerate{
|
|
| 194 |
#' \item Extract `.var` from each dataset. |
|
| 195 |
#' \item Remove `NA` values independently from each vector. |
|
| 196 |
#' } |
|
| 197 |
#' |
|
| 198 |
#' \strong{Paired case (`paired = TRUE`):}
|
|
| 199 |
#' \enumerate{
|
|
| 200 |
#' \item Check that `paired_by` uniquely identifies rows in each dataset, |
|
| 201 |
#' considering only rows that are complete cases for the `paired_by` columns. |
|
| 202 |
#' |
|
| 203 |
#' \item Merge `df1` and `df2` by the columns specified in `paired_by`. |
|
| 204 |
#' The merged data contains only the `paired_by` columns and the `.var` column |
|
| 205 |
#' from each dataset. |
|
| 206 |
#' |
|
| 207 |
#' \item Remove rows containing any missing values (`NA`) in the merged data. |
|
| 208 |
#' |
|
| 209 |
#' \item Extract aligned vectors corresponding to `.var`. |
|
| 210 |
#' } |
|
| 211 |
#' |
|
| 212 |
#' This function is intended for internal use in two-sample statistical |
|
| 213 |
#' procedures such as paired and unpaired t-tests. |
|
| 214 |
#' |
|
| 215 |
#' @param df1 (`data.frame`)\cr First dataset. |
|
| 216 |
#' @param df2 (`data.frame`)\cr Second dataset. |
|
| 217 |
#' @param .var (`character(1)`)\cr Name of the variable to extract from both |
|
| 218 |
#' datasets. |
|
| 219 |
#' @param paired (`logical(1)`)\cr Whether the values in `df1[[.var]]` and |
|
| 220 |
#' `df2[[.var]]` should be treated as paired (matched) samples. |
|
| 221 |
#' @param paired_by (`character`)\cr Column name(s) used to match observations |
|
| 222 |
#' between `df1` and `df2`. Required only if `paired = TRUE`. |
|
| 223 |
#' |
|
| 224 |
#' @return |
|
| 225 |
#' A named `list` with: |
|
| 226 |
#' \describe{
|
|
| 227 |
#' \item{x1}{Non-missing values from `df1[[.var]]` after optional pairing.}
|
|
| 228 |
#' \item{x2}{Non-missing values from `df2[[.var]]` after optional pairing.}
|
|
| 229 |
#' } |
|
| 230 |
#' |
|
| 231 |
#' Returned vectors may be shorter than the original inputs due to removal of |
|
| 232 |
#' unmatched observations, missing values (`NA`). |
|
| 233 |
#' |
|
| 234 |
#' @author WW |
|
| 235 |
#' |
|
| 236 |
#' @keywords internal |
|
| 237 |
#' |
|
| 238 |
#' @importFrom stats complete.cases |
|
| 239 |
#' |
|
| 240 |
#' @examples |
|
| 241 |
#' df1 <- data.frame(id = c("A", "B", "C", "D"), value = 1:4)
|
|
| 242 |
#' df2 <- data.frame(id = c("A", "C", "D", "E", "F"), value = c(11, 13:14, NA, 16))
|
|
| 243 |
#' df1 |
|
| 244 |
#' df2 |
|
| 245 |
#' |
|
| 246 |
#' # Unpaired |
|
| 247 |
#' extract_vectors(df1, df2, "value") |
|
| 248 |
#' |
|
| 249 |
#' # Paired |
|
| 250 |
#' extract_vectors(df1, df2, "value", paired = TRUE, paired_by = "id") |
|
| 251 |
#' |
|
| 252 |
extract_vectors <- function(df1, df2, .var, paired = FALSE, paired_by) {
|
|
| 253 | 70x |
checkmate::assert_data_frame(df1) |
| 254 | 70x |
checkmate::assert_data_frame(df2) |
| 255 | 70x |
checkmate::assert_string(.var) |
| 256 | 70x |
checkmate::assert_names(colnames(df1), must.include = .var) |
| 257 | 69x |
checkmate::assert_names(colnames(df2), must.include = .var) |
| 258 | 69x |
checkmate::assert_flag(paired) |
| 259 | ||
| 260 | 69x |
if (paired) {
|
| 261 | 30x |
checkmate::assert_character(paired_by) |
| 262 | 30x |
checkmate::assert_names(colnames(df1), must.include = paired_by) |
| 263 | 27x |
checkmate::assert_names(colnames(df2), must.include = paired_by) |
| 264 | ||
| 265 | 26x |
df1_keys <- df1[complete.cases(df1[, paired_by]), paired_by] |
| 266 | 26x |
df2_keys <- df2[complete.cases(df2[, paired_by]), paired_by] |
| 267 | ||
| 268 | 26x |
if (any(duplicated(df1_keys))) {
|
| 269 | 4x |
stop("Duplicate values in 'paired_by' columns in df1 (complete cases only).")
|
| 270 |
} |
|
| 271 | ||
| 272 | 22x |
if (any(duplicated(df2_keys))) {
|
| 273 | 2x |
stop("Duplicate values in 'paired_by' columns in df2 (complete cases only).")
|
| 274 |
} |
|
| 275 | ||
| 276 | 20x |
suffixes <- c("_df1", "_df2")
|
| 277 | ||
| 278 | 20x |
df <- merge( |
| 279 | 20x |
df1[, c(paired_by, .var), drop = FALSE], |
| 280 | 20x |
df2[, c(paired_by, .var), drop = FALSE], |
| 281 | 20x |
by = paired_by, |
| 282 | 20x |
suffixes = suffixes |
| 283 |
) |
|
| 284 | 20x |
df <- df[complete.cases(df), , drop = FALSE] |
| 285 | ||
| 286 | 20x |
varsfx <- paste0(.var, suffixes) |
| 287 | 20x |
x1 <- df[[varsfx[1]]] |
| 288 | 20x |
x2 <- df[[varsfx[2]]] |
| 289 |
} else {
|
|
| 290 | 39x |
x1 <- df1[[.var]] |
| 291 | 39x |
x2 <- df2[[.var]] |
| 292 | ||
| 293 | 39x |
x1 <- x1[!is.na(x1)] |
| 294 | 39x |
x2 <- x2[!is.na(x2)] |
| 295 |
} |
|
| 296 | ||
| 297 | 59x |
list(x1 = x1, x2 = x2) |
| 298 |
} |
|
| 299 | ||
| 300 | ||
| 301 |
#' Helper for Finding AVISIT after which CHG are all Missing |
|
| 302 |
#' |
|
| 303 |
#' @description |
|
| 304 |
#' Helper for Finding AVISIT after which CHG are all Missing. |
|
| 305 |
#' |
|
| 306 |
#' @param df (`data.frame`)\cr with `CHG` and `AVISIT` variables. |
|
| 307 |
#' |
|
| 308 |
#' @return A string with either the factor level after which `AVISIT` is all missing, |
|
| 309 |
#' or `NA`. |
|
| 310 |
#' @export |
|
| 311 |
#' |
|
| 312 |
#' @examples |
|
| 313 |
#' df <- data.frame( |
|
| 314 |
#' AVISIT = factor(c(1, 2, 3, 4, 5)), |
|
| 315 |
#' CHG = c(5, NA, NA, NA, 3) |
|
| 316 |
#' ) |
|
| 317 |
#' find_missing_chg_after_avisit(df) |
|
| 318 |
#' |
|
| 319 |
#' df2 <- data.frame( |
|
| 320 |
#' AVISIT = factor(c(1, 2, 3, 4, 5)), |
|
| 321 |
#' CHG = c(5, NA, 3, NA, NA) |
|
| 322 |
#' ) |
|
| 323 |
#' find_missing_chg_after_avisit(df2) |
|
| 324 |
#' |
|
| 325 |
#' df3 <- data.frame( |
|
| 326 |
#' AVISIT = factor(c(1, 2, 3, 4, 5)), |
|
| 327 |
#' CHG = c(NA, NA, NA, NA, NA) |
|
| 328 |
#' ) |
|
| 329 |
#' find_missing_chg_after_avisit(df3) |
|
| 330 |
find_missing_chg_after_avisit <- function(df) {
|
|
| 331 | 8x |
checkmate::assert_data_frame(df) |
| 332 | 7x |
checkmate::assert_factor(df$AVISIT, unique = TRUE, any.missing = FALSE) |
| 333 | 6x |
checkmate::assert_numeric(df$CHG) |
| 334 | ||
| 335 |
# Ensure the dataframe is sorted by AVISIT |
|
| 336 | 5x |
df <- df[order(df$AVISIT), ] |
| 337 | ||
| 338 |
# Last visit with available data. |
|
| 339 | 5x |
visit_levels_available <- as.integer(df[!is.na(df$CHG), ]$AVISIT) |
| 340 | ||
| 341 | 5x |
if (!length(visit_levels_available)) {
|
| 342 | 1x |
return(levels(df$AVISIT)[1]) |
| 343 |
} |
|
| 344 | 4x |
visit_levels_max_available <- max(visit_levels_available) |
| 345 | ||
| 346 |
# Visits with missing data. |
|
| 347 | 4x |
visit_levels_missing <- as.integer(df[is.na(df$CHG), ]$AVISIT) |
| 348 | ||
| 349 |
# Missing visits at the end. |
|
| 350 | 4x |
visit_levels_missing_end <- visit_levels_missing[ |
| 351 | 4x |
visit_levels_missing > visit_levels_max_available |
| 352 |
] |
|
| 353 | ||
| 354 |
# Return first one if there is any. |
|
| 355 | 4x |
if (length(visit_levels_missing_end)) {
|
| 356 | 3x |
levels(df$AVISIT)[min(visit_levels_missing_end)] |
| 357 |
} else {
|
|
| 358 |
NA_character_ |
|
| 359 |
} |
|
| 360 |
} |
|
| 361 | ||
| 362 | ||
| 363 |
#' |
|
| 364 |
#' @description |
|
| 365 |
#' Helper for transposing a named list of named lists, where inner lists have same names |
|
| 366 |
#' |
|
| 367 |
#' @param x (`list`)\cr with depth of 2. |
|
| 368 |
#' |
|
| 369 |
#' @return Transposed version of list where inner elements now are outer elements. |
|
| 370 |
#' @noRd |
|
| 371 |
#' @keywords internal |
|
| 372 |
transpose_named_list <- function(x) {
|
|
| 373 |
# x: named list of named lists |
|
| 374 | 121x |
keys <- unique(lapply(x, names)) |
| 375 | 121x |
if (length(keys) != 1) {
|
| 376 | 1x |
stop("Input list must have same names on all sublists")
|
| 377 |
} else {
|
|
| 378 | 120x |
keys <- keys[[1]] |
| 379 |
} |
|
| 380 |
# rebuild structure |
|
| 381 | 120x |
setNames( |
| 382 | 120x |
lapply(keys, function(k) {
|
| 383 | 398x |
setNames( |
| 384 | 398x |
lapply(x, function(inner_list) {
|
| 385 | 635x |
inner_list[[k]] |
| 386 |
}), |
|
| 387 | 398x |
names(x) |
| 388 |
) |
|
| 389 |
}), |
|
| 390 | 120x |
keys |
| 391 |
) |
|
| 392 |
} |
|
| 393 | ||
| 394 |
#' @title Copy missing attributes to an object |
|
| 395 |
#' |
|
| 396 |
#' @noRd |
|
| 397 |
#' |
|
| 398 |
#' @description `r lifecycle::badge("stable")`
|
|
| 399 |
#' |
|
| 400 |
#' Copies attributes from `source` to `target` that are not already present |
|
| 401 |
#' on `target`. Existing attributes of `target` are preserved. |
|
| 402 |
#' |
|
| 403 |
#' @param source An object providing the attributes to copy. |
|
| 404 |
#' @param target An object to which missing attributes are copied. |
|
| 405 |
#' |
|
| 406 |
#' @return The `target` object with any missing attributes copied from `source`. |
|
| 407 |
#' |
|
| 408 |
#' @author WW |
|
| 409 |
#' @keywords internal |
|
| 410 |
#' @seealso [factor_by_order()] |
|
| 411 |
#' |
|
| 412 |
#' @examples |
|
| 413 |
#' |
|
| 414 |
#' x <- factor(c("Placebo", "Placebo", "Drug X"))
|
|
| 415 |
#' |
|
| 416 |
#' x_labeled <- formatters::with_label(x, label = "Treatment Group") |
|
| 417 |
#' attributes(x_labeled) |
|
| 418 |
#' |
|
| 419 |
#' # Copy the `label` attribute from `x_labeled` to `x`. |
|
| 420 |
#' x_copy <- copy_attributes(x_labeled, x) |
|
| 421 |
#' attributes(x_copy) |
|
| 422 |
#' |
|
| 423 |
copy_attributes <- function(source, target) {
|
|
| 424 | 24x |
source_attr <- attributes(source) |
| 425 | 24x |
if (is.null(source_attr)) {
|
| 426 | 11x |
return(target) |
| 427 |
} |
|
| 428 | ||
| 429 | 13x |
target_attr <- attributes(target) |
| 430 | ||
| 431 | 13x |
to_copy <- setdiff(names(source_attr), names(target_attr)) |
| 432 | 13x |
attributes(target) <- c(target_attr, source_attr[to_copy]) |
| 433 | 13x |
target |
| 434 |
} |
|
| 435 | ||
| 436 |
#' @title Check whether two vectors define a one-to-one correspondence |
|
| 437 |
#' |
|
| 438 |
#' @noRd |
|
| 439 |
#' |
|
| 440 |
#' @description `r lifecycle::badge("stable")`
|
|
| 441 |
#' |
|
| 442 |
#' Tests whether `x` and `y` define a bijection between their unique non-missing |
|
| 443 |
#' values. |
|
| 444 |
#' |
|
| 445 |
#' Missing values are treated specially: if `match_na = FALSE`, any missing |
|
| 446 |
#' values will trigger an error. If `match_na = TRUE`, an `NA` in `x` |
|
| 447 |
#' must correspond to an `NA` in `y` at the same position; otherwise, an error |
|
| 448 |
#' is thrown. |
|
| 449 |
#' |
|
| 450 |
#' @note Factors are not supported because they may contain unused levels that |
|
| 451 |
#' do not appear in the observed data. For such levels, the corresponding |
|
| 452 |
#' values in the other vector are not available, and therefore a bijection |
|
| 453 |
#' cannot be determined. |
|
| 454 |
#' |
|
| 455 |
#' @param x (`character` or `numeric`)\cr A vector defining one side of the |
|
| 456 |
#' mapping. |
|
| 457 |
#' @param y (`character` or `numeric`)\cr A vector defining the other side of |
|
| 458 |
#' the mapping. Must have the same length as `x`. |
|
| 459 |
#' @param match_na (`logical(1)`)\cr Whether to permit missing values (`NA`) |
|
| 460 |
#' if they appear at identical positions in both vectors. |
|
| 461 |
#' If `FALSE` (the default), any presence of missing values triggers an error. |
|
| 462 |
#' |
|
| 463 |
#' @return A single logical value indicating whether `x` and `y` define a |
|
| 464 |
#' bijection. |
|
| 465 |
#' |
|
| 466 |
#' @seealso [factor_by_order()] |
|
| 467 |
#' @keywords internal |
|
| 468 |
#' @author WW |
|
| 469 |
#' |
|
| 470 |
#' @examples |
|
| 471 |
#' is_bijection(c("A", "A", "B"), c(1, 1, 2))
|
|
| 472 |
#' |
|
| 473 |
#' is_bijection(c("A", "B"), c(1, 1))
|
|
| 474 |
#' |
|
| 475 |
#' is_bijection(c("A", NA), c(1, NA))
|
|
| 476 |
#' |
|
| 477 |
#' is_bijection(c("A", NA), c(1, 2))
|
|
| 478 |
is_bijection <- function(x, y, match_na = FALSE) {
|
|
| 479 | 76x |
checkmate::assert( |
| 480 | 76x |
checkmate::test_character(x) || checkmate::test_numeric(x) |
| 481 |
) |
|
| 482 | 73x |
checkmate::assert( |
| 483 | 73x |
checkmate::test_character(y, len = length(x)) || |
| 484 | 73x |
checkmate::test_numeric(y, len = length(x)) |
| 485 |
) |
|
| 486 | 72x |
checkmate::assert_flag(match_na) |
| 487 | ||
| 488 |
# NA validation. |
|
| 489 | 72x |
na_x <- is.na(x) |
| 490 | 72x |
na_y <- is.na(y) |
| 491 | 72x |
if (any(na_x != na_y)) {
|
| 492 | 21x |
stop("NAs are present in 'x' and 'y', but not at identical positions.")
|
| 493 |
} |
|
| 494 |
# At this point, na_x and na_y are component-wise equal; |
|
| 495 |
# therefore, any(na_x | na_y) is equivalent to any(na_x). |
|
| 496 | 51x |
if (!match_na && any(na_x)) {
|
| 497 | 11x |
stop("NAs are present in 'x' and 'y', but 'match_na' is set to FALSE.")
|
| 498 |
} |
|
| 499 | ||
| 500 |
# Check bijection. |
|
| 501 | 40x |
n_unique_x <- length(unique(x)) |
| 502 | 40x |
n_unique_y <- length(unique(y)) |
| 503 | ||
| 504 | 40x |
bijection <- if (n_unique_x != n_unique_y) {
|
| 505 | 7x |
FALSE |
| 506 |
} else {
|
|
| 507 | 33x |
x_first_positions <- match(x, x) |
| 508 | 33x |
all(y == y[x_first_positions], na.rm = TRUE) |
| 509 |
} |
|
| 510 | ||
| 511 | 40x |
bijection |
| 512 |
} |
|
| 513 | ||
| 514 |
#' @title Create a factor with levels ordered by a separate ordering vector |
|
| 515 |
#' |
|
| 516 |
#' @description `r lifecycle::badge("stable")`
|
|
| 517 |
#' |
|
| 518 |
#' Converts a character vector or factor into a factor where the level order is |
|
| 519 |
#' determined by a second integer-like vector containing the corresponding order |
|
| 520 |
#' values. |
|
| 521 |
#' |
|
| 522 |
#' @details The values in `x` and `y` must define a bijection between the unique |
|
| 523 |
#' non-missing values: each unique value in `x` must correspond to exactly one |
|
| 524 |
#' unique value in `y`, and vice versa. |
|
| 525 |
#' Missing values are handled separately: `NA` values in `x` and `y` must |
|
| 526 |
#' occur at the same positions. Missing values are not included as factor |
|
| 527 |
#' levels. |
|
| 528 |
#' |
|
| 529 |
#' If `x` is already a factor containing unobserved (unused) levels, these |
|
| 530 |
#' unobserved levels will be dropped in the resulting factor. |
|
| 531 |
#' |
|
| 532 |
#' @param x (`character` or `factor`)\cr A vector to be converted to a factor. |
|
| 533 |
#' @param y (`integerish`)\cr A vector defining the order of levels in `x`. |
|
| 534 |
#' Must have the same length as `x`. |
|
| 535 |
#' @param ordered (`logical(1)`)\cr Indicates whether the result should be |
|
| 536 |
#' an ordered factor. Defaults to `FALSE`. |
|
| 537 |
#' |
|
| 538 |
#' @return A factor created from `x`, with levels ordered according to `y`. |
|
| 539 |
#' Attributes of `x` other than `class` and `levels` are preserved. |
|
| 540 |
#' If `ordered = TRUE`, an ordered factor is returned. |
|
| 541 |
#' If `x` is empty or consists entirely of missing values, a factor with |
|
| 542 |
#' no levels is returned. |
|
| 543 |
#' |
|
| 544 |
#' @author WW |
|
| 545 |
#' |
|
| 546 |
#' @export |
|
| 547 |
#' @examples |
|
| 548 |
#' factor_by_order(c("A", "A", "B"), c(1, 1, 2))
|
|
| 549 |
#' |
|
| 550 |
#' factor_by_order(c("A", "A", "B"), c(1, 1, 2), ordered = TRUE)
|
|
| 551 |
#' |
|
| 552 |
#' factor_by_order(c("A", "A", "B"), c(2, 2, 1))
|
|
| 553 |
#' |
|
| 554 |
#' factor_by_order(c("A", "A", "B", NA), c(1, 1, 2, NA))
|
|
| 555 |
#' |
|
| 556 |
#' \dontrun{
|
|
| 557 |
#' factor_by_order(c("A", "A", "B", NA), c(1, 2, 2, 4))
|
|
| 558 |
#' } |
|
| 559 |
#' |
|
| 560 |
factor_by_order <- function(x, y, ordered = FALSE) {
|
|
| 561 | 22x |
checkmate::assert_multi_class(x, classes = c("character", "factor"))
|
| 562 | 22x |
checkmate::assert_integerish(y, len = length(x)) |
| 563 | 20x |
checkmate::assert_flag(ordered) |
| 564 | ||
| 565 | 20x |
x_char <- as.character(x) |
| 566 | ||
| 567 | 20x |
if (!is_bijection(x_char, y, match_na = TRUE)) {
|
| 568 | 1x |
stop("`x` and `y` must define a bijection between their unique non-NA values; NA values must correspond.")
|
| 569 |
} |
|
| 570 | ||
| 571 | 18x |
factor_levels <- unique(x_char[order(y)]) |
| 572 | 18x |
factor_levels <- factor_levels[!is.na(factor_levels)] |
| 573 | 18x |
f <- factor(x_char, levels = factor_levels, ordered = ordered) |
| 574 | ||
| 575 |
# Preserve non-factor attributes of `x`. |
|
| 576 | 18x |
copy_attributes(source = x, target = f) |
| 577 |
} |
|
| 578 | ||
| 579 |
#' @title Strictly Match a Value in a Character Vector |
|
| 580 |
#' |
|
| 581 |
#' @description |
|
| 582 |
#' Finds a unique match of a value in either the odd or even positions of a |
|
| 583 |
#' character vector. An error is raised if no match or more than one match is |
|
| 584 |
#' found in the selected positions. |
|
| 585 |
#' |
|
| 586 |
#' @param x (`character(1)`)\cr |
|
| 587 |
#' The value to match. |
|
| 588 |
#' @param y (`character`)\cr |
|
| 589 |
#' The character vector in which to search for `x`. |
|
| 590 |
#' @param odd (`flag`)\cr |
|
| 591 |
#' Whether to restrict the match to odd positions. Defaults to `TRUE`. |
|
| 592 |
#' If `FALSE`, only even positions are considered. |
|
| 593 |
#' |
|
| 594 |
#' @return An integer containing the unique position of `x` in `y`. |
|
| 595 |
#' |
|
| 596 |
#' @keywords internal |
|
| 597 |
#' @author WW |
|
| 598 |
#' |
|
| 599 |
#' @examples |
|
| 600 |
#' \dontrun{
|
|
| 601 |
#' strict_match("A", c("A", "Placebo"))
|
|
| 602 |
#' |
|
| 603 |
#' strict_match("SEX", c("SomeVar", "SomeVal", "SEX", "Male"))
|
|
| 604 |
#' |
|
| 605 |
#' strict_match("ARM", c("SEX", "Male"))
|
|
| 606 |
#' strict_match("Male", c("SEX", "Male"))
|
|
| 607 |
#' strict_match("ARM", c("ARM", "Placebo", "ARM", "Active"))
|
|
| 608 |
#' } |
|
| 609 |
#' |
|
| 610 |
strict_match <- function(x, y, odd = TRUE) {
|
|
| 611 | 80x |
checkmate::assert_string(x) |
| 612 | 80x |
checkmate::assert_character(y, any.missing = FALSE) |
| 613 | 80x |
checkmate::assert_flag(odd) |
| 614 | ||
| 615 | 80x |
pos <- which(x == y) |
| 616 | ||
| 617 |
# odd = TRUE -> use 1L -> keep odd positions |
|
| 618 |
# odd = FALSE -> use 0L -> keep even positions |
|
| 619 | 80x |
pos <- if (odd) {
|
| 620 | 78x |
pos[pos %% 2L != 0L] |
| 621 |
} else {
|
|
| 622 | 2x |
pos[pos %% 2L == 0L] |
| 623 |
} |
|
| 624 | ||
| 625 | 80x |
if (length(pos) == 0L) {
|
| 626 | 3x |
stop(paste0( |
| 627 | 3x |
"Value ('",
|
| 628 | 3x |
x, |
| 629 | 3x |
"') not found in the ", |
| 630 | 3x |
ifelse(odd, "odd", "even"), |
| 631 | 3x |
" positions of ('",
|
| 632 | 3x |
paste(y, collapse = "."), |
| 633 |
"')." |
|
| 634 |
)) |
|
| 635 |
} |
|
| 636 | ||
| 637 | 77x |
if (length(pos) > 1L) {
|
| 638 | 1x |
stop(paste0( |
| 639 | 1x |
"Value ('",
|
| 640 | 1x |
x, |
| 641 | 1x |
"') must be unique in the ", |
| 642 | 1x |
ifelse(odd, "odd", "even"), |
| 643 | 1x |
" positions of ('",
|
| 644 | 1x |
paste(y, collapse = "."), |
| 645 |
"')." |
|
| 646 |
)) |
|
| 647 |
} |
|
| 648 | ||
| 649 | 76x |
pos |
| 650 |
} |
|
| 651 | ||
| 652 |
#' Safe Conversion to Logical Vector |
|
| 653 |
#' |
|
| 654 |
#' @param x (`vector`)\cr The input vector to be safely converted to a logical vector. |
|
| 655 |
#' @param na.rm (`flag`)\cr Whether to remove missing values after conversion. |
|
| 656 |
#' |
|
| 657 |
#' @return |
|
| 658 |
#' A logical vector corresponding to the input `x`. If the conversion introduces |
|
| 659 |
#' unexpected `NA` values, an error is raised. |
|
| 660 |
#' |
|
| 661 |
#' @keywords internal |
|
| 662 |
safe_as_logical <- function(x, na.rm = FALSE) {
|
|
| 663 | 56x |
checkmate::assert_vector(x, strict = TRUE) |
| 664 | 56x |
checkmate::assert_flag(na.rm) |
| 665 | 56x |
result <- as.logical(x) |
| 666 | 56x |
if (anyNA(result) && !identical(is.na(x), is.na(result))) {
|
| 667 | 1x |
stop("Conversion to logical introduced unexpected NAs.")
|
| 668 |
} |
|
| 669 | 55x |
if (anyNA(result)) {
|
| 670 | 7x |
if (na.rm) {
|
| 671 | 4x |
result <- result[!is.na(result)] |
| 672 |
} else {
|
|
| 673 | 3x |
stop("Missing values detected in response and `na.rm = FALSE`.", call. = FALSE)
|
| 674 |
} |
|
| 675 |
} |
|
| 676 | 52x |
result |
| 677 |
} |
| 1 |
#' Analysis Function for Response Variables |
|
| 2 |
#' |
|
| 3 |
#' This function calculates counts and percentages for response variables (Y/N values), |
|
| 4 |
#' with optional risk difference calculations. |
|
| 5 |
#' |
|
| 6 |
#' @param df (`data.frame`)\cr data set containing all analysis variables. |
|
| 7 |
#' @param .var (`string`)\cr variable name that is passed by `rtables`. |
|
| 8 |
#' @param .df_row (`data.frame`)\cr data frame across all of the columns for the given row split. |
|
| 9 |
#' @param .N_col (`integer`)\cr column-wise N (column count) for the full column being analyzed. |
|
| 10 |
#' @param .spl_context (`data.frame`)\cr gives information about ancestor split states. |
|
| 11 |
#' @param resp_var (`string`)\cr response variable name containing Y/N values. |
|
| 12 |
#' @param id (`string`)\cr subject variable name. |
|
| 13 |
#' @param drop_levels (`logical`)\cr if TRUE, non-observed levels will not be included. |
|
| 14 |
#' @param riskdiff (`logical`)\cr if TRUE, risk difference calculations will be performed. |
|
| 15 |
#' @param ref_path (`string`)\cr column path specifications for the control group. |
|
| 16 |
#' @param variables (`list`)\cr variables to include in the analysis. |
|
| 17 |
#' @param conf_level (`proportion`)\cr confidence level of the interval. |
|
| 18 |
#' @param method (`character`)\cr method for calculating confidence intervals. |
|
| 19 |
#' @param weights_method (`character`)\cr method for calculating weights. |
|
| 20 |
#' @param .formats (`character` or NULL)\cr formats to apply to the statistics. If NULL, default formats will be used. |
|
| 21 |
#' @param na_str (`character`)\cr string to use for NA values. Defaults to `rep("NA", 3)`.
|
|
| 22 |
#' @param legacy (`logical(1)`)\cr To allow for backward compatibility default of used format. |
|
| 23 |
#' @return Formatted analysis function which is used as `afun` in `analyze_vars()` |
|
| 24 |
#' and as `cfun` in `summarize_row_groups()`. |
|
| 25 |
#' |
|
| 26 |
#' @export |
|
| 27 |
#' |
|
| 28 |
#' @examples |
|
| 29 |
#' library(dplyr) |
|
| 30 |
#' ADSL <- ex_adsl |> select(USUBJID, ARM, SEX) |
|
| 31 |
#' |
|
| 32 |
#' ADAE <- ex_adae |> select(USUBJID, ARM, SEX, AEBODSYS, AEDECOD) |
|
| 33 |
#' |
|
| 34 |
#' ADAE <- ADAE |> |
|
| 35 |
#' mutate(TRTEMFL = "Y") |
|
| 36 |
#' |
|
| 37 |
#' lyt <- basic_table(show_colcounts = TRUE) |> |
|
| 38 |
#' split_cols_by("ARM") |>
|
|
| 39 |
#' analyze("SEX",
|
|
| 40 |
#' show_labels = "visible", |
|
| 41 |
#' afun = a_freq_resp_var_j, |
|
| 42 |
#' extra_args = list(resp_var = "TRTEMFL", riskdiff = FALSE) |
|
| 43 |
#' ) |
|
| 44 |
#' |
|
| 45 |
#' result <- build_table(lyt, df = ADAE, alt_counts_df = ADSL) |
|
| 46 |
#' |
|
| 47 |
#' result |
|
| 48 |
a_freq_resp_var_j <- function( |
|
| 49 |
df, |
|
| 50 |
.var, |
|
| 51 |
.df_row, |
|
| 52 |
.N_col, |
|
| 53 |
.spl_context, |
|
| 54 |
resp_var = NULL, |
|
| 55 |
id = "USUBJID", |
|
| 56 |
drop_levels = FALSE, |
|
| 57 |
riskdiff = TRUE, |
|
| 58 |
ref_path = NULL, |
|
| 59 |
variables = list(strata = NULL), |
|
| 60 |
conf_level = formals(s_proportion_diff)$conf_level, |
|
| 61 |
method = c( |
|
| 62 |
"wald", |
|
| 63 |
"waldcc", |
|
| 64 |
"cmh", |
|
| 65 |
"ha", |
|
| 66 |
"newcombe", |
|
| 67 |
"newcombecc", |
|
| 68 |
"strat_newcombe", |
|
| 69 |
"strat_newcombecc", |
|
| 70 |
"cmh_sato", |
|
| 71 |
"cmh_mn", |
|
| 72 |
"uncond_exact_diff" |
|
| 73 |
), |
|
| 74 |
weights_method = formals(s_proportion_diff)$weights_method, |
|
| 75 |
.formats = NULL, |
|
| 76 |
na_str = rep("NA", 3),
|
|
| 77 |
legacy = FALSE |
|
| 78 |
) {
|
|
| 79 |
# ---- Derive statistics: xx / xx (xx.x%) |
|
| 80 | ||
| 81 | 24x |
if (is.null(resp_var)) {
|
| 82 | 1x |
stop( |
| 83 | 1x |
"afun a_freq_resp_var_j: resp_var cannot be NULL." |
| 84 |
) |
|
| 85 |
} |
|
| 86 | ||
| 87 | 23x |
resp_var_values <- unique(df[[resp_var]][!is.na(df[[resp_var]])]) |
| 88 |
if ( |
|
| 89 | 23x |
is.character(df[[resp_var]]) && |
| 90 | 23x |
any(is.na(df[[resp_var]])) && |
| 91 | 23x |
all(resp_var_values == "Y") |
| 92 |
) {
|
|
| 93 | ! |
stop( |
| 94 | ! |
paste0( |
| 95 | ! |
"afun a_freq_resp_var_j: it is not clear if missing resp_var should be considered non-response. ", |
| 96 | ! |
"Please make it a factor with appropriate Y(/N) levels." |
| 97 |
) |
|
| 98 |
) |
|
| 99 |
} |
|
| 100 | ||
| 101 | 23x |
if (length(setdiff(resp_var_values, c("Y", "N"))) > 0) {
|
| 102 | 1x |
stop("afun a_freq_resp_var_j: resp_var must contain only Y/N values.")
|
| 103 |
} |
|
| 104 | ||
| 105 | 22x |
if (is.character(df[[resp_var]])) {
|
| 106 | 19x |
df[[resp_var]] <- factor(df[[resp_var]], levels = c("Y", "N"))
|
| 107 |
} |
|
| 108 | ||
| 109 | 22x |
df <- df[!is.na(df[[.var]]), ] |
| 110 | ||
| 111 |
# nolint start |
|
| 112 | 22x |
if ((is.factor(df[[resp_var]]) && |
| 113 | 22x |
(identical(levels(df[[resp_var]]), c("Y", "N")) || identical(levels(df[[resp_var]]), c("N", "Y")))) ||
|
| 114 | 22x |
is.character(df[[resp_var]]) |
| 115 |
) { # nolint end
|
|
| 116 |
# missing values in resp_var should be excluded, not considered as not met response |
|
| 117 |
# subject will then not contribute to denominator |
|
| 118 | 22x |
df <- df[!is.na(df[[resp_var]]), ] |
| 119 | 22x |
.df_row <- .df_row[!is.na(.df_row[[resp_var]]), ] |
| 120 |
} |
|
| 121 | ||
| 122 | 22x |
if (drop_levels) {
|
| 123 | 13x |
obs_levs <- unique(.df_row[[.var]]) |
| 124 | 13x |
obs_levs <- intersect(levels(.df_row[[.var]]), obs_levs) |
| 125 | ||
| 126 | 13x |
val_var <- obs_levs |
| 127 |
# restrict the levels to the ones specified in val_var |
|
| 128 | 13x |
df <- df[df[[.var]] %in% val_var, ] |
| 129 | 13x |
.df_row <- .df_row[.df_row[[.var]] %in% val_var, ] |
| 130 | ||
| 131 | 13x |
df <- h_update_factor(df, .var, val_var) |
| 132 |
} |
|
| 133 | ||
| 134 | 22x |
varvec <- df[[.var]] |
| 135 | ||
| 136 | 22x |
levs <- if (is.factor(varvec)) levels(varvec) else sort(unique(varvec)) |
| 137 | ||
| 138 | 22x |
colid <- .spl_context$cur_col_id[[1]] |
| 139 | 22x |
inriskdiffcol <- grepl("difference", tolower(colid), fixed = TRUE)
|
| 140 | ||
| 141 | 22x |
if (riskdiff) {
|
| 142 | 10x |
trt_var <- ref_path[length(ref_path) - 1L] |
| 143 | 10x |
ctrl_grp <- ref_path[length(ref_path)] |
| 144 | 10x |
cur_trt_grp <- h_get_cur_trt_grp(trt_var, .spl_context) |
| 145 |
} |
|
| 146 | ||
| 147 | 22x |
fn <- function(levii) {
|
| 148 | 82x |
dfii <- df[df[[.var]] == levii, ] |
| 149 | 82x |
dfrowii <- .df_row[.df_row[[.var]] == levii, ] |
| 150 | ||
| 151 | 82x |
if (!inriskdiffcol) {
|
| 152 |
# use the core s_freq_j function on the current level of the incoming variable (.var) |
|
| 153 |
# note that the response variable will become .var in the below call |
|
| 154 |
# val is restricted to Y to show number of response on the current level of .var |
|
| 155 | 70x |
rslt <- s_freq_j( |
| 156 | 70x |
dfii, |
| 157 | 70x |
.df_row = dfrowii, |
| 158 | 70x |
.var = resp_var, |
| 159 | 70x |
alt_df = NULL, |
| 160 | 70x |
parent_df = NULL, |
| 161 | 70x |
val = "Y", |
| 162 | 70x |
denom = "n_df" |
| 163 |
) |
|
| 164 | ||
| 165 | 70x |
.stat <- "count_unique_denom_fraction" |
| 166 | 70x |
x_stat <- rslt[[.stat]]$Y |
| 167 |
# use .formats if provided, otherwise default to jjcsformat_count_denom_fraction |
|
| 168 | 70x |
if (!legacy) {
|
| 169 | 70x |
fmt <- if (is.null(.formats)) jjcsformat_count_denom_fraction else .formats |
| 170 |
} else {
|
|
| 171 | ! |
fmt <- if (is.null(.formats)) jjcsformat_count_denom_fraction_legacy else .formats |
| 172 |
} |
|
| 173 | ||
| 174 | 70x |
rslt <- rcell(x_stat, format = fmt) |
| 175 |
} else {
|
|
| 176 |
# use the risk difference function s_risk_diff_val_j on the current level of the incoming variable (.var) |
|
| 177 |
# note that the response variable will become .var in the below call |
|
| 178 |
# val is restricted to Y to show number of response on the current level of .var |
|
| 179 | 12x |
denom_df <- dfrowii |
| 180 |
# for combined facet, denom_df value for the treatment group needs update |
|
| 181 | 12x |
denom_df <- upd_denom_df_combo( |
| 182 | 12x |
denom_df, |
| 183 | 12x |
trt_var, |
| 184 | 12x |
cur_trt_grp, |
| 185 | 12x |
.spl_context |
| 186 |
) |
|
| 187 | ||
| 188 | 12x |
rslt <- s_risk_diff_val_j( |
| 189 | 12x |
df = dfii, |
| 190 | 12x |
.var = resp_var, |
| 191 | 12x |
.df_row = dfrowii, |
| 192 | 12x |
val = "Y", |
| 193 | 12x |
denom_df = denom_df, |
| 194 | 12x |
id = id, |
| 195 | 12x |
riskdiff = riskdiff, |
| 196 |
# treatment/ref group related arguments |
|
| 197 | 12x |
trt_var = trt_var, |
| 198 | 12x |
ctrl_grp = ctrl_grp, |
| 199 | 12x |
cur_trt_grp = cur_trt_grp, |
| 200 |
# relrisk specific arguments |
|
| 201 | 12x |
variables = variables, |
| 202 | 12x |
conf_level = conf_level, |
| 203 | 12x |
method = method, |
| 204 | 12x |
weights_method = weights_method |
| 205 |
) |
|
| 206 | 12x |
x_stat <- rslt[["rr_ci_3d"]]$Y |
| 207 |
# use .formats if provided, otherwise default to rr format |
|
| 208 | 12x |
fmt <- if (is.null(.formats)) jjcsformat_xx("xx.x (xx.x, xx.x)") else .formats
|
| 209 | 12x |
rslt <- rcell( |
| 210 | 12x |
x_stat, |
| 211 | 12x |
format = fmt, |
| 212 | 12x |
format_na_str = na_str |
| 213 |
) |
|
| 214 |
} |
|
| 215 | ||
| 216 |
# rslt is a single rcell row |
|
| 217 | 82x |
return(rslt) |
| 218 |
} |
|
| 219 | ||
| 220 |
### apply function fn to all levels of incoming .var |
|
| 221 |
### cls is a list of single rcell rows - ie one line per level is presented |
|
| 222 | 22x |
cls <- lapply(levs, fn) |
| 223 | ||
| 224 | 22x |
names(cls) <- levs |
| 225 | 22x |
return(cls) |
| 226 |
} |
| 1 |
#' rm_other_facets_fact |
|
| 2 |
#' @param nm character. names of facets to keep. all other facets will be |
|
| 3 |
#' removed |
|
| 4 |
#' @returns a function suitable for use within the `post` portion make_split_fun |
|
| 5 |
rm_other_facets_fact <- function(nm) {
|
|
| 6 | 2x |
function(ret, spl, .spl_context, fulldf) {
|
| 7 | 6x |
keep <- which(names(ret$values) %in% nm) |
| 8 | 6x |
stopifnot(length(keep) > 0) |
| 9 |
# values already have subsetting expressions on them |
|
| 10 | 6x |
make_split_result(ret$values[keep], ret$datasplit[keep], ret$labels[keep]) |
| 11 |
} |
|
| 12 |
} |
|
| 13 | ||
| 14 |
#' @name real_add_overall_facet |
|
| 15 |
#' |
|
| 16 |
#' @title Add Overall Facet |
|
| 17 |
#' |
|
| 18 |
#' @description |
|
| 19 |
#' A function to help add an overall facet to your tables. |
|
| 20 |
#' |
|
| 21 |
#' @param name (`character`)\cr Name/virtual 'value' for the new facet. |
|
| 22 |
#' @param label (`character`)\cr Label for the new facet. |
|
| 23 |
#' |
|
| 24 |
#' @note Current add_overall_facet is bugged. Can be used directly after it's fixed |
|
| 25 |
#' https://github.com/pharmaverse/rtables/issues/768 |
|
| 26 |
#' @examples |
|
| 27 |
#' splfun <- make_split_fun(post = list(real_add_overall_facet("Total", "Total")))
|
|
| 28 |
#' |
|
| 29 |
#' @export |
|
| 30 |
#' @returns Function usable directly as a split function. |
|
| 31 |
real_add_overall_facet <- function(name, label) {
|
|
| 32 | 2x |
function(ret, spl, .spl_context, fulldf) {
|
| 33 | 6x |
add_to_split_result( |
| 34 | 6x |
ret, |
| 35 | 6x |
values = name, |
| 36 | 6x |
datasplit = stats::setNames(list(fulldf), name), |
| 37 | 6x |
labels = stats::setNames(label, name), |
| 38 | 6x |
subset_exprs = quote(TRUE) |
| 39 |
) |
|
| 40 |
} |
|
| 41 |
} |
|
| 42 | ||
| 43 |
#' @name make_combo_splitfun |
|
| 44 |
#' |
|
| 45 |
#' @title Split Function Helper |
|
| 46 |
#' |
|
| 47 |
#' @description |
|
| 48 |
#' A function which aids the construction for users to create their own split function for combined columns. |
|
| 49 |
#' @param nm (`character`)\cr Name/virtual 'value' for the new facet. |
|
| 50 |
#' @param label (`character`)\cr Label for the new facet. |
|
| 51 |
#' @param levels (`character` or NULL)\cr The levels to combine into the new facet, |
|
| 52 |
#' or NULL, indicating the facet should include all incoming data. |
|
| 53 |
#' @param rm_other_facets (`logical`)\cr Should facets other than the newly |
|
| 54 |
#' created one be removed. Defaults to `TRUE`. |
|
| 55 |
#' @export |
|
| 56 |
#' @returns Function usable directly as a split function. |
|
| 57 |
#' @examples |
|
| 58 |
#' aesevall_spf <- make_combo_splitfun(nm = "AESEV_ALL", label = "Any AE", levels = NULL) |
|
| 59 |
make_combo_splitfun <- function(nm, label = nm, levels = NULL, rm_other_facets = TRUE) {
|
|
| 60 | 2x |
if (is.null(levels)) {
|
| 61 | 1x |
fn <- real_add_overall_facet(name = nm, label = label) |
| 62 |
} else {
|
|
| 63 | 1x |
fn <- add_combo_facet(name = nm, label = label, levels = levels) |
| 64 |
} |
|
| 65 | 2x |
if (rm_other_facets) {
|
| 66 | 2x |
rmfun <- rm_other_facets_fact(nm) |
| 67 |
} else {
|
|
| 68 | ! |
rmfun <- NULL |
| 69 |
} |
|
| 70 | 2x |
make_split_fun(post = c(list(fn), if (rm_other_facets) list(rmfun))) |
| 71 |
} |
|
| 72 | ||
| 73 |
blank_regex <- "^[[:space:]]*$" |
|
| 74 | ||
| 75 |
combine_nonblank <- function(name, label) {
|
|
| 76 | 1x |
function(ret, spl, .spl_context, fulldf) {
|
| 77 | 1x |
df <- fulldf[!grepl(blank_regex, fulldf[[spl_variable(spl)]]), ] |
| 78 | 1x |
add_to_split_result( |
| 79 | 1x |
ret, |
| 80 | 1x |
values = name, |
| 81 | 1x |
datasplit = stats::setNames(list(df), name), |
| 82 | 1x |
labels = stats::setNames(label, name) |
| 83 |
) |
|
| 84 |
} |
|
| 85 |
} |
|
| 86 | ||
| 87 |
rm_blank_levels <- function(df, spl, ...) {
|
|
| 88 | 1x |
var <- spl_variable(spl) |
| 89 | 1x |
varvec <- df[[var]] |
| 90 | 1x |
oldlevs <- levels(varvec) |
| 91 | 1x |
newlevs <- oldlevs[!grepl(blank_regex, oldlevs)] |
| 92 | 1x |
df <- df[!grepl(blank_regex, varvec), ] |
| 93 | 1x |
df[[var]] <- factor(df[[var]], levels = newlevs) |
| 94 | 1x |
df |
| 95 |
} |
|
| 96 | ||
| 97 |
find_torm <- function(spl_ret, torm, torm_regex, keep_matches) {
|
|
| 98 | 59x |
if (!is.null(torm)) {
|
| 99 | 59x |
ans_lgl <- names(spl_ret$datasplit) %in% torm |
| 100 |
} else {
|
|
| 101 | ! |
ans_lgl <- grepl(torm_regex, names(spl_ret$datasplit)) |
| 102 |
} |
|
| 103 | 59x |
if (keep_matches) {
|
| 104 | 32x |
ans_lgl <- !ans_lgl |
| 105 |
} |
|
| 106 | 59x |
which(ans_lgl) |
| 107 |
} |
|
| 108 | ||
| 109 |
.check_rem_cond <- function(cond_str, cond_regex, spl_ctx, pos, type) {
|
|
| 110 | 118x |
if (is.null(cond_str) && is.null(cond_regex)) {
|
| 111 | 32x |
return(TRUE) |
| 112 |
} |
|
| 113 | 86x |
if (!is.null(cond_str) && !is.null(cond_regex)) {
|
| 114 | ! |
stop( |
| 115 | ! |
"Got both ", |
| 116 | ! |
paste(paste0(type, c("", "_regex")), collapse = " and "),
|
| 117 | ! |
". Please specify at most one of these." |
| 118 |
) |
|
| 119 |
} |
|
| 120 | 86x |
ctx_data <- spl_ctx[[type]][pos] |
| 121 | 86x |
if (!is.null(cond_str)) {
|
| 122 | 86x |
any(cond_str %in% ctx_data) |
| 123 |
} else {
|
|
| 124 | ! |
any(grepl(cond_regex, ctx_data)) |
| 125 |
} |
|
| 126 |
} |
|
| 127 | ||
| 128 |
## handle support for NA and negative positions only called once but code is nicer with it factored out here |
|
| 129 |
resolve_ancestor_pos <- function(anc_pos, numrows) {
|
|
| 130 | 59x |
if (is.na(anc_pos)) {
|
| 131 | 26x |
anc_pos <- seq_len(numrows) |
| 132 | 33x |
} else if (any(anc_pos < 0)) {
|
| 133 | 1x |
if (!all(anc_pos < 0)) {
|
| 134 | ! |
stop("Got mix of negative and non-negative values for ancestor_pos; ", "this is not supported.")
|
| 135 |
} |
|
| 136 |
## direct parent is actually NROW(.spl_context) so avoid off-by-1 error with the + 1 here |
|
| 137 | 1x |
anc_pos <- numrows - (anc_pos + 1) |
| 138 |
} |
|
| 139 | 59x |
anc_pos |
| 140 |
} |
|
| 141 | ||
| 142 |
#' @name cond_rm_facets |
|
| 143 |
#' @title Conditional Removal of Facets |
|
| 144 |
#' |
|
| 145 |
#' @param facets (`character` or NULL)\cr Vector of facet names to be removed |
|
| 146 |
#' if condition(s) are met |
|
| 147 |
#' @param facets_regex (`character`)\cr Regular expression to identify facet |
|
| 148 |
#' names to be removed if condition(s) are met. |
|
| 149 |
#' @param ancestor_pos (`numeric`)\cr Row in spl_context to check the condition |
|
| 150 |
#' within. E.g., 1 represents the first split, 2 represents the second split |
|
| 151 |
#' nested within the first, etc. NA specifies that the conditions |
|
| 152 |
#' should be checked at all split levels. Negative integers indicate position |
|
| 153 |
#' counting back from the current one, e.g., -1 indicates the direct parent |
|
| 154 |
#' (most recent split before this one). Negative and positive/NA positions |
|
| 155 |
#' cannot be mixed. |
|
| 156 |
#' @param split (`character` or NULL)\cr If specified, name of the split |
|
| 157 |
#' at position `ancestor_pos` must be identical to this value for |
|
| 158 |
#' the removal condition to be met. |
|
| 159 |
#' @param split_regex (`character` or NULL)\cr If specified, a regular expression |
|
| 160 |
#' the name of the split at position `ancestor_pos` must match for |
|
| 161 |
#' the removal condition to be met. Cannot be specified at the same time |
|
| 162 |
#' as `split`. |
|
| 163 |
#' @param value (`character` or NULL)\cr If specified, split (facet) value |
|
| 164 |
#' at position `ancestor_pos` must be identical to this value for |
|
| 165 |
#' removal condition to be met. |
|
| 166 |
#' @param value_regex (`character` or NULL)\cr If specified, a regular expression |
|
| 167 |
#' the value of the split at position `ancestor_pos` must match for |
|
| 168 |
#' the removal condition to be met. Cannot be specified at the same time |
|
| 169 |
#' as `value`. |
|
| 170 |
#' @param keep_matches (`logical`)\cr Given the specified condition is met, |
|
| 171 |
#' should the facets removed be those matching `facets`/`facets_regex` |
|
| 172 |
#' (`FALSE`, the default), or those *not* matching (`TRUE`). |
|
| 173 |
#' |
|
| 174 |
#' @details Facet removal occurs when the specified condition(s) |
|
| 175 |
#' on the split(s) and or value(s) are met within at least one |
|
| 176 |
#' of the split_context rows indicated by `ancestor_pos`; otherwise |
|
| 177 |
#' the set of facets is returned unchanged. |
|
| 178 |
#' |
|
| 179 |
#' If facet removal is performed, either *all* facets which match `facets` (or |
|
| 180 |
#' `facets_regex` will be removed ( the default `keep_matches == FALSE` |
|
| 181 |
#' case), or all *non-matching* facets will be removed (when |
|
| 182 |
#' `keep_matches_only == TRUE`). |
|
| 183 |
#' |
|
| 184 |
#' @note A degenerate table is likely to be returned if all facets |
|
| 185 |
#' are removed. |
|
| 186 |
#' |
|
| 187 |
#' @returns A function suitable for use in `make_split_fun`'s |
|
| 188 |
#' `post` argument which encodes the specified condition. |
|
| 189 |
#' @export |
|
| 190 |
#' @examples |
|
| 191 |
#' |
|
| 192 |
#' rm_a_from_placebo <- cond_rm_facets( |
|
| 193 |
#' facets = "A", |
|
| 194 |
#' ancestor_pos = NA, |
|
| 195 |
#' value_regex = "Placeb", |
|
| 196 |
#' split = "ARM" |
|
| 197 |
#' ) |
|
| 198 |
#' mysplit <- make_split_fun(post = list(rm_a_from_placebo)) |
|
| 199 |
#' |
|
| 200 |
#' lyt <- basic_table() |> |
|
| 201 |
#' split_cols_by("ARM") |>
|
|
| 202 |
#' split_cols_by("STRATA1", split_fun = mysplit) |>
|
|
| 203 |
#' analyze("AGE", mean, format = "xx.x")
|
|
| 204 |
#' build_table(lyt, ex_adsl) |
|
| 205 |
#' |
|
| 206 |
#' rm_bc_from_combo <- cond_rm_facets( |
|
| 207 |
#' facets = c("B", "C"),
|
|
| 208 |
#' ancestor_pos = -1, |
|
| 209 |
#' value_regex = "Combi" |
|
| 210 |
#' ) |
|
| 211 |
#' mysplit2 <- make_split_fun(post = list(rm_bc_from_combo)) |
|
| 212 |
#' |
|
| 213 |
#' lyt2 <- basic_table() |> |
|
| 214 |
#' split_cols_by("ARM") |>
|
|
| 215 |
#' split_cols_by("STRATA1", split_fun = mysplit2) |>
|
|
| 216 |
#' analyze("AGE", mean, format = "xx.x")
|
|
| 217 |
#' tbl2 <- build_table(lyt2, ex_adsl) |
|
| 218 |
#' tbl2 |
|
| 219 |
#' |
|
| 220 |
#' rm_bc_from_combo2 <- cond_rm_facets( |
|
| 221 |
#' facets_regex = "^A$", |
|
| 222 |
#' ancestor_pos = -1, |
|
| 223 |
#' value_regex = "Combi", |
|
| 224 |
#' keep_matches = TRUE |
|
| 225 |
#' ) |
|
| 226 |
#' mysplit3 <- make_split_fun(post = list(rm_bc_from_combo2)) |
|
| 227 |
#' |
|
| 228 |
#' lyt3 <- basic_table() |> |
|
| 229 |
#' split_cols_by("ARM") |>
|
|
| 230 |
#' split_cols_by("STRATA1", split_fun = mysplit3) |>
|
|
| 231 |
#' analyze("AGE", mean, format = "xx.x")
|
|
| 232 |
#' tbl3 <- build_table(lyt3, ex_adsl) |
|
| 233 |
#' |
|
| 234 |
#' stopifnot(identical(cell_values(tbl2), cell_values(tbl3))) |
|
| 235 |
cond_rm_facets <- function( |
|
| 236 |
facets = NULL, |
|
| 237 |
facets_regex = NULL, |
|
| 238 |
ancestor_pos = 1, |
|
| 239 |
split = NULL, |
|
| 240 |
split_regex = NULL, |
|
| 241 |
value = NULL, |
|
| 242 |
value_regex = NULL, |
|
| 243 |
keep_matches = FALSE |
|
| 244 |
) {
|
|
| 245 |
## detect errors/miscalling which don't even require us to have the facets |
|
| 246 | 29x |
if (is.null(split) && is.null(split_regex) && is.null(value) && is.null(value_regex)) {
|
| 247 | 1x |
stop( |
| 248 | 1x |
"Must specify condition in terms of at least one of ", |
| 249 | 1x |
"split name (split or split_regex) or ", |
| 250 | 1x |
"facet value (value or value_regex)." |
| 251 |
) |
|
| 252 |
} |
|
| 253 | 28x |
if (is.null(facets) && is.null(facets_regex)) {
|
| 254 | 1x |
stop("Must specify facets either facets or facets_regex, got neither.")
|
| 255 | 27x |
} else if (!is.null(facets) && !is.null(facets_regex)) {
|
| 256 | 1x |
stop("Got both facets and facets_regex, this is not supported, please specify only one.")
|
| 257 |
} |
|
| 258 | 26x |
function(ret, spl, .spl_context, fulldf) {
|
| 259 | 59x |
torm_ind <- c() |
| 260 | 59x |
ancestor_pos <- resolve_ancestor_pos(ancestor_pos, NROW(.spl_context)) |
| 261 | 59x |
torm_ind <- find_torm(ret, facets, facets_regex, keep_matches = keep_matches) |
| 262 | 59x |
fct_abbrev <- ifelse(is.null(facets_regex), paste(facets, collapse = ", "), facets_regex) |
| 263 | 59x |
split_match <- .check_rem_cond( |
| 264 | 59x |
split, split_regex, .spl_context, ancestor_pos, type = "split" |
| 265 |
) |
|
| 266 | 59x |
value_match <- .check_rem_cond( |
| 267 | 59x |
value, value_regex, .spl_context, ancestor_pos, type = "value" |
| 268 |
) |
|
| 269 | 59x |
if (split_match && value_match) {
|
| 270 | 33x |
no_rm_msg <- paste0( |
| 271 | 33x |
"No facets matched removal criteria [", fct_abbrev, "] ", |
| 272 | 33x |
"in function created with cond_rm_facets.\n", |
| 273 | 33x |
"Occured at path: ", |
| 274 | 33x |
rtables::spl_context_to_disp_path(.spl_context) |
| 275 |
) |
|
| 276 | ||
| 277 | 33x |
all_rm_msg <- paste0( |
| 278 | 33x |
"All facets matched removal criteria [", |
| 279 | 33x |
fct_abbrev, "] in function created with cond_rm_facets. ", |
| 280 | 33x |
"This will result in a degenerate table (if the condition ", |
| 281 | 33x |
"is met) within row splitting and in table-creation failing ", |
| 282 | 33x |
"entirely in column splitting.\n", |
| 283 | 33x |
"Occured at path: ", |
| 284 | 33x |
rtables::spl_context_to_disp_path(.spl_context) |
| 285 |
) |
|
| 286 | 33x |
no_rm_case <- (length(torm_ind) == 0 && !keep_matches) || |
| 287 | 33x |
(length(torm_ind) == length(ret$datasplit) && keep_matches) |
| 288 | 33x |
all_rm_case <- (length(torm_ind) == 0 && keep_matches) || |
| 289 | 33x |
(length(torm_ind) == length(ret$datasplit) && !keep_matches) |
| 290 | ||
| 291 | 33x |
if (no_rm_case) {
|
| 292 | ! |
warning(no_rm_msg, call. = FALSE) |
| 293 | 33x |
} else if (all_rm_case) {
|
| 294 | ! |
warning(all_rm_msg, call. = FALSE) |
| 295 |
} |
|
| 296 |
## find_torm handles the keep matching case, so |
|
| 297 |
## by this point torm_ind is always the ones to remove |
|
| 298 | 33x |
ret <- lapply(ret, function(part) part[-torm_ind]) |
| 299 |
} |
|
| 300 | ||
| 301 | 59x |
ret |
| 302 |
} |
|
| 303 |
} |
|
| 304 | ||
| 305 | ||
| 306 |
#' @name rm_levels |
|
| 307 |
#' |
|
| 308 |
#' @title Removal of Levels |
|
| 309 |
#' |
|
| 310 |
#' @description |
|
| 311 |
#' Custom function for removing level inside pre step in make_split_fun. |
|
| 312 |
#' |
|
| 313 |
#' @param excl (`character`)\cr Choose which level(s) to remove |
|
| 314 |
#' @return A function implementing pre-processing split behavior (for use in |
|
| 315 |
#' `make_split_fun(pre = )` which removes the levels in `excl` from the data |
|
| 316 |
#' before facets are generated. |
|
| 317 |
#' @export |
|
| 318 |
rm_levels <- function(excl) {
|
|
| 319 | 2x |
function(df, spl, ...) {
|
| 320 | 2x |
var <- spl_variable(spl) |
| 321 | 2x |
varvec <- df[[var]] |
| 322 | 2x |
oldlevs <- levels(varvec) |
| 323 | ||
| 324 | 2x |
exclevs <- oldlevs %in% excl |
| 325 | 2x |
newlevs <- oldlevs[!exclevs] |
| 326 | 2x |
df[[var]] <- factor(df[[var]], levels = newlevs) |
| 327 | 2x |
df |
| 328 |
} |
|
| 329 |
} |
|
| 330 | ||
| 331 |
#' Shortcut for Creating Custom Column Splits |
|
| 332 |
#' |
|
| 333 |
#' This is a shortcut for a common use of [rtables::make_split_result()] where you need to create |
|
| 334 |
#' custom column splits with different labels but using the same full dataset for each column. |
|
| 335 |
#' It automatically sets up the values, datasplit (using the same full dataset for each column), |
|
| 336 |
#' and subset_exprs (using TRUE for all subsets) parameters for make_split_result(). |
|
| 337 |
#' |
|
| 338 |
#' @param ... sequence of named labels for the columns. |
|
| 339 |
#' @param fulldf (`data.frame`)\cr The `fulldf` which will be used for each column. |
|
| 340 |
#' @return The result from [rtables::make_split_result()]. |
|
| 341 |
#' |
|
| 342 |
#' @keywords internal |
|
| 343 |
short_split_result <- function(..., fulldf) {
|
|
| 344 | 48x |
labels <- c(...) |
| 345 | 48x |
values <- stats::setNames(names(labels), names(labels)) |
| 346 | 48x |
datasplit <- stats::setNames(replicate(n = length(labels), list(fulldf)), names(labels)) |
| 347 | 48x |
subset_exprs <- replicate(n = length(labels), list(expression(TRUE))) |
| 348 | 48x |
make_split_result(values = values, labels = labels, datasplit = datasplit, subset_exprs = subset_exprs) |
| 349 |
} |
|
| 350 | ||
| 351 |
#' Predicate to Check if Split Should be Excluded |
|
| 352 |
#' |
|
| 353 |
#' @inheritParams proposal_argument_convention |
|
| 354 |
#' @returns `TRUE` if the current split context matches any of the exclude levels, |
|
| 355 |
#' `FALSE` otherwise. |
|
| 356 |
#' |
|
| 357 |
#' @export |
|
| 358 |
#' @examples |
|
| 359 |
#' |
|
| 360 |
#' do_exclude_split( |
|
| 361 |
#' exclude_levels = list(AVISIT = "Baseline"), |
|
| 362 |
#' .spl_context = data.frame( |
|
| 363 |
#' split = c("AVISIT", "ARM"),
|
|
| 364 |
#' value = c("Week 4", "Placebo")
|
|
| 365 |
#' ) |
|
| 366 |
#' ) |
|
| 367 |
#' do_exclude_split( |
|
| 368 |
#' exclude_levels = list(AVISIT = "Baseline"), |
|
| 369 |
#' .spl_context = data.frame( |
|
| 370 |
#' split = c("AVISIT", "ARM"),
|
|
| 371 |
#' value = c("Baseline", "Placebo")
|
|
| 372 |
#' ) |
|
| 373 |
#' ) |
|
| 374 |
do_exclude_split <- function(exclude_levels, .spl_context) {
|
|
| 375 | 19x |
checkmate::assert_list(exclude_levels, names = "unique") |
| 376 | 19x |
checkmate::assert_data_frame(.spl_context) |
| 377 | ||
| 378 | 19x |
split_vals <- .spl_context[, c("split", "value")]
|
| 379 | 19x |
split_vals_list <- as.list(split_vals$value) |> |
| 380 | 19x |
stats::setNames(split_vals$split) |
| 381 | 19x |
found_splits <- intersect(names(split_vals_list), names(exclude_levels)) |
| 382 | 19x |
for (split_var in found_splits) {
|
| 383 | 20x |
if (any(split_vals_list[[split_var]] %in% exclude_levels[[split_var]])) {
|
| 384 | 10x |
return(TRUE) |
| 385 |
} |
|
| 386 |
} |
|
| 387 | 9x |
FALSE |
| 388 |
} |
|
| 389 | ||
| 390 | ||
| 391 | ||
| 392 | ||
| 393 |
insert_subset_exprs <- function(partinfo, spl, comp_path = NULL) {
|
|
| 394 | 25x |
rvs <- rawvalues(partinfo$values) |
| 395 | 25x |
names(rvs) <- names(partinfo$values) |
| 396 | 25x |
exprs <- lapply(rvs, function(rvi) make_subset_expr(spl, rvi)) |
| 397 | 25x |
newvals <- mapply(function(val, expr) {
|
| 398 | 100x |
exvals <- rtables::splv_extra(val) |
| 399 | 100x |
if (!is.null(rtables::value_expr(val))) {
|
| 400 | ! |
return(val) |
| 401 |
} |
|
| 402 | 100x |
val@subset_expression <- expr |
| 403 | 100x |
val |
| 404 |
}, |
|
| 405 | 25x |
val = partinfo$values, |
| 406 | 25x |
expr = exprs, |
| 407 | 25x |
SIMPLIFY = FALSE) |
| 408 | 25x |
names(newvals) <- names(partinfo$values) |
| 409 | ||
| 410 | 25x |
names(exprs) <- names(rvs) |
| 411 | 25x |
make_split_result( |
| 412 |
## names(partinfo$values), ## AllLevelsSentinel is not playing nice here but should be handled by expr |
|
| 413 | 25x |
newvals, |
| 414 | 25x |
partinfo$datasplit, |
| 415 | 25x |
partinfo$labels |
| 416 |
) |
|
| 417 |
} |
| 1 |
#' @name summary_subset |
|
| 2 |
#' |
|
| 3 |
#' @title Wrappers Around `tern::a_summary()` with Optional Subsetting and Labels |
|
| 4 |
#' |
|
| 5 |
#' @description `r lifecycle::badge("stable")`
|
|
| 6 |
#' |
|
| 7 |
#' These wrappers around [tern::a_summary()] allow optional row subsetting |
|
| 8 |
#' using a logical expression evaluated in the context of the input data frame. |
|
| 9 |
#' |
|
| 10 |
#' `a_summary_subset()` is an [rtables] analysis function (see |
|
| 11 |
#' [rtables::analyze()]), while `c_summary_subset_label()` is a content row |
|
| 12 |
#' function (see [rtables::summarize_row_groups()]) that may optionally |
|
| 13 |
#' append a label to the resulting content row. |
|
| 14 |
#' |
|
| 15 |
#' @details |
|
| 16 |
#' [tern::a_summary()] is a formatted analysis function that computes summary |
|
| 17 |
#' statistics for a single variable. In typical use within the [rtables] |
|
| 18 |
#' framework, the analysis variable is first extracted from a data frame and |
|
| 19 |
#' then passed to [tern::a_summary()] as a standalone vector. |
|
| 20 |
#' |
|
| 21 |
#' The `a_summary_subset()` and `c_summary_subset_label()` wrappers extend |
|
| 22 |
#' this behavior by: |
|
| 23 |
#' - accepting a data frame (`df`) as input, |
|
| 24 |
#' - optionally subsetting rows using a logical expression, |
|
| 25 |
#' - extracting the analysis variable `.var` after subsetting, |
|
| 26 |
#' - and passing the resulting vector to [tern::a_summary()]. |
|
| 27 |
#' |
|
| 28 |
#' @inheritParams proposal_argument_convention |
|
| 29 |
#' @param filter_expr (`expression`) |
|
| 30 |
#' A single logical expression wrapped in `expression(...)` used to subset |
|
| 31 |
#' rows of `df` before summarization. Exactly one expression is allowed. |
|
| 32 |
#' The expression is evaluated using [eval()] in an environment containing |
|
| 33 |
#' the columns of `df`, meaning that all column names are available as |
|
| 34 |
#' variables. |
|
| 35 |
#' |
|
| 36 |
#' In the current implementation, variables used in the expression can only |
|
| 37 |
#' be column names of `df`. That is, the following is not supported: |
|
| 38 |
#' `min_age <- 45;` |
|
| 39 |
#' `a_summary_subset(df, "AVAL", expression(AGE > min_age))`, |
|
| 40 |
#' given that `AVAL` is a column in `df` and `min_age` is not a column in `df`. |
|
| 41 |
#' |
|
| 42 |
#' The expression must evaluate to a logical vector whose length is exactly |
|
| 43 |
#' equal to `nrow(df)`. No recycling is performed. |
|
| 44 |
#' In particular, scalar logical values such as `expression(TRUE)` or |
|
| 45 |
#' `expression(FALSE)` are not allowed when `nrow(df) > 1`. |
|
| 46 |
#' @param ... Additional arguments passed to [tern::a_summary()], excluding |
|
| 47 |
#' the `x` argument, which is constructed internally from `df[[.var]]` |
|
| 48 |
#' after applying the filter. |
|
| 49 |
#' |
|
| 50 |
#' @return |
|
| 51 |
#' Returns the same type of object as [tern::a_summary()]. |
|
| 52 |
#' |
|
| 53 |
#' @author WW |
|
| 54 |
#' |
|
| 55 |
#' @seealso |
|
| 56 |
#' [tern::a_summary()] |
|
| 57 |
#' |
|
| 58 |
NULL |
|
| 59 | ||
| 60 |
#' @describeIn summary_subset Analysis function with optional subsetting. |
|
| 61 |
#' @importFrom tern a_summary |
|
| 62 |
#' @importFrom assertthat is.string |
|
| 63 |
#' @export |
|
| 64 |
#' @examples |
|
| 65 |
#' |
|
| 66 |
#' ## Analysis function: |
|
| 67 |
#' |
|
| 68 |
#' df <- data.frame( |
|
| 69 |
#' ID = 1:8, |
|
| 70 |
#' AGE = c(34, NA, 52, 45, 50, NA, 41, 29), |
|
| 71 |
#' AVAL = c(1.2, 3, -1.5, 0.8, 3.5, 2, -1.1, 0.3) |
|
| 72 |
#' ) |
|
| 73 |
#' df |
|
| 74 |
#' |
|
| 75 |
#' stats <- c("n", "sum")
|
|
| 76 |
#' |
|
| 77 |
#' tern::a_summary(df$AVAL, .stats = stats) |
|
| 78 |
#' |
|
| 79 |
#' a_summary_subset(df, "AVAL", expression(!is.na(AGE)), .stats = stats) |
|
| 80 |
#' a_summary_subset(df, "AVAL", expression(AGE > 30 & AVAL > 0), .stats = stats) |
|
| 81 |
#' |
|
| 82 |
a_summary_subset <- function(df, .var, filter_expr, ...) {
|
|
| 83 | 25x |
checkmate::assert_data_frame(df) |
| 84 | 25x |
checkmate::assert_string(.var) |
| 85 | 25x |
checkmate::assert_names(colnames(df), must.include = .var) |
| 86 | 24x |
checkmate::assert_class(filter_expr, "expression") |
| 87 | 24x |
checkmate::assert_true(length(filter_expr) == 1) |
| 88 | ||
| 89 | 23x |
row_mask <- eval(filter_expr[[1]], envir = df) |
| 90 | 21x |
checkmate::assert_logical(row_mask, len = nrow(df)) |
| 91 | 15x |
x <- df[row_mask, .var, drop = TRUE] |
| 92 | ||
| 93 | 15x |
dots <- list(...) |
| 94 | 15x |
dots$x <- x |
| 95 | 15x |
if (".formats" %in% names(dots) && identical(dots[[".formats"]], "default")) {
|
| 96 | ! |
if ("formats_var" %in% names(dots) && assertthat::is.string(dots[["formats_var"]])) {
|
| 97 | ! |
xfmt <- dots[["formats_var"]] |
| 98 | ! |
checkmate::assert_names(colnames(df), must.include = xfmt) |
| 99 | ! |
.formats <- unique(df[[xfmt]])[[1]] |
| 100 | ! |
dots$.formats <- .formats |
| 101 | ! |
dots$formats_var <- NULL |
| 102 |
} |
|
| 103 |
} |
|
| 104 | ||
| 105 | 15x |
do.call(tern::a_summary, dots) |
| 106 |
} |
|
| 107 | ||
| 108 |
#' @describeIn summary_subset Content row function with optional subsetting |
|
| 109 |
#' and an optional label prepended to the resulting content row using |
|
| 110 |
#' [prepend_label_cell()]. |
|
| 111 |
#' |
|
| 112 |
#' @inheritParams prepend_label_cell |
|
| 113 |
#' |
|
| 114 |
#' @export |
|
| 115 |
#' @examples |
|
| 116 |
#' |
|
| 117 |
#' ## Content row function: |
|
| 118 |
#' |
|
| 119 |
#' df <- data.frame( |
|
| 120 |
#' USUBJID = rep(1:6, each = 2), |
|
| 121 |
#' AVISIT = rep(c("Baseline", "Day 1"), 6),
|
|
| 122 |
#' AVAL = c(1, 3, 2, 9, 13, 19, 15, 23, 43, 56, 24, 32), |
|
| 123 |
#' ABLFL = rep(c(TRUE, FALSE), 6), |
|
| 124 |
#' BASE = rep(c(1, 2, 13, 15, 43, 24), each = 2), |
|
| 125 |
#' CHG = c(0, 2, 0, 7, 0, 6, 0, 8, 0, 13, 0, 8) |
|
| 126 |
#' ) |
|
| 127 |
#' df |
|
| 128 |
#' |
|
| 129 |
#' c_summary_subset_label( |
|
| 130 |
#' df = df, |
|
| 131 |
#' .var = "BASE", |
|
| 132 |
#' filter_expr = expression(ABLFL), |
|
| 133 |
#' label = "Baseline", |
|
| 134 |
#' .stats = c("n", "mean_sd")
|
|
| 135 |
#' ) |
|
| 136 |
c_summary_subset_label <- function(df, |
|
| 137 |
labelstr, |
|
| 138 |
.var, |
|
| 139 |
filter_expr, |
|
| 140 |
label = "", |
|
| 141 |
label_indent_mod = 0L, |
|
| 142 |
...) {
|
|
| 143 | 14x |
checkmate::assert_data_frame(df) |
| 144 | 14x |
checkmate::assert_string(.var) |
| 145 | 14x |
checkmate::assert_names(colnames(df), must.include = .var) |
| 146 | 13x |
checkmate::assert_class(filter_expr, "expression") |
| 147 | 13x |
checkmate::assert_true(length(filter_expr) == 1) |
| 148 | 12x |
checkmate::assert_string(label, na.ok = TRUE) |
| 149 | 12x |
checkmate::assert_int(label_indent_mod) |
| 150 | ||
| 151 | 12x |
y <- a_summary_subset(df = df, .var = .var, filter_expr = filter_expr, ...) |
| 152 | 8x |
prepend_label_cell(y, label = label, label_indent_mod = label_indent_mod) |
| 153 |
} |
| 1 |
#' Analysis and Content Summary Function Producing Blank Line |
|
| 2 |
#' |
|
| 3 |
#' @inheritParams proposal_argument_convention |
|
| 4 |
#' |
|
| 5 |
#' @export |
|
| 6 |
ac_blank_line <- function(df, labelstr = "") {
|
|
| 7 | 36x |
in_rows(.list = NA_real_, .labels = labelstr, .formats = "xx", .format_na_strs = "") |
| 8 |
} |
|
| 9 | ||
| 10 |
#' Insertion of Blank Lines in a Layout |
|
| 11 |
#' |
|
| 12 |
#' @inheritParams proposal_argument_convention |
|
| 13 |
#' |
|
| 14 |
#' @description This is a hack for `rtables` in order to be able to add row gaps, |
|
| 15 |
#' i.e. blank lines. |
|
| 16 |
#' In particular, by default this function needs to maintain a global state for avoiding |
|
| 17 |
#' duplicate table names. The global state variable is hidden by using |
|
| 18 |
#' a dot in front of its name. However, this likely won't work with parallelisation across |
|
| 19 |
#' multiple threads and also causes non-reproducibility of the resulting `rtables` |
|
| 20 |
#' object. Therefore also a custom table name can be used. |
|
| 21 |
#' |
|
| 22 |
#' @return The modified layout now including a blank line after the current |
|
| 23 |
#' row content. |
|
| 24 |
#' @export |
|
| 25 |
#' |
|
| 26 |
#' @examples |
|
| 27 |
#' ADSL <- ex_adsl |
|
| 28 |
#' |
|
| 29 |
#' lyt <- basic_table() |> |
|
| 30 |
#' split_cols_by("ARM") |>
|
|
| 31 |
#' split_rows_by("STRATA1") |>
|
|
| 32 |
#' analyze(vars = "AGE", afun = function(x) {
|
|
| 33 |
#' in_rows( |
|
| 34 |
#' "Mean (sd)" = rcell(c(mean(x), sd(x)), format = "xx.xx (xx.xx)") |
|
| 35 |
#' ) |
|
| 36 |
#' }) |> |
|
| 37 |
#' insert_blank_line() |> |
|
| 38 |
#' analyze(vars = "AGE", table_names = "AGE_Range", afun = function(x) {
|
|
| 39 |
#' in_rows( |
|
| 40 |
#' "Range" = rcell(range(x), format = "xx.xx - xx.xx") |
|
| 41 |
#' ) |
|
| 42 |
#' }) |
|
| 43 |
#' build_table(lyt, ADSL) |
|
| 44 |
insert_blank_line <- function(lyt, table_names = NULL) {
|
|
| 45 | 4x |
varnames_rows <- vars_in_layout(lyt@row_layout) |
| 46 | 4x |
checkmate::assert_character(varnames_rows, min.len = 1L) |
| 47 | 4x |
last_varname_rows <- utils::tail(varnames_rows, 1L) |
| 48 | ||
| 49 | 4x |
this_table_name <- if (is.null(table_names)) {
|
| 50 | 2x |
default_table_name <- paste0(".post_", last_varname_rows, "_blank")
|
| 51 | ||
| 52 |
# A named list for tracking table names and counts |
|
| 53 | 2x |
table_count <- getOption("junco.insert_blank_line")
|
| 54 | ||
| 55 | 2x |
new_count <- if (default_table_name %in% names(table_count)) {
|
| 56 | 1x |
table_count[[default_table_name]] + 1 |
| 57 |
} else {
|
|
| 58 | 1x |
1 |
| 59 |
} |
|
| 60 | ||
| 61 | 2x |
table_count[[default_table_name]] <- new_count |
| 62 | 2x |
options(junco.insert_blank_line = table_count) |
| 63 | ||
| 64 | 2x |
paste(default_table_name, new_count, sep = "_") |
| 65 |
} else {
|
|
| 66 | 2x |
checkmate::assert_string(table_names, min.chars = 1L) |
| 67 | 2x |
table_names |
| 68 |
} |
|
| 69 | ||
| 70 | 4x |
analyze( |
| 71 | 4x |
lyt, |
| 72 | 4x |
vars = last_varname_rows, |
| 73 | 4x |
afun = ac_blank_line, |
| 74 | 4x |
show_labels = "hidden", |
| 75 | 4x |
table_names = this_table_name |
| 76 |
) |
|
| 77 |
} |
|
| 78 | ||
| 79 |
add_blank_line_rcells <- function(ret) {
|
|
| 80 |
# check that ret is expected structure and not NULL |
|
| 81 | 5x |
if (is.null(ret)) {
|
| 82 | ! |
stop("add_blank_line_rcells: ret cannot be NULL.")
|
| 83 |
} |
|
| 84 | 5x |
if (!(inherits(ret, "RowsVerticalSection") || inherits(ret, "CellValue"))) {
|
| 85 | ! |
stop("add_blank_line_rcells: ret must be of class RowsVerticalSection or CellValue.")
|
| 86 |
} |
|
| 87 | ||
| 88 | 5x |
if (inherits(ret, "RowsVerticalSection")) {
|
| 89 | 5x |
xlabel <- attr(ret, "row_labels") |
| 90 | 5x |
indent_mods <- lapply(ret, function(obj) {
|
| 91 | 5x |
attr(obj, "indent_mod") |
| 92 |
}) |
|
| 93 | 5x |
ret <- unclass(ret) # avoid error from new c.RowsVerticalSection |
| 94 |
} else {
|
|
| 95 | ! |
xlabel <- attr(ret, "label") |
| 96 | ! |
indent_mods <- attr(ret, "indent_mod") |
| 97 |
} |
|
| 98 | ||
| 99 | 5x |
fmts <- lapply(ret, obj_format) |
| 100 | 5x |
na_strs <- lapply(ret, obj_na_str) |
| 101 |
# ret <- append(ret,rcell(NA_real_,format = 'xx')) use a character version for the new line, rather than NA - to |
|
| 102 |
# allow NA processing for other stuff |
|
| 103 | 5x |
ret <- c(ret, in_rows(rcell(NA, format = "xx"), .labels = " ", .formats = "xx", .format_na_strs = " ")) |
| 104 | 5x |
fmts <- append(fmts, "xx") |
| 105 | 5x |
na_strs <- append(na_strs, " ") |
| 106 | 5x |
indent_mods <- append(indent_mods, 0L) |
| 107 | 5x |
xlabel <- append(xlabel, " ") |
| 108 | 5x |
ret <- stats::setNames(ret, xlabel) |
| 109 | ||
| 110 |
# perform the update to add the extra line |
|
| 111 | 5x |
fret <- in_rows( |
| 112 | 5x |
.list = ret, |
| 113 | 5x |
.labels = xlabel, |
| 114 | 5x |
.formats = fmts, |
| 115 | 5x |
.format_na_strs = na_strs, |
| 116 | 5x |
.indent_mods = indent_mods |
| 117 |
) |
|
| 118 | ||
| 119 | 5x |
fret |
| 120 |
} |
|
| 121 | ||
| 122 |
#' @title Prepend a Label Row to a RowsVerticalSection Object |
|
| 123 |
#' |
|
| 124 |
#' @description `r lifecycle::badge("stable")`
|
|
| 125 |
#' |
|
| 126 |
#' Prepends a label row to a `RowsVerticalSection` object. |
|
| 127 |
#' |
|
| 128 |
#' `RowsVerticalSection` objects are returned by analysis functions used within |
|
| 129 |
#' the **rtables** framework and are typically created with |
|
| 130 |
#' [rtables::in_rows()]. |
|
| 131 |
#' |
|
| 132 |
#' This function is typically used to introduce section headers |
|
| 133 |
#' (e.g., "Descriptive Statistics") in tabular reports and other reporting |
|
| 134 |
#' outputs. |
|
| 135 |
#' |
|
| 136 |
#' @note The `row_na_strs` and `row_footnotes` attributes of the |
|
| 137 |
#' `RowsVerticalSection` object are not preserved. |
|
| 138 |
#' Since an additional row is added, be aware of recycling rules; that is, |
|
| 139 |
#' the length of all attributes of `x` should be equal to `length(x)`. |
|
| 140 |
#' |
|
| 141 |
#' @param x (`RowsVerticalSection`)\cr Analysis result object. |
|
| 142 |
#' @param label (`character(1)`)\cr Label to insert as the first row. |
|
| 143 |
#' @param label_indent_mod (`integer(1)`)\cr Indentation modifier applied to |
|
| 144 |
#' the label row. |
|
| 145 |
#' |
|
| 146 |
#' @returns A `RowsVerticalSection` object with a label row prepended. |
|
| 147 |
#' |
|
| 148 |
#' @importFrom rtables rcell in_rows |
|
| 149 |
#' |
|
| 150 |
#' @export |
|
| 151 |
#' |
|
| 152 |
#' @examples |
|
| 153 |
#' rvs <- rtables::in_rows( |
|
| 154 |
#' Mean = rtables::rcell(5), |
|
| 155 |
#' Range = rtables::rcell(c(1, 8)) |
|
| 156 |
#' ) |
|
| 157 |
#' |
|
| 158 |
#' prepend_label_cell( |
|
| 159 |
#' rvs, |
|
| 160 |
#' "Descriptive Statistics", |
|
| 161 |
#' label_indent_mod = 1L |
|
| 162 |
#' ) |
|
| 163 |
#' |
|
| 164 |
prepend_label_cell <- function(x, label = "", label_indent_mod = 0L) {
|
|
| 165 | 14x |
checkmate::assert_class(x, "RowsVerticalSection") |
| 166 | 14x |
checkmate::assert_string(label, na.ok = TRUE) |
| 167 | 14x |
checkmate::assert_int(label_indent_mod) |
| 168 | 14x |
checkmate::assert_true( |
| 169 | 14x |
!("__label" %in% c(names(x), attr(x, "row_names")))
|
| 170 |
) |
|
| 171 | ||
| 172 | 14x |
label_rcell <- rcell(NULL, label = label, indent_mod = label_indent_mod) |
| 173 | 14x |
ret <- rtables::in_rows( |
| 174 | 14x |
.list = c(list("__label" = label_rcell), x)
|
| 175 |
) |
|
| 176 | ||
| 177 |
# row_names |
|
| 178 | 14x |
x_rn <- attr(x, "row_names") |
| 179 | 14x |
if (!is.null(x_rn)) {
|
| 180 | 14x |
label_rn <- if (!is.null(names(x_rn))) {
|
| 181 | ! |
c("__label" = "__label")
|
| 182 |
} else {
|
|
| 183 | 14x |
"__label" |
| 184 |
} |
|
| 185 | 14x |
attr(ret, "row_names") <- c(label_rn, x_rn) |
| 186 |
} |
|
| 187 | ||
| 188 |
# row_labels |
|
| 189 | 14x |
x_rl <- attr(x, "row_labels") |
| 190 | 14x |
if (!is.null(x_rl)) {
|
| 191 | 14x |
label_rl <- if (!is.null(names(x_rl))) {
|
| 192 | 10x |
c("__label" = label)
|
| 193 |
} else {
|
|
| 194 | 4x |
label |
| 195 |
} |
|
| 196 | 14x |
attr(ret, "row_labels") <- c(label_rl, x_rl) |
| 197 |
} |
|
| 198 | ||
| 199 |
# row_formats |
|
| 200 | 14x |
x_rf <- attr(x, "row_formats") |
| 201 | 14x |
if (length(x_rf) >= 2 || !is.null(x_rf[[1]])) {
|
| 202 | 11x |
attr(ret, "row_formats") <- c("__label" = list(NULL), x_rf)
|
| 203 |
} |
|
| 204 | ||
| 205 |
# indent_mods |
|
| 206 | 14x |
x_im <- attr(x, "indent_mods") |
| 207 | 14x |
if (!is.null(x_im)) {
|
| 208 | 11x |
label_im <- if (!is.null(names(x_im))) {
|
| 209 | ! |
c("__label" = label_indent_mod)
|
| 210 |
} else {
|
|
| 211 | 11x |
label_indent_mod |
| 212 |
} |
|
| 213 | 11x |
attr(ret, "indent_mods") <- c(label_im, x_im) |
| 214 |
} |
|
| 215 | ||
| 216 | 14x |
ret |
| 217 |
} |
| 1 |
#' @keywords internal |
|
| 2 |
memoised_fit_mmrm <- memoise::memoise(fit_mmrm_j) |
|
| 3 | ||
| 4 |
#' Helper Function to Fit the MMRM and Return LS Mean Estimates and Contrasts |
|
| 5 |
#' |
|
| 6 |
#' @inheritParams proposal_argument_convention |
|
| 7 |
#' @param df_parent (`data.frame`)\cr data set containing all analysis variables |
|
| 8 |
#' from all visits and arms. |
|
| 9 |
#' @param ref_arm_level (`string`)\cr the reference arm which should be compared |
|
| 10 |
#' against. |
|
| 11 |
#' @param ref_visit_levels (`character`)\cr the reference visits which should not |
|
| 12 |
#' be included in the model fit. |
|
| 13 |
#' @param ... additional options passed to [fit_mmrm_j()]. |
|
| 14 |
#' |
|
| 15 |
#' @return The resulting estimates and contrasts LS means as returned by |
|
| 16 |
#' [tidy.tern_model()]. |
|
| 17 |
#' @keywords internal |
|
| 18 |
h_summarize_mmrm <- function(.var, df_parent, variables, ref_arm_level, ref_visit_levels, ...) {
|
|
| 19 | 51x |
checkmate::assert_string(.var) |
| 20 | 51x |
variables$response <- .var |
| 21 | ||
| 22 | 51x |
checkmate::assert_string(ref_arm_level) |
| 23 | 51x |
arm_levels <- levels(df_parent[[variables$arm]]) |
| 24 | 51x |
if (arm_levels[1L] != ref_arm_level) {
|
| 25 | 51x |
checkmate::assert_subset(ref_arm_level, arm_levels[-1L]) |
| 26 | 51x |
df_parent[[variables$arm]] <- stats::relevel(df_parent[[variables$arm]], ref = ref_arm_level) |
| 27 |
} |
|
| 28 | 51x |
checkmate::assert_character(ref_visit_levels) |
| 29 | 51x |
in_ref_visits <- df_parent[[variables$visit]] %in% ref_visit_levels |
| 30 | 51x |
df_parent <- df_parent[!in_ref_visits, , drop = FALSE] |
| 31 | 51x |
checkmate::assert_true(nrow(df_parent) > 1) |
| 32 | 51x |
new_levels <- setdiff(levels(df_parent[[variables$visit]]), ref_visit_levels) |
| 33 | 51x |
df_parent[[variables$visit]] <- factor(df_parent[[variables$visit]], levels = new_levels) |
| 34 | 51x |
mod_fit <- memoised_fit_mmrm(vars = variables, data = df_parent, ...) |
| 35 | 51x |
tidy(mod_fit) |
| 36 |
} |
|
| 37 | ||
| 38 |
#' Dynamic tabulation of MMRM results with tables |
|
| 39 |
#' |
|
| 40 |
#' @description `r lifecycle::badge('stable')`
|
|
| 41 |
#' |
|
| 42 |
#' These functions can be used to produce tables for MMRM results, within |
|
| 43 |
#' tables which are split by arms and visits. This is helpful when higher-level |
|
| 44 |
#' row splits are needed (e.g. splits by parameter or subgroup). |
|
| 45 |
#' |
|
| 46 |
#' @name summarize_mmrm |
|
| 47 |
#' @examples |
|
| 48 |
#' set.seed(123) |
|
| 49 |
#' longdat <- data.frame( |
|
| 50 |
#' ID = rep(DM$ID, 5), |
|
| 51 |
#' AVAL = c( |
|
| 52 |
#' rep(0, nrow(DM)), |
|
| 53 |
#' rnorm(n = nrow(DM) * 4) |
|
| 54 |
#' ), |
|
| 55 |
#' VISIT = factor(rep(paste0("V", 0:4), each = nrow(DM)))
|
|
| 56 |
#' ) |> |
|
| 57 |
#' dplyr::inner_join(DM, by = "ID") |
|
| 58 |
#' |
|
| 59 |
NULL |
|
| 60 | ||
| 61 |
#' @describeIn summarize_mmrm Statistics function which is extracting estimates, |
|
| 62 |
#' not including any results when in the reference visit, and only showing LS mean |
|
| 63 |
#' estimates when in the reference arm and not in reference visit. It uses |
|
| 64 |
#' [s_lsmeans()] for the final processing. |
|
| 65 |
#' |
|
| 66 |
#' @inheritParams proposal_argument_convention |
|
| 67 |
#' @param ref_levels (`list`)\cr with `visit` and `arm` reference levels. |
|
| 68 |
#' @param ... eventually passed to [fit_mmrm_j()] via [h_summarize_mmrm()]. |
|
| 69 |
#' @export |
|
| 70 |
s_summarize_mmrm <- function( |
|
| 71 |
df, |
|
| 72 |
.var, |
|
| 73 |
variables, |
|
| 74 |
ref_levels, |
|
| 75 |
.spl_context, |
|
| 76 |
alternative = c("two.sided", "less", "greater"),
|
|
| 77 |
show_relative = c("reduction", "increase"),
|
|
| 78 |
...) {
|
|
| 79 | 63x |
alternative <- match.arg(alternative) |
| 80 | ||
| 81 | 63x |
checkmate::assert_list(variables, names = "unique") |
| 82 | 63x |
visit_var <- variables$visit |
| 83 | 63x |
arm_var <- variables$arm |
| 84 | ||
| 85 | 63x |
checkmate::assert_list(ref_levels, names = "unique") |
| 86 | 63x |
checkmate::assert_subset(c(visit_var, arm_var), names(ref_levels)) |
| 87 | 63x |
ref_visits <- ref_levels[[visit_var]] |
| 88 | 63x |
ref_arm <- ref_levels[[arm_var]] |
| 89 | 63x |
checkmate::assert_string(ref_arm) |
| 90 | ||
| 91 | 63x |
current_visit <- as.character(unique(df[[visit_var]])) |
| 92 | 63x |
current_arm <- as.character(unique(df[[arm_var]])) |
| 93 | 63x |
checkmate::assert_string(current_visit) |
| 94 | 63x |
checkmate::assert_string(current_arm) |
| 95 | ||
| 96 | 63x |
in_ref_visits <- current_visit %in% ref_visits |
| 97 | 63x |
in_ref_arm <- current_arm == ref_arm |
| 98 | ||
| 99 | 63x |
if (in_ref_visits) {
|
| 100 |
## this is returned |
|
| 101 | 13x |
list( |
| 102 | 13x |
n = NULL, |
| 103 | 13x |
adj_mean_se = NULL, |
| 104 | 13x |
adj_mean_ci = NULL, |
| 105 | 13x |
adj_mean_est_ci = NULL, |
| 106 | 13x |
diff_mean_se = NULL, |
| 107 | 13x |
diff_mean_ci = NULL, |
| 108 | 13x |
diff_mean_est_ci = NULL, |
| 109 | 13x |
change = NULL, |
| 110 | 13x |
p_value = NULL |
| 111 |
) |
|
| 112 |
} else { # non ref visit
|
|
| 113 | 50x |
n_splits <- nrow(.spl_context) |
| 114 | ||
| 115 |
# Check that the current row split is by the visit variable. |
|
| 116 | 50x |
current_split_var <- .spl_context[n_splits, "split"] |
| 117 | 50x |
checkmate::assert_true(identical(current_split_var, visit_var)) |
| 118 | ||
| 119 |
# Then take the data frame with all visits and fit the MMRM on it. |
|
| 120 | 50x |
df_parent <- .spl_context[n_splits - 1, "full_parent_df"][[1]] |
| 121 | 50x |
lsm_df <- h_summarize_mmrm( |
| 122 | 50x |
.var = .var, |
| 123 | 50x |
df_parent = df_parent, |
| 124 | 50x |
variables = variables, |
| 125 | 50x |
ref_arm_level = ref_arm, |
| 126 | 50x |
ref_visit_levels = ref_visits, |
| 127 |
... |
|
| 128 |
) |
|
| 129 | ||
| 130 |
# Subset to the current table cell we are looking at. |
|
| 131 | 50x |
matches_visit <- lsm_df[[visit_var]] == current_visit |
| 132 | 50x |
matches_arm <- lsm_df[[arm_var]] == current_arm |
| 133 | 50x |
matches_both <- which(matches_visit & matches_arm) |
| 134 | 50x |
checkmate::assert_int(matches_both) |
| 135 | ||
| 136 | 50x |
s_lsmeans( |
| 137 | 50x |
lsm_df[matches_both, ], |
| 138 | 50x |
.in_ref_col = in_ref_arm, |
| 139 | 50x |
alternative = alternative, |
| 140 | 50x |
show_relative = show_relative |
| 141 |
) |
|
| 142 |
} # end ref visits if/else |
|
| 143 |
} |
|
| 144 | ||
| 145 |
#' @describeIn summarize_mmrm Formatted analysis function which is used as `afun`. |
|
| 146 |
#' |
|
| 147 |
#' @return |
|
| 148 |
#' * `a_summarize_mmrm()` returns the corresponding list with formatted [rtables::CellValue()]. |
|
| 149 |
#' |
|
| 150 |
#' @examples |
|
| 151 |
#' basic_table() |> |
|
| 152 |
#' split_rows_by("VISIT") |>
|
|
| 153 |
#' split_cols_by("ARM") |>
|
|
| 154 |
#' analyze( |
|
| 155 |
#' vars = "AVAL", |
|
| 156 |
#' afun = a_summarize_mmrm, |
|
| 157 |
#' na_str = tern::default_na_str(), |
|
| 158 |
#' show_labels = "hidden", |
|
| 159 |
#' extra_args = list( |
|
| 160 |
#' variables = list( |
|
| 161 |
#' covariates = c("AGE"),
|
|
| 162 |
#' id = "ID", |
|
| 163 |
#' arm = "ARM", |
|
| 164 |
#' visit = "VISIT" |
|
| 165 |
#' ), |
|
| 166 |
#' conf_level = 0.9, |
|
| 167 |
#' cor_struct = "toeplitz", |
|
| 168 |
#' ref_levels = list(VISIT = "V0", ARM = "B: Placebo") |
|
| 169 |
#' ) |
|
| 170 |
#' ) |> |
|
| 171 |
#' build_table(longdat) |> |
|
| 172 |
#' prune_table(all_zero) |
|
| 173 |
#' @export |
|
| 174 |
a_summarize_mmrm <- function( |
|
| 175 |
df, |
|
| 176 |
.var, |
|
| 177 |
.spl_context, |
|
| 178 |
..., |
|
| 179 |
.stats = NULL, |
|
| 180 |
.formats = NULL, |
|
| 181 |
.labels = NULL, |
|
| 182 |
.indent_mods = NULL) {
|
|
| 183 |
# Check for additional parameters to the statistics function |
|
| 184 | 60x |
dots_extra_args <- list(...) |
| 185 | ||
| 186 |
# Only support default stats, not custom stats |
|
| 187 | 60x |
.stats <- .split_std_from_custom_stats(.stats)$default_stats |
| 188 | ||
| 189 |
# Apply statistics function |
|
| 190 | 60x |
x_stats <- .apply_stat_functions( |
| 191 | 60x |
default_stat_fnc = s_summarize_mmrm, |
| 192 | 60x |
custom_stat_fnc_list = NULL, |
| 193 | 60x |
args_list = c(df = list(df), .var = .var, .spl_context = list(.spl_context), dots_extra_args) |
| 194 |
) |
|
| 195 | ||
| 196 |
# Format according to specifications |
|
| 197 | 60x |
format_stats( |
| 198 | 60x |
x_stats, |
| 199 | 60x |
method_groups = "summarize_mmrm", |
| 200 | 60x |
stats_in = .stats, |
| 201 | 60x |
formats_in = .formats, |
| 202 | 60x |
labels_in = .labels, |
| 203 | 60x |
indents_in = .indent_mods |
| 204 |
) |
|
| 205 |
} |
|
| 206 | ||
| 207 |
#' @describeIn summarize_mmrm Wrapper for the `afun` which can exclude |
|
| 208 |
#' row split levels from producing the analysis. These have to be specified in the |
|
| 209 |
#' `exclude_levels` argument, see `?do_exclude_split` for details. |
|
| 210 |
#' @export |
|
| 211 |
a_summarize_mmrm_with_exclude <- function( |
|
| 212 |
df, |
|
| 213 |
.var, |
|
| 214 |
exclude_levels, |
|
| 215 |
.spl_context, |
|
| 216 |
..., |
|
| 217 |
.stats = NULL, |
|
| 218 |
.formats = NULL, |
|
| 219 |
.labels = NULL, |
|
| 220 |
.indent_mods = NULL |
|
| 221 |
) {
|
|
| 222 | 2x |
if (do_exclude_split(exclude_levels, .spl_context)) {
|
| 223 | 1x |
NULL |
| 224 |
} else {
|
|
| 225 | 1x |
a_summarize_mmrm( |
| 226 | 1x |
df = df, |
| 227 | 1x |
.var = .var, |
| 228 | 1x |
.spl_context = .spl_context, |
| 229 |
..., |
|
| 230 | 1x |
.stats = .stats, |
| 231 | 1x |
.formats = .formats, |
| 232 | 1x |
.labels = .labels, |
| 233 | 1x |
.indent_mods = .indent_mods |
| 234 |
) |
|
| 235 |
} |
|
| 236 |
} |
| 1 |
#' @title s_function for proportion of factor levels |
|
| 2 |
#' @description A simple statistics function which prepares the numbers with percentages |
|
| 3 |
#' in the required format. The denominator here is from the alternative counts data set |
|
| 4 |
#' in the given row and column split. |
|
| 5 |
#' |
|
| 6 |
#' If a total row is shown, then here just the total number is shown (without 100%). |
|
| 7 |
#' @param x (`factor`)\cr categorical variable we want to analyze. |
|
| 8 |
#' @param .alt_df (`data.frame`)\cr alternative data frame used for denominator calculation. |
|
| 9 |
#' @param use_alt_counts (`flag`)\cr whether the `.alt_df` should be used for the total, i.e. the denominator. |
|
| 10 |
#' If not, then the number of non-missing values in `x` is used. |
|
| 11 |
#' @param show_total (`string`)\cr show the total level optionally on the top or in the bottom |
|
| 12 |
#' of the factor levels. |
|
| 13 |
#' @param total_label (`string`)\cr which label to use for the optional total level. |
|
| 14 |
#' @return The [rtables::in_rows()] result with the proportion statistics. |
|
| 15 |
#' @seealso [s_proportion_logical()] for tabulating logical `x`. |
|
| 16 |
#' @export |
|
| 17 |
s_proportion_factor <- function( |
|
| 18 |
x, |
|
| 19 |
.alt_df, |
|
| 20 |
use_alt_counts = TRUE, |
|
| 21 |
show_total = c("none", "top", "bottom"),
|
|
| 22 |
total_label = "Total" |
|
| 23 |
) {
|
|
| 24 | 5x |
checkmate::assert_factor(x) |
| 25 | 5x |
checkmate::assert_flag(use_alt_counts) |
| 26 | 5x |
show_total <- match.arg(show_total) |
| 27 | ||
| 28 | 5x |
N <- if (use_alt_counts) nrow(.alt_df) else sum(!is.na(x)) |
| 29 | 5x |
tab <- lapply(as.list(table(x)), function(xi) rcell(xi * c(1, 1 / N), format = jjcsformat_count_fraction)) |
| 30 | 5x |
if (show_total != "none") {
|
| 31 | 3x |
checkmate::assert_string(total_label) |
| 32 | 3x |
tab_total <- stats::setNames(list(rcell(N, format = "xx")), total_label) |
| 33 | ||
| 34 | 3x |
tab <- if (show_total == "top") {
|
| 35 | 1x |
c(tab_total, tab) |
| 36 |
} else {
|
|
| 37 | 2x |
c(tab, tab_total) |
| 38 |
} |
|
| 39 |
} |
|
| 40 | 5x |
in_rows(.list = tab) |
| 41 |
} |
|
| 42 | ||
| 43 |
#' @title s_function for proportion of `TRUE` in logical vector |
|
| 44 |
#' @description A simple statistics function which prepares the numbers with percentages |
|
| 45 |
#' in the required format. The denominator here is from the alternative counts data set |
|
| 46 |
#' in the given row and column split. |
|
| 47 |
#' @param x (`logical`)\cr binary variable we want to analyze. |
|
| 48 |
#' @param label (`string`)\cr label to use. |
|
| 49 |
#' @param .alt_df (`data.frame`)\cr alternative data frame used for denominator calculation. |
|
| 50 |
#' @return The [rtables::in_rows()] result with the proportion statistics. |
|
| 51 |
#' @seealso [s_proportion_factor()] for tabulating factor `x`. |
|
| 52 |
#' @export |
|
| 53 |
s_proportion_logical <- function(x, label = "Responders", .alt_df) {
|
|
| 54 | 1x |
n <- sum(x) |
| 55 | 1x |
N <- nrow(.alt_df) |
| 56 | 1x |
p_hat <- n / N |
| 57 | 1x |
list(n_prop = with_label(rcell(c(n, p_hat), format = jjcsformat_count_fraction), label)) |
| 58 |
} |
|
| 59 | ||
| 60 |
#' @title c_function for proportion of `TRUE` in logical vector |
|
| 61 |
#' @description A simple statistics function which prepares the numbers with percentages |
|
| 62 |
#' in the required format, for use in a split content row. The denominator here is |
|
| 63 |
#' from the column N. Note that we don't use here .alt_df because that might not |
|
| 64 |
#' have required row split variables available. |
|
| 65 |
#' @param x (`logical`)\cr binary variable we want to analyze. |
|
| 66 |
#' @param labelstr (`string`)\cr label string. |
|
| 67 |
#' @param labelstr (`string`)\cr label string. |
|
| 68 |
#' @param label_fstr (`string`)\cr format string for the label. |
|
| 69 |
#' @param format (`character` or `list`)\cr format for the statistics. |
|
| 70 |
#' @param .N_col (`numeric`)\cr number of columns. |
|
| 71 |
#' @param .N_col (`numeric`)\cr number of columns. |
|
| 72 |
#' @return The [rtables::in_rows()] result with the proportion statistics. |
|
| 73 |
#' @seealso [s_proportion_logical()] for the related statistics function. |
|
| 74 |
#' @export |
|
| 75 |
c_proportion_logical <- function(x, labelstr, label_fstr, format, .N_col) {
|
|
| 76 | 1x |
checkmate::assert_logical(x) |
| 77 | 1x |
num <- sum(x) |
| 78 | 1x |
denom <- .N_col |
| 79 | 1x |
in_rows(est_prop = c(num, num / denom), .formats = format, .labels = sprintf(label_fstr, labelstr)) |
| 80 |
} |
|
| 81 | ||
| 82 |
#' Helper Function to Create Logical Design Matrix from Factor Variable |
|
| 83 |
#' |
|
| 84 |
#' @description |
|
| 85 |
#' Helper Function to Create Logical Design Matrix from Factor Variable. |
|
| 86 |
#' |
|
| 87 |
#' @param df (`data.frame`)\cr including a factor variable with name in `.var`. |
|
| 88 |
#' @param .var (`string`)\cr name of the factor variable. |
|
| 89 |
#' |
|
| 90 |
#' @return The logical matrix with dummy encoding of all factor levels. |
|
| 91 |
#' @keywords internal |
|
| 92 |
#' @export |
|
| 93 |
#' @examples |
|
| 94 |
#' h_get_design_mat(df = data.frame(a = factor(c("a", "b", "a"))), .var = "a")
|
|
| 95 |
h_get_design_mat <- function(df, .var) {
|
|
| 96 | 5x |
checkmate::assert_data_frame(df) |
| 97 | 5x |
checkmate::assert_string(.var) |
| 98 | 5x |
checkmate::assert_factor(df[[.var]]) |
| 99 | ||
| 100 | 5x |
model_formula <- stats::as.formula(paste("~ -1 +", .var))
|
| 101 | 5x |
design_mat <- stats::model.matrix(model_formula, df) |
| 102 | 5x |
mode(design_mat) <- "logical" |
| 103 | 5x |
colnames(design_mat) <- levels(df[[.var]]) |
| 104 | 5x |
design_mat |
| 105 |
} |
|
| 106 | ||
| 107 |
#' Formatted Analysis Function For Proportion Confidence Interval for Logical |
|
| 108 |
#' |
|
| 109 |
#' @description |
|
| 110 |
#' Formatted Analysis Function For Proportion Confidence Interval for Logical. |
|
| 111 |
#' |
|
| 112 |
#' @param x (`logical`)\cr including binary response values. |
|
| 113 |
#' @param .alt_df (`data.frame`)\cr alternative data frame used for denominator calculation. |
|
| 114 |
#' @param formats (`list`)\cr including element `prop_ci` with the |
|
| 115 |
#' required format. Note that the value is in percent already. |
|
| 116 |
#' @param method (`string`)\cr please see [tern::s_proportion()] for possible |
|
| 117 |
#' methods. |
|
| 118 |
#' @param conf_level (`numeric`)\cr confidence level for the confidence interval. |
|
| 119 |
#' @param conf_level (`numeric`)\cr confidence level for the confidence interval. |
|
| 120 |
#' |
|
| 121 |
#' @return The [rtables::rcell()] result. |
|
| 122 |
#' @export |
|
| 123 |
#' |
|
| 124 |
#' @examples |
|
| 125 |
#' a_proportion_ci_logical( |
|
| 126 |
#' x = DM$SEX == "F", |
|
| 127 |
#' .alt_df = DM, |
|
| 128 |
#' conf_level = 0.95, |
|
| 129 |
#' formats = list(prop_ci = jjcsformat_xx("xx.xx% - xx.xx%")),
|
|
| 130 |
#' method = "wald" |
|
| 131 |
#' ) |
|
| 132 |
a_proportion_ci_logical <- function(x, .alt_df, conf_level, method, formats) {
|
|
| 133 | 5x |
checkmate::assert_logical(x) |
| 134 | 5x |
checkmate::assert_data_frame(.alt_df) |
| 135 | 5x |
checkmate::assert_list(formats) |
| 136 | ||
| 137 | 5x |
diff_n <- nrow(.alt_df) - length(x) |
| 138 | 5x |
if (diff_n > 0) {
|
| 139 | ! |
x <- c(x, rep(FALSE, length = diff_n)) |
| 140 |
} |
|
| 141 | 5x |
est <- s_proportion(x, conf_level = conf_level, method = method) |
| 142 | 5x |
rcell(est$prop_ci, format = formats$prop_ci) |
| 143 |
} |
|
| 144 | ||
| 145 |
#' Formatted Analysis Function For Proportion Confidence Interval for Factor |
|
| 146 |
#' |
|
| 147 |
#' @description |
|
| 148 |
#' Formatted Analysis Function For Proportion Confidence Interval for Factor. |
|
| 149 |
#' |
|
| 150 |
#' @param df (`data.frame`)\cr including factor `.var`. |
|
| 151 |
#' @param .var (`string`)\cr name of the factor variable. |
|
| 152 |
#' @param .var (`string`)\cr name of the factor variable. |
|
| 153 |
#' @param \dots see [a_proportion_ci_logical()] for additionally required |
|
| 154 |
#' arguments. |
|
| 155 |
#' |
|
| 156 |
#' @return The [rtables::rcell()] result. |
|
| 157 |
#' @export |
|
| 158 |
#' |
|
| 159 |
#' @examples |
|
| 160 |
#' a_proportion_ci_factor( |
|
| 161 |
#' df = DM, |
|
| 162 |
#' .var = "SEX", |
|
| 163 |
#' .alt_df = DM, |
|
| 164 |
#' conf_level = 0.95, |
|
| 165 |
#' formats = list(prop_ci = jjcsformat_xx("xx.x%, xx.x%")),
|
|
| 166 |
#' method = "clopper-pearson" |
|
| 167 |
#' ) |
|
| 168 |
a_proportion_ci_factor <- function(df, .var, ...) {
|
|
| 169 | 1x |
checkmate::assert_factor(df[[.var]]) |
| 170 | ||
| 171 | 1x |
design_mat <- h_get_design_mat(df, .var) |
| 172 | 1x |
design_df <- as.data.frame(design_mat) |
| 173 | 1x |
res <- lapply(design_df, a_proportion_ci_logical, ...) |
| 174 | 1x |
in_rows(.list = res, .labels = colnames(design_mat)) |
| 175 |
} |
|
| 176 | ||
| 177 |
#' Split Function for Proportion Analysis Columns |
|
| 178 |
#' |
|
| 179 |
#' Here we just split into 3 columns `n`, `%` and `Cum %`. |
|
| 180 |
#' |
|
| 181 |
#' @param ret (`list`)\cr return value from the previous split function. |
|
| 182 |
#' @param spl (`list`)\cr split information. |
|
| 183 |
#' @param fulldf (`data.frame`)\cr full data frame. |
|
| 184 |
#' @param .spl_context (`environment`)\cr split context environment. |
|
| 185 |
#' |
|
| 186 |
#' @seealso [rtables::make_split_fun()] describing the requirements for this kind of |
|
| 187 |
#' post-processing function. |
|
| 188 |
prop_post_fun <- function(ret, spl, fulldf, .spl_context) {
|
|
| 189 | 1x |
short_split_result(n = "n", percent = "%", cum_percent = "Cum %", fulldf = fulldf) |
| 190 |
} |
|
| 191 | ||
| 192 |
#' @rdname prop_post_fun |
|
| 193 |
#' |
|
| 194 |
#' @param df A data frame that contains all analysis variables. |
|
| 195 |
#' @param vals A character vector that contains values to use for the split. |
|
| 196 |
#' @param labels A character vector that contains labels for the statistics (without indent). |
|
| 197 |
#' @param trim A single logical that indicates whether to trim the values. |
|
| 198 |
#' @return a split function for use in [rtables::split_rows_by]. |
|
| 199 |
#' @export |
|
| 200 |
prop_split_fun <- make_split_fun(post = list(prop_post_fun)) |
|
| 201 | ||
| 202 |
#' Formatted Analysis Function for Proportion Analysis |
|
| 203 |
#' |
|
| 204 |
#' This function applies to a factor `x` when a column split was prepared with |
|
| 205 |
#' [prop_split_fun()] before. |
|
| 206 |
#' |
|
| 207 |
#' @details In the column named `n`, the counts of the categories as well as an |
|
| 208 |
#' optional `Total` count will be shown. In the column named `percent`, the |
|
| 209 |
#' percentages of the categories will be shown, with an optional blank entry for |
|
| 210 |
#' `Total`. In the column named `cum_percent`, the cumulative percentages will |
|
| 211 |
#' be shown instead. |
|
| 212 |
#' |
|
| 213 |
#' @param x (`factor`)\cr factor variable to analyze. |
|
| 214 |
#' @param .spl_context (`environment`)\cr split context environment. |
|
| 215 |
#' @param formats (`list`)\cr formats for the statistics. |
|
| 216 |
#' @param add_total_level (`flag`)\cr whether to add a total level. |
|
| 217 |
#' @param x (`factor`)\cr factor variable to analyze. |
|
| 218 |
#' @param .spl_context (`environment`)\cr split context environment. |
|
| 219 |
#' @param formats (`list`)\cr formats for the statistics. |
|
| 220 |
#' @param add_total_level (`flag`)\cr whether to add a total level. |
|
| 221 |
#' |
|
| 222 |
#' @return A `VerticalRowsSection` as returned by [rtables::in_rows]. |
|
| 223 |
#' @export |
|
| 224 |
prop_table_afun <- function(x, .spl_context, formats, add_total_level = FALSE) {
|
|
| 225 | 8x |
checkmate::assert_list(formats, len = 3, names = "unique") |
| 226 | 8x |
checkmate::assert_names(names(formats), permutation.of = c("n", "percent", "cum_percent"))
|
| 227 | 8x |
checkmate::assert_flag(add_total_level) |
| 228 | ||
| 229 | 8x |
stat <- utils::tail(.spl_context[nrow(.spl_context), "cur_col_split_val"][[1]], 1) |
| 230 | 8x |
optional_total <- function(x) if (add_total_level) x else NULL |
| 231 | 8x |
ns <- table(x) |
| 232 | 8x |
res <- if (stat == "n") {
|
| 233 | 2x |
c(as.list(ns), optional_total(length(x))) |
| 234 | 8x |
} else if (stat == "percent") {
|
| 235 | 2x |
c(as.list(ns / length(x) * 100), optional_total(list(NULL))) |
| 236 | 8x |
} else if (stat == "cum_percent") {
|
| 237 | 2x |
c(as.list(cumsum(ns / length(x) * 100)), optional_total(list(NULL))) |
| 238 |
} else {
|
|
| 239 | 2x |
stop("unexpected proportion statistic in split")
|
| 240 |
} |
|
| 241 | 6x |
in_rows(.list = res, .labels = c(levels(x), optional_total("Total")), .formats = formats[[stat]])
|
| 242 |
} |
| 1 |
#' @name diff_means |
|
| 2 |
#' |
|
| 3 |
#' @title Difference in Means |
|
| 4 |
#' |
|
| 5 |
#' @description `r lifecycle::badge("stable")`
|
|
| 6 |
#' |
|
| 7 |
#' Computes estimates and associated inferential statistics for the estimand |
|
| 8 |
#' defined as the difference in population means between two distributions. |
|
| 9 |
#' |
|
| 10 |
#' The following quantities are available: |
|
| 11 |
#' \describe{
|
|
| 12 |
#' \item{diff_means_n1}{Sample size of Group 1.}
|
|
| 13 |
#' \item{diff_means_n2}{Sample size of Group 2.}
|
|
| 14 |
#' \item{diff_means_est}{Point estimate of the difference in population means.}
|
|
| 15 |
#' \item{diff_means_se}{Standard error of the estimator of the difference in population means.}
|
|
| 16 |
#' \item{diff_means_est_se}{The point estimate and the standard error.}
|
|
| 17 |
#' \item{diff_means_ci}{Confidence interval for the difference in population means.}
|
|
| 18 |
#' \item{diff_means_est_ci}{The point estimate and the confidence interval.}
|
|
| 19 |
#' } |
|
| 20 |
#' |
|
| 21 |
#' @details |
|
| 22 |
#' Supports both non-paired and paired samples. For non-paired samples, the |
|
| 23 |
#' estimator is defined as the difference between sample means. For paired |
|
| 24 |
#' samples, the estimator is defined as the mean of within-pair differences. |
|
| 25 |
#' |
|
| 26 |
#' Inferential statistics, including standard errors and confidence intervals, |
|
| 27 |
#' are obtained using a t-distribution framework via the internal |
|
| 28 |
#' `safe_t_test()` function. |
|
| 29 |
#' |
|
| 30 |
#' It supports only numeric variables. |
|
| 31 |
#' Only complete (non-missing) observations are used in statistical computations. |
|
| 32 |
#' Non-finite values (i.e., `Inf`, `-Inf`) are not allowed. |
|
| 33 |
#' |
|
| 34 |
#' @inheritParams proposal_argument_convention |
|
| 35 |
#' @param .var (`character(1)`)\cr Name of the column in the dataset(s) |
|
| 36 |
#' containing observed sample values. |
|
| 37 |
#' @param ... Additional named arguments passed to `s_diff_means()` and |
|
| 38 |
#' `safe_t_test()`. |
|
| 39 |
#' |
|
| 40 |
#' @author WW |
|
| 41 |
NULL |
|
| 42 | ||
| 43 |
#' @describeIn diff_means Statistical function that computes estimates of the |
|
| 44 |
#' difference in means. |
|
| 45 |
#' The first sample is taken from `df1[[.var]]` and the second from |
|
| 46 |
#' `df2[[.var]]`. |
|
| 47 |
#' |
|
| 48 |
#' If `paired = FALSE`, missing values are removed separately from each sample |
|
| 49 |
#' prior to statistical computations. |
|
| 50 |
#' If `paired = TRUE`, observations are matched using `paired_by` prior to |
|
| 51 |
#' statistical analysis, and only complete pairs are used. |
|
| 52 |
#' |
|
| 53 |
#' Data extraction, alignment, and missing-value handling are delegated to the |
|
| 54 |
#' internal `extract_vectors()` function, which prepares cleaned numeric |
|
| 55 |
#' vectors for both paired and unpaired settings. |
|
| 56 |
#' |
|
| 57 |
#' Inferential statistics are then computed using `safe_t_test()` applied to |
|
| 58 |
#' the processed vectors. |
|
| 59 |
#' |
|
| 60 |
#' @param df1 (`data.frame`)\cr Dataset for the first sample. |
|
| 61 |
#' @param df2 (`data.frame`)\cr Dataset for the second sample. |
|
| 62 |
#' @param paired (`logical(1)`)\cr Whether the samples are paired. |
|
| 63 |
#' @param paired_by (`character` or `NULL`)\cr Column name(s) in `df1` and `df2` |
|
| 64 |
#' used to match observations between datasets. |
|
| 65 |
#' Required when `paired = TRUE` and must uniquely identify each pair in both |
|
| 66 |
#' datasets. |
|
| 67 |
#' |
|
| 68 |
#' @return |
|
| 69 |
#' * `s_diff_means()` returns a named `list` containing the quantities described |
|
| 70 |
#' in the Description section. |
|
| 71 |
#' |
|
| 72 |
#' @seealso `safe_t_test()`, `extract_vectors()` |
|
| 73 |
#' |
|
| 74 |
#' @importFrom tern f_conf_level |
|
| 75 |
#' @importFrom formatters with_label |
|
| 76 |
#' |
|
| 77 |
#' @export |
|
| 78 |
#' |
|
| 79 |
#' @examples |
|
| 80 |
#' |
|
| 81 |
#' ## Statistics function: |
|
| 82 |
#' |
|
| 83 |
#' df1 <- data.frame(id = c("A", "B", "C", "D"), value = 1:4)
|
|
| 84 |
#' df2 <- data.frame(id = c("A", "C", "D", "E", "F"), value = c(3:1, NA, 16))
|
|
| 85 |
#' |
|
| 86 |
#' # Unpaired samples. |
|
| 87 |
#' s_diff_means(df1, df2, "value", conf.level = .8) |
|
| 88 |
#' |
|
| 89 |
#' # Paired samples. |
|
| 90 |
#' s_diff_means(df1, df2, "value", paired = TRUE, paired_by = "id") |
|
| 91 |
#' |
|
| 92 |
s_diff_means <- function(df1, |
|
| 93 |
df2, |
|
| 94 |
.var, |
|
| 95 |
paired = FALSE, |
|
| 96 |
paired_by = NULL, |
|
| 97 |
...) {
|
|
| 98 | 56x |
checkmate::assert_data_frame(df1) |
| 99 | 56x |
checkmate::assert_data_frame(df2) |
| 100 | 56x |
checkmate::assert_string(.var) |
| 101 | 56x |
checkmate::assert_names(colnames(df1), must.include = .var) |
| 102 | 55x |
checkmate::assert_names(colnames(df2), must.include = .var) |
| 103 | 55x |
checkmate::assert_numeric(df1[[.var]], finite = TRUE) |
| 104 | 51x |
checkmate::assert_numeric(df2[[.var]], finite = TRUE) |
| 105 | 51x |
checkmate::assert_flag(paired, null.ok = FALSE) |
| 106 | 51x |
if (paired) {
|
| 107 | 16x |
checkmate::assert_character(paired_by) |
| 108 | 15x |
checkmate::assert_names(colnames(df1), must.include = paired_by) |
| 109 | 15x |
checkmate::assert_names(colnames(df2), must.include = paired_by) |
| 110 |
} |
|
| 111 | ||
| 112 | 50x |
vecs <- extract_vectors(df1, df2, .var, paired = paired, paired_by = paired_by) |
| 113 | 49x |
n1 <- length(vecs$x1) |
| 114 | 49x |
n2 <- length(vecs$x2) |
| 115 | ||
| 116 |
# Get estimates. |
|
| 117 | 49x |
ttest_res <- safe_t_test(vecs$x1, vecs$x2, paired = paired, ...) |
| 118 | 49x |
est <- if (paired) {
|
| 119 | 14x |
ttest_res$estimate |
| 120 |
} else {
|
|
| 121 | 35x |
ttest_res$estimate[1] - ttest_res$estimate[2] |
| 122 |
} |
|
| 123 | 49x |
ci <- ttest_res$conf.int |
| 124 | 49x |
se <- ttest_res$stderr |
| 125 | 49x |
conf.level <- attr(ci, "conf.level") |
| 126 | ||
| 127 | 49x |
names(n1) <- "diff_means_n1" |
| 128 | 49x |
names(n2) <- "diff_means_n2" |
| 129 | 49x |
names(est) <- "diff_means_est" |
| 130 | 49x |
names(ci) <- c("diff_means_ci_lwr", "diff_means_ci_upr")
|
| 131 | 49x |
names(se) <- "diff_means_se" |
| 132 | 49x |
est_ci <- c(est, ci) |
| 133 | 49x |
attr(est_ci, "conf.level") <- conf.level |
| 134 | ||
| 135 | 49x |
label <- "Difference in Means" |
| 136 | 49x |
cl <- tern::f_conf_level(conf.level) |
| 137 | ||
| 138 | 49x |
y <- list() |
| 139 | 49x |
y$diff_means_n1 <- formatters::with_label(n1, paste(label, "Sample Size (Group 1)")) |
| 140 | 49x |
y$diff_means_n2 <- formatters::with_label(n2, paste(label, "Sample Size (Group 2)")) |
| 141 | 49x |
y$diff_means_est <- formatters::with_label(est, label) |
| 142 | 49x |
y$diff_means_se <- formatters::with_label(se, paste(label, "SE")) |
| 143 | 49x |
y$diff_means_est_se <- formatters::with_label(c(est, se), paste(label, "(SE)")) |
| 144 | 49x |
y$diff_means_ci <- formatters::with_label(ci, paste(label, cl)) |
| 145 | 49x |
y$diff_means_est_ci <- formatters::with_label(est_ci, paste0(label, " (", cl, ")"))
|
| 146 | ||
| 147 | 49x |
y |
| 148 |
} |
|
| 149 | ||
| 150 |
#' @describeIn diff_means Formatted analysis function which is used as `afun` in |
|
| 151 |
#' [rtables::analyze()]. |
|
| 152 |
#' |
|
| 153 |
#' The sample from the first distribution is `df[[.var]]`, and the sample from |
|
| 154 |
#' the second distribution is `.ref_group[[.var]]`, where `.ref_group` is |
|
| 155 |
#' computed by [get_ref_info()] using `.spl_context` and `ref_path`. |
|
| 156 |
#' |
|
| 157 |
#' If `ref_path = NULL`, then `a_diff_means()` returns an [rtables::in_rows()] |
|
| 158 |
#' result with an empty `formatted_cell` for every statistic specified in |
|
| 159 |
#' `.stats`. |
|
| 160 |
#' |
|
| 161 |
#' @param df (`data.frame`)\cr Dataset for the first sample. |
|
| 162 |
#' @param ref_path (`character`)\cr Global reference group specification; see |
|
| 163 |
#' [get_ref_info()]. This specifies the column path for the second sample. |
|
| 164 |
#' Specifically, the second sample is `.ref_group[[.var]]`, where |
|
| 165 |
#' `.ref_group` is computed by [get_ref_info()] using `.spl_context` and |
|
| 166 |
#' `ref_path`. |
|
| 167 |
#' |
|
| 168 |
#' This is a workaround for an [rtables] limitation in supporting |
|
| 169 |
#' `.ref_group` / `.in_ref_col` in the presence of stacked headers. |
|
| 170 |
#' Normally, `.ref_group` and `.in_ref_col` are provided by the [rtables] |
|
| 171 |
#' tabulation framework when the current column split is within the reference |
|
| 172 |
#' variable. |
|
| 173 |
#' @param .stats (`character`)\cr Statistics to include in the table. |
|
| 174 |
#' |
|
| 175 |
#' Available options are: |
|
| 176 |
#' ``r shQuote(junco_get_stats("diff_means"), type = "sh")``
|
|
| 177 |
#' |
|
| 178 |
#' If `NULL`, all statistics are included. |
|
| 179 |
#' |
|
| 180 |
#' @return |
|
| 181 |
#' * `a_diff_means()` returns formatted [rtables::in_rows()] result with the |
|
| 182 |
#' quantities requested in `.stats`. |
|
| 183 |
#' |
|
| 184 |
#' @importFrom stats setNames |
|
| 185 |
#' |
|
| 186 |
#' @export |
|
| 187 |
#' |
|
| 188 |
#' @examples |
|
| 189 |
#' |
|
| 190 |
#' ## Analysis function: |
|
| 191 |
#' |
|
| 192 |
#' lyt <- basic_table() |> |
|
| 193 |
#' split_cols_by("ARM") |>
|
|
| 194 |
#' split_rows_by( |
|
| 195 |
#' "PARAMCD", |
|
| 196 |
#' labels_var = "PARAM", |
|
| 197 |
#' split_fun = keep_split_levels(c("SYSBP", "DIABP"))
|
|
| 198 |
#' ) |> |
|
| 199 |
#' split_rows_by( |
|
| 200 |
#' "AVISIT", |
|
| 201 |
#' split_fun = keep_split_levels(c("BASELINE", "WEEK 1 DAY 8"))
|
|
| 202 |
#' ) |> |
|
| 203 |
#' analyze( |
|
| 204 |
#' "AVAL", |
|
| 205 |
#' afun = tern::a_summary, |
|
| 206 |
#' extra_args = list(.stats = c("n", "mean")),
|
|
| 207 |
#' show_labels = "hidden" |
|
| 208 |
#' ) |> |
|
| 209 |
#' analyze( |
|
| 210 |
#' "AVAL", |
|
| 211 |
#' afun = a_diff_means, |
|
| 212 |
#' table_names = "aval_diff", |
|
| 213 |
#' extra_args = list( |
|
| 214 |
#' .stats = "diff_means_est_ci", |
|
| 215 |
#' ref_path = c("ARM", "B: Placebo")
|
|
| 216 |
#' ), |
|
| 217 |
#' show_labels = "hidden" |
|
| 218 |
#' ) |
|
| 219 |
#' |
|
| 220 |
#' tbl <- build_table(lyt, formatters::ex_advs) |
|
| 221 |
#' tbl |
|
| 222 |
#' |
|
| 223 |
a_diff_means <- function( |
|
| 224 |
df, |
|
| 225 |
.var, |
|
| 226 |
.spl_context, |
|
| 227 |
ref_path = NULL, |
|
| 228 |
..., |
|
| 229 |
.stats = NULL, |
|
| 230 |
.formats = NULL, |
|
| 231 |
.labels = NULL, |
|
| 232 |
.indent_mods = NULL |
|
| 233 |
) {
|
|
| 234 | 50x |
checkmate::assert_data_frame(df) |
| 235 | 50x |
checkmate::assert_string(.var) |
| 236 | 50x |
checkmate::assert_data_frame(.spl_context) |
| 237 | 50x |
checkmate::assert_character(ref_path, min.len = 1L, null.ok = TRUE) |
| 238 | 50x |
checkmate::assert_character(.stats, null.ok = TRUE) |
| 239 | 50x |
if (!is.null(.stats)) {
|
| 240 | 1x |
checkmate::assert_names(.stats, subset.of = junco_get_stats("diff_means"))
|
| 241 |
} |
|
| 242 | ||
| 243 | 50x |
ref <- get_ref_info(ref_path, .spl_context) |
| 244 | 50x |
.ref_group <- ref$ref_group |
| 245 | 50x |
.in_ref_col <- ref$in_ref_col |
| 246 | ||
| 247 | 50x |
y <- if (is.null(.in_ref_col) || .in_ref_col) {
|
| 248 | 16x |
.stats <- junco_get_stats("diff_means", stats_in = .stats)
|
| 249 | 16x |
setNames(vector(mode = "list", length = length(.stats)), .stats) |
| 250 |
} else {
|
|
| 251 | 34x |
s_diff_means(df1 = df, df2 = .ref_group, .var = .var, ...) |
| 252 |
} |
|
| 253 | ||
| 254 | 50x |
format_stats( |
| 255 | 50x |
y, |
| 256 | 50x |
method_groups = "diff_means", |
| 257 | 50x |
stats_in = .stats, |
| 258 | 50x |
formats_in = .formats, |
| 259 | 50x |
labels_in = .labels, |
| 260 | 50x |
indents_in = .indent_mods |
| 261 |
) |
|
| 262 |
} |
| 1 |
#' MMRM Analysis for Imputed Datasets |
|
| 2 |
#' |
|
| 3 |
#' Performs an MMRM for two or more groups returning the estimated |
|
| 4 |
#' 'treatment effect' (i.e. the contrast between treatment groups and the control |
|
| 5 |
#' group) and the least square means estimates in each group. |
|
| 6 |
#' |
|
| 7 |
#' @param data (`data.frame`)\cr containing the data to be used in the model. |
|
| 8 |
#' @param vars (`vars`)\cr list as generated by the set_vars() function from the rbmi package. |
|
| 9 |
#' Only the `subjid`, `group`, |
|
| 10 |
#' `visit`, `outcome` and `covariates` elements are required. See details. |
|
| 11 |
#' @param cov_struct (`string`)\cr the covariance structure to use. Note that the same |
|
| 12 |
#' covariance structure is assumed for all treatment groups. |
|
| 13 |
#' @param visits (`NULL` or `character`)\cr An optional character vector specifying |
|
| 14 |
#' which visits to fit the MMRM at. If `NULL`, the MMRM model will be fit to |
|
| 15 |
#' the whole dataset. |
|
| 16 |
#' @param weights (`string`)\cr the weighting strategy to be used when calculating the |
|
| 17 |
#' least square means, either `'counterfactual'` or `'equal'`. |
|
| 18 |
#' @param ... additional arguments passed to [mmrm::mmrm()], in particular |
|
| 19 |
#' `method` and `vcov` to control the degrees of freedom and variance-covariance |
|
| 20 |
#' adjustment methods as well as `reml` decide between REML and ML estimation. |
|
| 21 |
#' |
|
| 22 |
#' @details |
|
| 23 |
#' The function works as follows: |
|
| 24 |
#' |
|
| 25 |
#' 1. Optionally select the subset of the `data` corresponding to `visits. |
|
| 26 |
#' 2. Fit an MMRM as `vars$outcome ~ vars$group + vars$visit + vars$covariates` |
|
| 27 |
#' with the specified covariance structure for visits within subjects. |
|
| 28 |
#' 3. Extract the 'treatment effect' & least square means for each treatment group |
|
| 29 |
#' vs the control group. |
|
| 30 |
#' |
|
| 31 |
#' In order to meet the formatting standards set by the analyse() function from the rbmi package, |
|
| 32 |
#' the results will be collapsed |
|
| 33 |
#' into a single list suffixed by the visit name, e.g.: |
|
| 34 |
#' ``` |
|
| 35 |
#' list( |
|
| 36 |
#' var_B_visit_1 = list(est = ...), |
|
| 37 |
#' trt_B_visit_1 = list(est = ...), |
|
| 38 |
#' lsm_A_visit_1 = list(est = ...), |
|
| 39 |
#' lsm_B_visit_1 = list(est = ...), |
|
| 40 |
#' var_B_visit_2 = list(est = ...), |
|
| 41 |
#' trt_B_visit_2 = list(est = ...), |
|
| 42 |
#' lsm_A_visit_2 = list(est = ...), |
|
| 43 |
#' lsm_B_visit_2 = list(est = ...), |
|
| 44 |
#' ... |
|
| 45 |
#' ) |
|
| 46 |
#' ``` |
|
| 47 |
#' Please note that 'trt' refers to the treatment effects, and 'lsm' refers to the least |
|
| 48 |
#' square mean results. In the above example `vars$group` has two factor levels A and B. |
|
| 49 |
#' The new 'var' refers to the model estimated variance of the residuals at the given |
|
| 50 |
#' visit, together with the degrees of freedom (which is treatment group specific). |
|
| 51 |
#' |
|
| 52 |
#' If you want to include additional interaction terms in your model this can be done |
|
| 53 |
#' by providing them to the `covariates` argument of the set_vars() function from the rbmi package |
|
| 54 |
#' e.g. `set_vars(covariates = c('sex*age'))`.
|
|
| 55 |
#' |
|
| 56 |
#' @note The `group` and `visit` interaction `group:visit` is not included by |
|
| 57 |
#' default in the model, therefore please add that to `covariates` manually if |
|
| 58 |
#' you want to include it. This will make sense in most cases. |
|
| 59 |
#' @inherit rbmi_ancova return |
|
| 60 |
#' |
|
| 61 |
#' @seealso [rbmi_analyse()] |
|
| 62 |
#' @seealso [mmrm::mmrm()] |
|
| 63 |
#' @seealso The set_vars() function from the rbmi package |
|
| 64 |
#' @export |
|
| 65 |
rbmi_mmrm <- function( |
|
| 66 |
data, |
|
| 67 |
vars, |
|
| 68 |
cov_struct = c("us", "toep", "cs", "ar1"),
|
|
| 69 |
visits = NULL, |
|
| 70 |
weights = c("counterfactual", "equal"),
|
|
| 71 |
... |
|
| 72 |
) {
|
|
| 73 | 1x |
subjid <- vars[["subjid"]] |
| 74 | 1x |
outcome <- vars[["outcome"]] |
| 75 | 1x |
group <- vars[["group"]] |
| 76 | 1x |
visit <- vars[["visit"]] |
| 77 | 1x |
covariates <- vars[["covariates"]] |
| 78 | ||
| 79 | 1x |
checkmate::assert_string(subjid) |
| 80 | 1x |
checkmate::assert_string(outcome) |
| 81 | 1x |
checkmate::assert_string(group) |
| 82 | 1x |
checkmate::assert_string(visit) |
| 83 | 1x |
checkmate::assert_character(covariates, null.ok = TRUE) |
| 84 | ||
| 85 | 1x |
cov_struct <- match.arg(cov_struct) |
| 86 | 1x |
weights <- match.arg(weights) |
| 87 | ||
| 88 | 1x |
expected_vars <- c((utils::getFromNamespace("extract_covariates", "rbmi"))(covariates), outcome, group, subjid, visit)
|
| 89 | 1x |
checkmate::assert_subset(expected_vars, names(data)) |
| 90 | ||
| 91 | 1x |
checkmate::assert_factor(data[[visit]]) |
| 92 | 1x |
if (is.null(visits)) {
|
| 93 | 1x |
visits <- as.character(unique(data[[visit]])) |
| 94 |
} |
|
| 95 | 1x |
checkmate::assert_subset(visits, as.character(data[[visit]])) |
| 96 | 1x |
data <- data[data[[visit]] %in% visits, , drop = FALSE] |
| 97 | ||
| 98 | 1x |
covariates_part <- paste(covariates, collapse = " + ") |
| 99 | 1x |
grp_visit_part <- paste(group, "+", visit) |
| 100 | 1x |
random_effects_part <- paste0(cov_struct, "(", visit, " | ", subjid, ")")
|
| 101 | 1x |
rhs_formula <- paste(grp_visit_part, "+", random_effects_part) |
| 102 | 1x |
if (covariates_part != "") rhs_formula <- paste(covariates_part, "+", rhs_formula) |
| 103 | 1x |
formula <- stats::as.formula(paste(outcome, "~", rhs_formula)) |
| 104 | ||
| 105 | 1x |
fit <- mmrm::mmrm(formula, data = data, ...) |
| 106 | 1x |
res <- lapply(visits, function(x) {
|
| 107 | 3x |
res <- rbmi_mmrm_single_info(fit, visit_level = x, visit = visit, group = group, weights = weights) |
| 108 | 3x |
names(res) <- paste0(names(res), "_", x) |
| 109 | 3x |
return(res) |
| 110 |
}) |
|
| 111 | 1x |
unlist(res, recursive = FALSE) |
| 112 |
} |
|
| 113 | ||
| 114 |
#' Extract Single Visit Information from a Fitted MMRM for Multiple Imputation Analysis |
|
| 115 |
#' |
|
| 116 |
#' @description |
|
| 117 |
#' Extracts relevant estimates from a given fitted MMRM. See [rbmi_mmrm()] for full details. |
|
| 118 |
#' |
|
| 119 |
#' @param fit (`mmrm`)\cr the fitted MMRM. |
|
| 120 |
#' @param visit_level (`string`)\cr the visit level to extract information for. |
|
| 121 |
#' @param visit (`string`)\cr the name of the visit variable. |
|
| 122 |
#' @param group (`string`)\cr the name of the group variable. |
|
| 123 |
#' |
|
| 124 |
#' @return a list with `trt_*`, `var_*` and `lsm_*` elements. See [rbmi_mmrm] for |
|
| 125 |
#' full details. |
|
| 126 |
#' @inheritParams rbmi_mmrm |
|
| 127 |
#' @seealso [rbmi_mmrm()] |
|
| 128 |
rbmi_mmrm_single_info <- function(fit, visit_level, visit, group, weights) {
|
|
| 129 | 3x |
checkmate::assert_class(fit, "mmrm") |
| 130 | 3x |
checkmate::assert_string(visit_level) |
| 131 | 3x |
checkmate::assert_string(visit) |
| 132 | 3x |
checkmate::assert_string(group) |
| 133 | ||
| 134 | 3x |
data <- mmrm::component(fit, "full_frame") |
| 135 | 3x |
checkmate::assert_factor(data[[group]]) |
| 136 | 3x |
grp_levels <- levels(data[[group]]) |
| 137 | ||
| 138 | 3x |
visit_at_level <- stats::setNames(list(visit_level), visit) |
| 139 | 3x |
em_res <- emmeans::emmeans(fit, group, by = visit, at = visit_at_level, weights = weights) |
| 140 | 3x |
em_df <- as.data.frame(em_res) |
| 141 | ||
| 142 | 3x |
all_lsm <- lapply(grp_levels, function(x) {
|
| 143 | 9x |
this_df <- em_df[em_df[[group]] == x, ] |
| 144 | 9x |
with(this_df, list(est = emmean, se = SE, df = df)) |
| 145 |
}) |
|
| 146 | 3x |
names(all_lsm) <- paste0("lsm_", grp_levels)
|
| 147 | ||
| 148 | 3x |
var_est <- mmrm::VarCorr(fit)[visit_level, visit_level] |
| 149 | ||
| 150 | 3x |
cont_res <- emmeans::contrast(em_res, "trt.vs.ctrl", ref = grp_levels[1]) |
| 151 | 3x |
cont_df <- as.data.frame(cont_res) |
| 152 | ||
| 153 | 3x |
all_var <- lapply(grp_levels[-1], function(x) {
|
| 154 | 6x |
this_df <- cont_df[cont_df[["contrast"]] == paste(x, "-", grp_levels[1]), ] |
| 155 |
# Note: This should be revisited here for the `se` - currently we take the SE from the LS means contrast, not |
|
| 156 |
# from the variance estimate! |
|
| 157 | 6x |
with(this_df, list(est = var_est, se = SE, df = df)) |
| 158 |
}) |
|
| 159 | 3x |
names(all_var) <- paste0("var_", grp_levels[-1])
|
| 160 | ||
| 161 | 3x |
all_trt <- lapply(grp_levels[-1], function(x) {
|
| 162 | 6x |
this_df <- cont_df[cont_df[["contrast"]] == paste(x, "-", grp_levels[1]), ] |
| 163 | 6x |
with(this_df, list(est = estimate, se = SE, df = df)) |
| 164 |
}) |
|
| 165 | 3x |
names(all_trt) <- paste0("trt_", grp_levels[-1])
|
| 166 | ||
| 167 | 3x |
c(all_var, all_trt, all_lsm) |
| 168 |
} |
| 1 |
#' @title Combined Proportion, Risk Difference, and Risk Difference Test |
|
| 2 |
#' Analysis Function with Method Selection Based on the Mantel-Fleiss |
|
| 3 |
#' Criterion |
|
| 4 |
#' |
|
| 5 |
#' @description |
|
| 6 |
#' Performs a combined analysis of proportions, treatment-vs-control risk |
|
| 7 |
#' differences, and tests of risk differences. |
|
| 8 |
#' |
|
| 9 |
#' The method used for the risk difference and risk difference test is |
|
| 10 |
#' selected based on the Mantel-Fleiss criterion. If the Mantel-Fleiss (MF) |
|
| 11 |
#' criterion is satisfied, the Cochran-Mantel-Haenszel (CMH) method with Sato |
|
| 12 |
#' variance is used for both the risk difference and the corresponding |
|
| 13 |
#' statistical test. |
|
| 14 |
#' |
|
| 15 |
#' If the Mantel-Fleiss criterion is not satisfied, an exact method is used. |
|
| 16 |
#' Specifically, the exact unconditional method is used for estimation of the |
|
| 17 |
#' risk difference, and Fisher's exact test is used for the hypothesis test. |
|
| 18 |
#' |
|
| 19 |
#' @inheritParams proposal_argument_convention |
|
| 20 |
#' @inheritParams a_proportion_diff_mf mf_threshold exact_footnote |
|
| 21 |
#' |
|
| 22 |
#' @param .df_row (`data.frame`) \cr |
|
| 23 |
#' Data set containing all analysis variables across all columns for the given |
|
| 24 |
#' row split. |
|
| 25 |
#' It is required for risk difference and risk difference test calculations |
|
| 26 |
#' to obtain the reference data. |
|
| 27 |
#' @param .var (`character(1)`)\cr |
|
| 28 |
#' Name of a response variable in `df` and `.df_row`. |
|
| 29 |
#' The variable must be a factor. |
|
| 30 |
#' @param val (`character(1)`)\cr |
|
| 31 |
#' The value used to identify the response of interest. An observation is |
|
| 32 |
#' considered a response of interest when `df[[.var]] == val` (and |
|
| 33 |
#' `.df_row[[.var]] == val` for the reference group). |
|
| 34 |
#' Since `df[[.var]]` and `.df_row[[.var]]` must be factors, `val` must be |
|
| 35 |
#' a `character` value matching one of their levels. |
|
| 36 |
#' @param colpaths (`list(3)`)\cr |
|
| 37 |
#' A named list containing elements defining the column paths corresponding |
|
| 38 |
#' to the three types of analysis. Supported names are `"prop"`, `"diff"`, and |
|
| 39 |
#' `"pval"`. Each element must contain the column path used to identify the |
|
| 40 |
#' corresponding column(s). |
|
| 41 |
#' Each column path must contain at least two elements and have an even length. |
|
| 42 |
#' For example, valid column paths include `c("ARM", "Placebo")` or
|
|
| 43 |
#' `c("ARM", "*")`.
|
|
| 44 |
#' See [rtables::col_paths()] for details on column path specification. |
|
| 45 |
#' @param trt_var (`character(1)`)\cr |
|
| 46 |
#' The name of the treatment variable. This variable is used to identify |
|
| 47 |
#' the control group in `.df_row`. |
|
| 48 |
#' It is used only for risk difference and risk difference test calculations. |
|
| 49 |
#' @param ctrl_group (`character(1)`)\cr |
|
| 50 |
#' The value of `trt_var` identifying the control treatment group. |
|
| 51 |
#' It is used only for risk difference and risk difference test calculations. |
|
| 52 |
#' @param variables (`list` or `NULL`)\cr |
|
| 53 |
#' Optional stratification variables used by the Cochran-Mantel-Haenszel |
|
| 54 |
#' analysis. These are specified as `list(strata = <strata_vars>)`, where |
|
| 55 |
#' `<strata_vars>` is a character vector containing column names in `df`. |
|
| 56 |
#' All specified strata variables must be factors. |
|
| 57 |
#' Strata variables are used only for risk difference and risk difference |
|
| 58 |
#' test calculations. |
|
| 59 |
#' @param .stats (`list`)\cr |
|
| 60 |
#' A named list of at most three elements specifying the statistic(s) to |
|
| 61 |
#' calculate for each type of analysis. Supported names are `"prop"`, `"diff"`, |
|
| 62 |
#' and `"pval"`. |
|
| 63 |
#' The supported values for each analysis type are determined by the `.stats` |
|
| 64 |
#' argument of [a_freq_j()], [a_proportion_diff_mf()], and |
|
| 65 |
#' [a_test_proportion_diff_mf()], respectively. |
|
| 66 |
#' |
|
| 67 |
#' If a statistic is not supplied for any of the three analysis types, the |
|
| 68 |
#' following defaults are used: |
|
| 69 |
#' \itemize{
|
|
| 70 |
#' \item `prop = "count_unique_denom_fraction"` |
|
| 71 |
#' \item `diff = "diff_est_ci"` |
|
| 72 |
#' \item `pval = "pval"` |
|
| 73 |
#' } |
|
| 74 |
#' |
|
| 75 |
#' @param .formats (`list`)\cr |
|
| 76 |
#' A named list of at most three elements specifying the formats for the |
|
| 77 |
#' statistics specified in `.stats`. Supported names are `"prop"`, `"diff"`, |
|
| 78 |
#' and `"pval"`. The format specified for each analysis type is passed to |
|
| 79 |
#' the corresponding analysis function. |
|
| 80 |
#' |
|
| 81 |
#' @param ... Additional arguments reserved for compatibility with the |
|
| 82 |
#' rtables analysis function interface. |
|
| 83 |
#' |
|
| 84 |
#' @details |
|
| 85 |
#' The analysis function determines which type of result to calculate based |
|
| 86 |
#' on the current column path specified by `colpaths`. |
|
| 87 |
#' \itemize{
|
|
| 88 |
#' \item `"prop"`: calculates and displays the proportion for the current |
|
| 89 |
#' analysis facet. |
|
| 90 |
#' \item `"diff"`: calculates the treatment-vs-control proportion difference. |
|
| 91 |
#' \item `"pval"`: calculates the treatment-vs-control p-value. |
|
| 92 |
#' } |
|
| 93 |
#' |
|
| 94 |
#' For proportion columns, [a_freq_j()] is used. For risk difference and |
|
| 95 |
#' p-value columns, the control group is identified from `.df_row` using |
|
| 96 |
#' `trt_var` and `ctrl_group`, and the corresponding [a_proportion_diff_mf()] |
|
| 97 |
#' or [a_test_proportion_diff_mf()] function is called. |
|
| 98 |
#' |
|
| 99 |
#' The current column is identified using [in_column()] against each element |
|
| 100 |
#' of `colpaths` list. |
|
| 101 |
#' If the current column does not correspond to any of the configured `prop`, |
|
| 102 |
#' `diff`, or `pval` paths, an empty [rtables::rcell()]. |
|
| 103 |
#' |
|
| 104 |
#' The function first determines the current row label from `.spl_context`. |
|
| 105 |
#' If the current split represents the root of the analysis, the label |
|
| 106 |
#' `"Full analysis set"` is used; otherwise, the value of the leaf split is |
|
| 107 |
#' used. |
|
| 108 |
#' |
|
| 109 |
#' @return |
|
| 110 |
#' Returns the corresponding `CellValue` or `RowsVerticalSection` object with |
|
| 111 |
#' formatted results. |
|
| 112 |
#' |
|
| 113 |
#' @seealso [a_freq_j()], [a_proportion_diff_mf()], [a_test_proportion_diff_mf()], |
|
| 114 |
#' |
|
| 115 |
#' @author WW |
|
| 116 |
#' @export |
|
| 117 |
#' @examples |
|
| 118 |
#' library(dplyr) |
|
| 119 |
#' |
|
| 120 |
#' adrs_f <- formatters::ex_adrs |> |
|
| 121 |
#' filter( |
|
| 122 |
#' ARM %in% c("A: Drug X", "B: Placebo"),
|
|
| 123 |
#' PARAMCD == "BESRSPI" |
|
| 124 |
#' ) |> |
|
| 125 |
#' droplevels() |> |
|
| 126 |
#' mutate( |
|
| 127 |
#' rsp = factor(ifelse(AVALC == "CR", "Y", "N")), |
|
| 128 |
#' ARM_FOR_RISKCOLS = ARM # Required for Risk Difference and p-value columns |
|
| 129 |
#' ) |
|
| 130 |
#' |
|
| 131 |
#' riskcols_combodf <- tribble( |
|
| 132 |
#' ~valname, ~label, ~levelcombo, ~exargs, |
|
| 133 |
#' "diff", "Risk Difference", "A: Drug X", list(), |
|
| 134 |
#' "pval", "p-value", "B: Placebo", list() |
|
| 135 |
#' ) |
|
| 136 |
#' |
|
| 137 |
#' col_split_fun <- add_combo_levels( |
|
| 138 |
#' riskcols_combodf, |
|
| 139 |
#' keep_levels = riskcols_combodf$valname |
|
| 140 |
#' ) |
|
| 141 |
#' |
|
| 142 |
#' # Additional arguments for a_combo_prop_diff_pval_mf(). |
|
| 143 |
#' extra_args <- list( |
|
| 144 |
#' colpaths = list( |
|
| 145 |
#' prop = c("ARM", "*"),
|
|
| 146 |
#' diff = c("ARM_FOR_RISKCOLS", "diff"),
|
|
| 147 |
#' pval = c("ARM_FOR_RISKCOLS", "pval")
|
|
| 148 |
#' ), |
|
| 149 |
#' id = "SUBJID", |
|
| 150 |
#' trt_var = "ARM", |
|
| 151 |
#' ctrl_group = "B: Placebo", |
|
| 152 |
#' val = "Y", |
|
| 153 |
#' variables = list(strata = "BMRKR2"), |
|
| 154 |
#' .stats = list(diff = "diff") |
|
| 155 |
#' ) |
|
| 156 |
#' |
|
| 157 |
#' lyt <- basic_table(top_level_section_div = " ") |> |
|
| 158 |
#' split_cols_by("ARM") |>
|
|
| 159 |
#' split_cols_by( |
|
| 160 |
#' "ARM_FOR_RISKCOLS", |
|
| 161 |
#' split_fun = col_split_fun, |
|
| 162 |
#' nested = FALSE |
|
| 163 |
#' ) |> |
|
| 164 |
#' analyze( |
|
| 165 |
#' "rsp", |
|
| 166 |
#' afun = a_combo_prop_diff_pval_mf, |
|
| 167 |
#' extra_args = extra_args |
|
| 168 |
#' ) |> |
|
| 169 |
#' split_rows_by( |
|
| 170 |
#' var = "SEX", |
|
| 171 |
#' split_label = "Gender", |
|
| 172 |
#' child_labels = "hidden", |
|
| 173 |
#' label_pos = "visible" |
|
| 174 |
#' ) |> |
|
| 175 |
#' analyze( |
|
| 176 |
#' vars = "rsp", |
|
| 177 |
#' afun = a_combo_prop_diff_pval_mf, |
|
| 178 |
#' extra_args = extra_args, |
|
| 179 |
#' show_labels = "hidden" |
|
| 180 |
#' ) |
|
| 181 |
#' |
|
| 182 |
#' tbl <- build_table(lyt, adrs_f) |
|
| 183 |
#' tbl |
|
| 184 |
#' |
|
| 185 |
a_combo_prop_diff_pval_mf <- function(df, |
|
| 186 |
.var, |
|
| 187 |
.df_row, |
|
| 188 |
.spl_context, |
|
| 189 |
val, |
|
| 190 |
colpaths, |
|
| 191 |
id = "USUBJID", |
|
| 192 |
trt_var, |
|
| 193 |
ctrl_group, |
|
| 194 |
variables = list(strata = NULL), |
|
| 195 |
mf_threshold = NULL, |
|
| 196 |
conf_level = 0.95, |
|
| 197 |
alternative = c("two.sided", "less", "greater"),
|
|
| 198 |
exact_footnote = rtables::RefFootnote("Exact Inference", 1L, "+"),
|
|
| 199 |
.stats = NULL, |
|
| 200 |
.formats = NULL, |
|
| 201 |
...) {
|
|
| 202 | 24x |
checkmate::assert_list(colpaths, names = "named") |
| 203 | 24x |
checkmate::assert_subset(names(colpaths), c("prop", "diff", "pval"))
|
| 204 | 24x |
checkmate::assert_character(colpaths$prop, any.missing = FALSE, min.len = 2, null.ok = TRUE) |
| 205 | 24x |
checkmate::assert_character(colpaths$diff, any.missing = FALSE, min.len = 2, null.ok = TRUE) |
| 206 | 24x |
checkmate::assert_character(colpaths$pval, any.missing = FALSE, min.len = 2, null.ok = TRUE) |
| 207 | 24x |
checkmate::assert_data_frame(df) |
| 208 | 24x |
checkmate::assert_data_frame(.df_row) |
| 209 |
# .var |
|
| 210 | 24x |
checkmate::assert_string(.var) |
| 211 | 24x |
checkmate::assert_subset(.var, colnames(df)) |
| 212 | 24x |
checkmate::assert_subset(.var, colnames(.df_row)) |
| 213 | 24x |
checkmate::assert_factor(df[[.var]]) |
| 214 | 24x |
checkmate::assert_factor(.df_row[[.var]]) |
| 215 |
# id |
|
| 216 | 24x |
checkmate::assert_string(id) |
| 217 | 24x |
checkmate::assert_subset(id, colnames(df)) |
| 218 | 24x |
checkmate::assert_subset(id, colnames(.df_row)) |
| 219 |
# trt_var |
|
| 220 | 24x |
checkmate::assert_string(trt_var) |
| 221 | 24x |
checkmate::assert_subset(trt_var, colnames(df)) |
| 222 | 24x |
checkmate::assert_subset(trt_var, colnames(.df_row)) |
| 223 | ||
| 224 | 24x |
checkmate::assert_list(.stats, names = "named", null.ok = TRUE) |
| 225 | 24x |
checkmate::assert_subset(names(.stats), c("prop", "diff", "pval"))
|
| 226 | 24x |
checkmate::assert_list(.formats, names = "named", null.ok = TRUE) |
| 227 | 24x |
checkmate::assert_subset(names(.formats), c("prop", "diff", "pval"))
|
| 228 | ||
| 229 | 24x |
leaf_splc <- .spl_context[nrow(.spl_context), ] |
| 230 | 24x |
row_label <- if (leaf_splc$split == "root" && leaf_splc$value == "root") {
|
| 231 | 8x |
"Full analysis set" |
| 232 |
} else {
|
|
| 233 | 16x |
leaf_splc$value |
| 234 |
} |
|
| 235 | ||
| 236 | 24x |
in_prop <- in_column(colpaths$prop, .spl_context) |
| 237 | 24x |
in_diff <- in_column(colpaths$diff, .spl_context) |
| 238 | 24x |
in_pval <- in_column(colpaths$pval, .spl_context) |
| 239 |
# Assert that we are in at most one column at a time. |
|
| 240 | 24x |
checkmate::assert_true(sum(in_prop, in_diff, in_pval) <= 1L) |
| 241 | ||
| 242 | 24x |
if (in_prop) {
|
| 243 |
# NA values of df[[.var]] as dropped silently! |
|
| 244 |
# https://github.com/johnsonandjohnson/junco/issues/461 |
|
| 245 | 12x |
if (is.null(.stats$prop)) {
|
| 246 | 12x |
.stats$prop <- "count_unique_denom_fraction" |
| 247 |
} |
|
| 248 | 12x |
a_freq_j( |
| 249 | 12x |
df = df, |
| 250 | 12x |
.var = .var, |
| 251 | 12x |
val = val, |
| 252 | 12x |
.df_row = .df_row, |
| 253 | 12x |
.spl_context = .spl_context, |
| 254 | 12x |
id = id, |
| 255 | 12x |
denom = "n_df", |
| 256 | 12x |
riskdiff = FALSE, |
| 257 | 12x |
variables = NULL, |
| 258 | 12x |
label = row_label, |
| 259 | 12x |
.stats = .stats$prop, |
| 260 | 12x |
.formats = .formats$prop |
| 261 |
) |
|
| 262 | 12x |
} else if (in_diff || in_pval) {
|
| 263 | 12x |
ref_indices <- .df_row[[trt_var]] == ctrl_group |
| 264 | 12x |
.ref_group <- .df_row[ref_indices, , drop = FALSE] |
| 265 | 12x |
if (in_diff) {
|
| 266 | 6x |
if (is.null(.stats$diff)) {
|
| 267 | ! |
.stats$diff <- "diff_est_ci" |
| 268 |
} |
|
| 269 | 6x |
a_proportion_diff_mf( |
| 270 | 6x |
df, |
| 271 | 6x |
.var = .var, |
| 272 | 6x |
.in_ref_col = FALSE, |
| 273 | 6x |
.ref_group = .ref_group, |
| 274 | 6x |
val = val, |
| 275 | 6x |
variables = variables, |
| 276 | 6x |
conf_level = conf_level, |
| 277 | 6x |
mf_method = "cmh_sato", |
| 278 | 6x |
mf_threshold = mf_threshold, |
| 279 | 6x |
.stats = .stats$diff, |
| 280 | 6x |
.formats = .formats$diff, |
| 281 | 6x |
.labels = setNames(list(row_label), .stats$diff), |
| 282 | 6x |
exact_footnote = exact_footnote |
| 283 |
) |
|
| 284 |
} else { # In p-value column.
|
|
| 285 | 6x |
if (is.null(.stats$pval)) {
|
| 286 | 6x |
.stats$pval <- "pval" |
| 287 |
} |
|
| 288 | 6x |
a_test_proportion_diff_mf( |
| 289 | 6x |
df, |
| 290 | 6x |
.var = .var, |
| 291 | 6x |
.in_ref_col = FALSE, |
| 292 | 6x |
.ref_group = .ref_group, |
| 293 | 6x |
val = val, |
| 294 | 6x |
variables = variables, |
| 295 | 6x |
alternative = alternative, |
| 296 | 6x |
mf_method = "cmh_sato", |
| 297 | 6x |
mf_threshold = mf_threshold, |
| 298 | 6x |
.stats = .stats$pval, |
| 299 | 6x |
.formats = .formats$pval, |
| 300 | 6x |
.labels = setNames(list(row_label), .stats$pval), |
| 301 | 6x |
exact_footnote = exact_footnote |
| 302 |
) |
|
| 303 |
} |
|
| 304 |
} else {
|
|
| 305 | ! |
rcell(NULL, format = NULL, label = row_label) |
| 306 |
} |
|
| 307 |
} |
| 1 |
#' Function Factory to Create Padded In Rows Content |
|
| 2 |
#' |
|
| 3 |
#' @param length_out (`numeric` or `NULL`)\cr full length which should be padded |
|
| 4 |
#' by `NA` which will be printed as empty strings. |
|
| 5 |
#' @param label (`string`)\cr row label to be used for the first row only. |
|
| 6 |
#' |
|
| 7 |
#' @return The function of `content` and `.formats`. |
|
| 8 |
#' @keywords internal |
|
| 9 |
pad_in_rows_fct <- function(length_out = NULL, label = "") {
|
|
| 10 | 169x |
checkmate::assert_count(length_out, null.ok = TRUE) |
| 11 | 169x |
checkmate::assert_string(label) |
| 12 | ||
| 13 | 169x |
function(content, .formats) {
|
| 14 | 169x |
content_list <- as.list(content) |
| 15 | 169x |
if (!is.null(length_out)) {
|
| 16 | 169x |
missing_length <- length_out - length(content_list) |
| 17 | 169x |
if (missing_length > 0) {
|
| 18 | 27x |
content_list <- c(content_list, rep(NA, missing_length)) |
| 19 |
} |
|
| 20 |
} |
|
| 21 | 169x |
in_rows( |
| 22 | 169x |
.list = content_list, |
| 23 | 169x |
.names = as.character(seq_along(content_list)), |
| 24 | 169x |
.labels = c( |
| 25 | 169x |
label, |
| 26 | 169x |
rep( |
| 27 |
"", |
|
| 28 | 169x |
length(content_list) - |
| 29 | 169x |
1L |
| 30 |
) |
|
| 31 |
), |
|
| 32 | 169x |
.formats = .formats, |
| 33 | 169x |
.format_na_strs = "" |
| 34 |
) |
|
| 35 |
} |
|
| 36 |
} |
|
| 37 | ||
| 38 |
#' @rdname summarize_lsmeans_wide |
|
| 39 |
#' @export |
|
| 40 |
lsmeans_wide_first_split_fun_fct <- function(include_variance) {
|
|
| 41 | 7x |
checkmate::assert_flag(include_variance) |
| 42 | ||
| 43 | 7x |
post_fun <- function(ret, spl, fulldf, .spl_context) {
|
| 44 | 7x |
if (include_variance) {
|
| 45 | 4x |
short_split_result( |
| 46 | 4x |
reference_group = "Reference Group", |
| 47 | 4x |
testing_group = "Testing Group", |
| 48 | 4x |
variance = "", |
| 49 | 4x |
comparison = "Testing-Reference", |
| 50 | 4x |
fulldf = fulldf |
| 51 |
) |
|
| 52 |
} else {
|
|
| 53 | 3x |
short_split_result( |
| 54 | 3x |
reference_group = "Reference Group", |
| 55 | 3x |
testing_group = "Testing Group", |
| 56 | 3x |
comparison = "Testing-Reference", |
| 57 | 3x |
fulldf = fulldf |
| 58 |
) |
|
| 59 |
} |
|
| 60 |
} |
|
| 61 | 7x |
make_split_fun(post = list(post_fun)) |
| 62 |
} |
|
| 63 | ||
| 64 |
#' @rdname summarize_lsmeans_wide |
|
| 65 |
#' @export |
|
| 66 |
lsmeans_wide_second_split_fun_fct <- function(pval_sided, conf_level, include_pval) {
|
|
| 67 | 5x |
checkmate::assert_flag(include_pval) |
| 68 | ||
| 69 | 5x |
post_fun <- function(ret, spl, fulldf, .spl_context) {
|
| 70 | 18x |
colset <- .spl_context[nrow(.spl_context), "value"][[1]] |
| 71 | 18x |
if (colset %in% c("reference_group", "testing_group")) {
|
| 72 | 10x |
short_split_result( |
| 73 | 10x |
treatment = "Treatment", |
| 74 | 10x |
n = "N", |
| 75 | 10x |
lsmean = "LS Mean", |
| 76 | 10x |
se = "SE", |
| 77 | 10x |
fulldf = fulldf |
| 78 |
) |
|
| 79 | 8x |
} else if (colset == "variance") {
|
| 80 | 3x |
short_split_result(mse = "M. S. Error", df = "Error DF", fulldf = fulldf) |
| 81 |
} else {
|
|
| 82 | 5x |
if (include_pval) {
|
| 83 | 3x |
short_split_result( |
| 84 | 3x |
lsmean = "LS Mean", |
| 85 | 3x |
se = "SE", |
| 86 | 3x |
ci = f_conf_level(conf_level), |
| 87 | 3x |
pval = paste0(abs(as.numeric(pval_sided)), "-sided p-value~[super a]"), |
| 88 | 3x |
fulldf = fulldf |
| 89 |
) |
|
| 90 |
} else {
|
|
| 91 | 2x |
short_split_result( |
| 92 | 2x |
lsmean = "LS Mean", |
| 93 | 2x |
se = "SE", |
| 94 | 2x |
ci = f_conf_level(conf_level), |
| 95 | 2x |
fulldf = fulldf |
| 96 |
) |
|
| 97 |
} |
|
| 98 |
} |
|
| 99 |
} |
|
| 100 | 5x |
make_split_fun(post = list(post_fun)) |
| 101 |
} |
|
| 102 | ||
| 103 |
#' @rdname summarize_lsmeans_wide |
|
| 104 |
#' @export |
|
| 105 |
lsmeans_wide_cfun <- function( |
|
| 106 |
df, |
|
| 107 |
labelstr, |
|
| 108 |
.spl_context, |
|
| 109 |
variables, |
|
| 110 |
ref_level, |
|
| 111 |
treatment_levels, |
|
| 112 |
pval_sided = c("2", "1", "-1"),
|
|
| 113 |
conf_level, |
|
| 114 |
formats |
|
| 115 |
) {
|
|
| 116 | 169x |
this_col_split <- .spl_context[nrow(.spl_context), "cur_col_split_val"][[1]] |
| 117 | 169x |
pad_in_rows <- pad_in_rows_fct(length_out = length(treatment_levels), label = labelstr) |
| 118 | 169x |
if (this_col_split[1] %in% c("reference_group", "testing_group")) {
|
| 119 | 100x |
this_level <- if (this_col_split[1] == "reference_group") {
|
| 120 | 52x |
ref_level |
| 121 |
} else {
|
|
| 122 | 48x |
treatment_levels |
| 123 |
} |
|
| 124 | 100x |
has_this_level <- df[[variables$arm]] %in% this_level |
| 125 | 100x |
if (this_col_split[2] == "treatment") {
|
| 126 | 27x |
pad_in_rows(this_level, .formats = "xx") |
| 127 | 73x |
} else if (this_col_split[2] == "n") {
|
| 128 | 24x |
pad_in_rows(df$n[has_this_level], .formats = "xx") |
| 129 | 49x |
} else if (this_col_split[2] == "lsmean") {
|
| 130 | 24x |
pad_in_rows(df$estimate_est[has_this_level], .formats = formats$lsmean) |
| 131 |
} else {
|
|
| 132 | 25x |
pad_in_rows(df$se_est[has_this_level], .formats = formats$se) |
| 133 |
} |
|
| 134 | 69x |
} else if (this_col_split[1] == "variance") {
|
| 135 | 20x |
has_trt <- df[[variables$arm]] %in% treatment_levels |
| 136 | 20x |
all_the_same <- (length(unique(df$mse[has_trt])) == 1) && (length(unique(df$df[has_trt])) == 1) |
| 137 |
# Note: We only take the first value, because in this ANCOVA case they are identical and we don't want to |
|
| 138 |
# repeat it in the table. |
|
| 139 | 20x |
inds <- if (all_the_same) 1 else seq_len(sum(has_trt)) |
| 140 | 20x |
if (this_col_split[2] == "mse") {
|
| 141 | 10x |
pad_in_rows(df$mse[has_trt][inds], .formats = formats$mse) |
| 142 |
} else {
|
|
| 143 | 10x |
pad_in_rows(df$df[has_trt][inds], .formats = formats$df) |
| 144 |
} |
|
| 145 |
} else {
|
|
| 146 | 49x |
has_trt <- df[[variables$arm]] %in% treatment_levels |
| 147 | 49x |
switch(this_col_split[2], |
| 148 | 12x |
"lsmean" = pad_in_rows(df$estimate_contr[has_trt], .formats = formats$lsmean_diff), |
| 149 | 12x |
"se" = pad_in_rows(df$se_contr[has_trt], .formats = formats$se), |
| 150 | 12x |
"ci" = pad_in_rows( |
| 151 | 12x |
tern::combine_vectors(df$lower_cl_contr[has_trt], df$upper_cl_contr[has_trt]), |
| 152 | 12x |
.formats = formats$ci |
| 153 |
), |
|
| 154 |
"pval" = {
|
|
| 155 | 13x |
pval_sided <- match.arg(pval_sided) |
| 156 | 13x |
pval_col <- switch(pval_sided, |
| 157 | 13x |
`2` = "p_value", |
| 158 | 13x |
`1` = "p_value_greater", |
| 159 | 13x |
`-1` = "p_value_less" |
| 160 |
) |
|
| 161 | 13x |
pad_in_rows(df[[pval_col]][has_trt], .formats = formats$pval) |
| 162 |
} |
|
| 163 |
) |
|
| 164 |
} |
|
| 165 |
} |
|
| 166 | ||
| 167 |
#' Layout Generating Function for LS Means Wide Table Layouts |
|
| 168 |
#' |
|
| 169 |
#' @inheritParams proposal_argument_convention |
|
| 170 |
#' @param variables (`list`)\cr see [fit_ancova()] for required variable |
|
| 171 |
#' specifications. |
|
| 172 |
#' @param ref_level (`string`)\cr the reference level of the treatment arm variable. |
|
| 173 |
#' @param treatment_levels (`character`)\cr the non-reference levels of the treatment arm |
|
| 174 |
#' variable. |
|
| 175 |
#' @param pval_sided (`string`)\cr either '2' for two-sided or '1' for 1-sided with greater than |
|
| 176 |
#' control or '-1' for 1-sided with smaller than control alternative hypothesis. |
|
| 177 |
#' @param formats (`list`)\cr including `lsmean`, `mse`, `df`, `lsmean_diff`, `se`, |
|
| 178 |
#' `ci`, `pval` formats. |
|
| 179 |
#' @param lyt (`layout`)\cr empty layout, i.e. result of [rtables::basic_table()] |
|
| 180 |
#' @param include_variance (`flag`)\cr whether to include the variance statistics |
|
| 181 |
#' (M.S. error and d.f.). |
|
| 182 |
#' @param include_pval (`flag`)\cr whether to include the p-value column. |
|
| 183 |
#' |
|
| 184 |
#' @return Modified layout. |
|
| 185 |
#' @details The functions [lsmeans_wide_first_split_fun_fct()], [lsmeans_wide_second_split_fun_fct()] and |
|
| 186 |
#' [lsmeans_wide_cfun()] are also exported and can be used directly when the layout is slightly different (e.g. |
|
| 187 |
#' contains additional subgroup row split). |
|
| 188 |
#' |
|
| 189 |
#' @export |
|
| 190 |
#' @examples |
|
| 191 |
#' variables <- list( |
|
| 192 |
#' response = "FEV1", |
|
| 193 |
#' covariates = c("RACE", "SEX"),
|
|
| 194 |
#' arm = "ARMCD", |
|
| 195 |
#' id = "USUBJID", |
|
| 196 |
#' visit = "AVISIT" |
|
| 197 |
#' ) |
|
| 198 |
#' fit <- fit_ancova( |
|
| 199 |
#' vars = variables, |
|
| 200 |
#' data = mmrm::fev_data, |
|
| 201 |
#' conf_level = 0.9, |
|
| 202 |
#' weights_emmeans = "equal" |
|
| 203 |
#' ) |
|
| 204 |
#' anl <- broom::tidy(fit) |
|
| 205 |
#' basic_table() |> |
|
| 206 |
#' summarize_lsmeans_wide( |
|
| 207 |
#' variables = variables, |
|
| 208 |
#' ref_level = fit$ref_level, |
|
| 209 |
#' treatment_levels = fit$treatment_levels, |
|
| 210 |
#' pval_sided = "2", |
|
| 211 |
#' conf_level = 0.8 |
|
| 212 |
#' ) |> |
|
| 213 |
#' build_table(df = anl) |
|
| 214 |
summarize_lsmeans_wide <- function( |
|
| 215 |
lyt, |
|
| 216 |
variables, |
|
| 217 |
ref_level, |
|
| 218 |
treatment_levels, |
|
| 219 |
conf_level, |
|
| 220 |
pval_sided = "2", |
|
| 221 |
include_variance = TRUE, |
|
| 222 |
include_pval = TRUE, |
|
| 223 |
formats = list( |
|
| 224 |
lsmean = jjcsformat_xx("xx.x"),
|
|
| 225 |
mse = jjcsformat_xx("xx.x"),
|
|
| 226 |
df = jjcsformat_xx("xx."),
|
|
| 227 |
lsmean_diff = jjcsformat_xx("xx.x"),
|
|
| 228 |
se = jjcsformat_xx("xx.xx"),
|
|
| 229 |
ci = jjcsformat_xx("(xx.xx, xx.xx)"),
|
|
| 230 |
pval = jjcsformat_pval_fct(0) |
|
| 231 |
) |
|
| 232 |
) {
|
|
| 233 |
# Check that all required format elements are present in the formats parameter |
|
| 234 | 3x |
checkmate::assert_names( |
| 235 | 3x |
names(formats), |
| 236 | 3x |
must.include = c("lsmean", "mse", "df", "lsmean_diff", "se", "ci", "pval"),
|
| 237 | 3x |
.var.name = "formats" |
| 238 |
) |
|
| 239 | 3x |
lyt |> |
| 240 | 3x |
split_cols_by( |
| 241 | 3x |
variables$arm, |
| 242 | 3x |
split_fun = lsmeans_wide_first_split_fun_fct(include_variance = include_variance) |
| 243 |
) |> |
|
| 244 | 3x |
split_cols_by( |
| 245 | 3x |
variables$arm, |
| 246 | 3x |
split_fun = lsmeans_wide_second_split_fun_fct( |
| 247 | 3x |
include_pval = include_pval, |
| 248 | 3x |
pval_sided = pval_sided, |
| 249 | 3x |
conf_level = conf_level |
| 250 |
) |
|
| 251 |
) |> |
|
| 252 | 3x |
split_rows_by(variables$visit, section_div = "") |> |
| 253 | 3x |
summarize_row_groups( |
| 254 | 3x |
var = variables$arm, |
| 255 | 3x |
cfun = lsmeans_wide_cfun, |
| 256 | 3x |
extra_args = list( |
| 257 | 3x |
variables = variables, |
| 258 | 3x |
ref_level = ref_level, |
| 259 | 3x |
treatment_levels = treatment_levels, |
| 260 | 3x |
pval_sided = pval_sided, |
| 261 | 3x |
conf_level = conf_level, |
| 262 | 3x |
formats = formats |
| 263 |
) |
|
| 264 |
) |
|
| 265 |
} |
| 1 |
#' @name remove_col_count |
|
| 2 |
#' |
|
| 3 |
#' @title Removal of Unwanted Column Counts |
|
| 4 |
#' |
|
| 5 |
#' @description |
|
| 6 |
#' Remove the N=xx column headers for specified span_label_var columns - default is 'rrisk_header'. |
|
| 7 |
#' |
|
| 8 |
#' @details This works for only the lowest level of column splitting (since colcounts is used). |
|
| 9 |
#' |
|
| 10 |
#' @param obj (`TableTree`)\cr TableTree object. |
|
| 11 |
#' @param span_label_var (`character`)\cr |
|
| 12 |
#' The spanning header text variable value for which column headers will be removed from. |
|
| 13 |
#' |
|
| 14 |
#' @return TableTree object with column counts in specified columns removed. |
|
| 15 |
#' @export |
|
| 16 |
remove_col_count <- function(obj, span_label_var = "rrisk_header") {
|
|
| 17 |
## programatically figure out which ones we want |
|
| 18 | 1x |
unwanted_count <- function(pth) pth[1] == span_label_var |
| 19 | 1x |
to_blank <- sapply(col_paths(obj), unwanted_count) |
| 20 | 1x |
col_counts(obj)[to_blank] <- NA_integer_ |
| 21 | 1x |
return(obj) |
| 22 |
} |
| 1 |
#' Pool a scalar estimate across imputations |
|
| 2 |
#' |
|
| 3 |
#' Applies Rubin's rules to combine scalar estimates and their |
|
| 4 |
#' within-imputation variance estimates. |
|
| 5 |
#' |
|
| 6 |
#' @details For `m` imputations, the pooled estimate is the mean of `q`. The |
|
| 7 |
#' pooled variance combines the mean within-imputation variance with the |
|
| 8 |
#' between-imputation variance, using the usual `(1 + 1 / m)` multiplier. |
|
| 9 |
#' The returned standard error is the square root of the pooled variance. |
|
| 10 |
#' |
|
| 11 |
#' @param q (`numeric`) |
|
| 12 |
#' Vector of scalar estimates, with one value for each imputed dataset. |
|
| 13 |
#' @param u (`numeric`) |
|
| 14 |
#' Vector of within-imputation variance estimates corresponding to `q`. |
|
| 15 |
#' |
|
| 16 |
#' @return A named `list` with the pooled estimate (`est`), standard error |
|
| 17 |
#' (`se`), variance (`var`), and number of imputations (`m`). |
|
| 18 |
#' |
|
| 19 |
#' @seealso [pool_z_stat()] |
|
| 20 |
#' @export |
|
| 21 |
#' |
|
| 22 |
#' @examples |
|
| 23 |
#' pool_rubin_scalar( |
|
| 24 |
#' q = c(0.20, 0.25, 0.15), |
|
| 25 |
#' u = c(0.01, 0.016, 0.012) |
|
| 26 |
#' ) |
|
| 27 |
pool_rubin_scalar <- function(q, u) {
|
|
| 28 |
# q: vector of estimates |
|
| 29 |
# u: vector of within-imputation variances |
|
| 30 | 2x |
m <- length(q) |
| 31 | 2x |
qbar <- mean(q, na.rm = TRUE) |
| 32 | 2x |
ubar <- mean(u, na.rm = TRUE) |
| 33 | 2x |
b <- stats::var(q, na.rm = TRUE) |
| 34 | 2x |
tvar <- ubar + (1 + 1 / m) * b |
| 35 | 2x |
se <- sqrt(tvar) |
| 36 | 2x |
list(est = qbar, se = se, var = tvar, m = m) |
| 37 |
} |
|
| 38 | ||
| 39 |
#' Pool z statistics across imputations |
|
| 40 |
#' |
|
| 41 |
#' Combines z statistics from multiple imputed datasets and calculates a |
|
| 42 |
#' two-sided p-value for the pooled statistic. |
|
| 43 |
#' |
|
| 44 |
#' @details The within-imputation variance of a z statistic is approximately 1. |
|
| 45 |
#' The between-imputation variance is pooled using the same |
|
| 46 |
#' `(1 + 1 / m)` multiplier as [pool_rubin_scalar()]. The returned p-value is |
|
| 47 |
#' calculated from the standard normal distribution. |
|
| 48 |
#' |
|
| 49 |
#' @param z_stat_vals (`numeric`) |
|
| 50 |
#' Vector of z statistics, with one value for each imputed dataset. |
|
| 51 |
#' |
|
| 52 |
#' @return A named `list` with the pooled z statistic (`z`) and its two-sided |
|
| 53 |
#' p-value (`p`). |
|
| 54 |
#' |
|
| 55 |
#' @seealso [pool_rubin_scalar()] |
|
| 56 |
#' @export |
|
| 57 |
#' |
|
| 58 |
#' @examples |
|
| 59 |
#' pool_z_stat(c(1.1, 1.3, 1.2)) |
|
| 60 |
pool_z_stat <- function(z_stat_vals) {
|
|
| 61 | 2x |
m <- length(z_stat_vals) |
| 62 | 2x |
qbar <- mean(z_stat_vals, na.rm = TRUE) |
| 63 | 2x |
b <- stats::var(z_stat_vals, na.rm = TRUE) |
| 64 | 2x |
ubar <- 1 # approx var(z) ~ 1 because it is a z statistic |
| 65 | 2x |
tvar <- ubar + (1 + 1 / m) * b |
| 66 | 2x |
z_pool <- qbar / sqrt(tvar) |
| 67 | 2x |
p_pool <- 2 * stats::pnorm(abs(z_pool), lower.tail = FALSE) |
| 68 | 2x |
list(z = z_pool, p = p_pool) |
| 69 |
} |
| 1 |
#' Shortcut Layout Function for Standard Continuous Variable Analysis |
|
| 2 |
#' |
|
| 3 |
#' @inheritParams proposal_argument_convention |
|
| 4 |
#' @param formats (`list`)\cr formats including `mean_sd`, `median` and `range` |
|
| 5 |
#' specifications. |
|
| 6 |
#' |
|
| 7 |
#' @return Modified layout. |
|
| 8 |
#' @export |
|
| 9 |
analyze_values <- function(lyt, vars, ..., formats) {
|
|
| 10 | 1x |
checkmate::assert_list(formats) |
| 11 | 1x |
checkmate::assert_names(names(formats), must.include = c("mean_sd", "median", "range"))
|
| 12 | ||
| 13 | 1x |
analyze_vars( |
| 14 | 1x |
lyt, |
| 15 | 1x |
vars = vars, |
| 16 |
..., |
|
| 17 | 1x |
.stats = c("n", "mean_sd", "median", "range"),
|
| 18 | 1x |
.formats = c(n = "xx", mean_sd = formats$mean_sd, median = formats$median, range = formats$range), |
| 19 | 1x |
.labels = c(n = "N", mean_sd = "Mean (SD)", median = "Median", range = "Min, Max"), |
| 20 | 1x |
.indent_mods = c(n = 0, mean_sd = 1, median = 1, range = 1) |
| 21 |
) |
|
| 22 |
} |
| 1 |
#' @name cur_col_split_path_utils |
|
| 2 |
#' |
|
| 3 |
#' @title Utilities for the Current Column Split Path |
|
| 4 |
#' |
|
| 5 |
#' @description `r lifecycle::badge("stable")`
|
|
| 6 |
#' |
|
| 7 |
#' These helper functions are intended for use in [rtables] custom analysis |
|
| 8 |
#' functions that depend on the current column split context. |
|
| 9 |
#' |
|
| 10 |
#' @inheritParams proposal_argument_convention |
|
| 11 |
#' |
|
| 12 |
#' @author WW |
|
| 13 |
#' |
|
| 14 |
#' @seealso |
|
| 15 |
#' - [rtables::col_paths()] for obtaining column paths from a built table. |
|
| 16 |
#' |
|
| 17 |
#' @examples |
|
| 18 |
#' .spl_context_1 <- data.frame( |
|
| 19 |
#' cur_col_split = I(list(c("ARM"))),
|
|
| 20 |
#' cur_col_split_val = I(list(c("Placebo")))
|
|
| 21 |
#' ) |
|
| 22 |
#' |
|
| 23 |
#' .spl_context_2 <- data.frame( |
|
| 24 |
#' cur_col_split = I(list(c("ARM", "desc_stat"))),
|
|
| 25 |
#' cur_col_split_val = I(list(c("Placebo", "N")))
|
|
| 26 |
#' ) |
|
| 27 |
#' |
|
| 28 |
NULL |
|
| 29 | ||
| 30 |
#' @describeIn cur_col_split_path_utils |
|
| 31 |
#' Get the current column split path. |
|
| 32 |
#' |
|
| 33 |
#' @return |
|
| 34 |
#' * `cur_col_split_path()` returns a character vector containing the current |
|
| 35 |
#' column split path extracted from `.spl_context`, interleaved as |
|
| 36 |
#' `c(var1, val1, var2, val2, ...)`. |
|
| 37 |
#' |
|
| 38 |
#' @export |
|
| 39 |
#' |
|
| 40 |
#' @examples |
|
| 41 |
#' cur_col_split_path(.spl_context_1) |
|
| 42 |
#' cur_col_split_path(.spl_context_2) |
|
| 43 |
#' |
|
| 44 |
cur_col_split_path <- function(.spl_context) {
|
|
| 45 | 852x |
checkmate::assert_data_frame(.spl_context, min.rows = 1L) |
| 46 | 852x |
checkmate::assert_subset(c("cur_col_split", "cur_col_split_val"), choices = colnames(.spl_context))
|
| 47 | 850x |
checkmate::assert_list(.spl_context[nrow(.spl_context), "cur_col_split"], min.len = 1L) |
| 48 | 849x |
checkmate::assert_list(.spl_context[nrow(.spl_context), "cur_col_split_val"], min.len = 1L) |
| 49 | 848x |
checkmate::assert_character(.spl_context[nrow(.spl_context), "cur_col_split"][[1]], names = "unnamed") |
| 50 | 847x |
checkmate::assert_character(.spl_context[nrow(.spl_context), "cur_col_split_val"][[1]]) |
| 51 | 846x |
checkmate::assert_true( |
| 52 | 846x |
length(.spl_context[nrow(.spl_context), "cur_col_split"][[1]]) == |
| 53 | 846x |
length(.spl_context[nrow(.spl_context), "cur_col_split_val"][[1]]) |
| 54 |
) |
|
| 55 | ||
| 56 | 844x |
leaf_splc <- .spl_context[nrow(.spl_context), ] |
| 57 | 844x |
c(rbind(leaf_splc$cur_col_split[[1]], leaf_splc$cur_col_split_val[[1]])) |
| 58 |
} |
|
| 59 | ||
| 60 |
#' @describeIn cur_col_split_path_utils |
|
| 61 |
#' Determine whether a given column path matches the current column split path. |
|
| 62 |
#' |
|
| 63 |
#' @param col_path (`character` or `NULL`)\cr |
|
| 64 |
#' Column path specified as alternating split variable names and split values. |
|
| 65 |
#' Must have at least two elements and an even length, e.g. |
|
| 66 |
#' `c("ARM", "Placebo")` or `c("ARM", "*", "desc_stat", "N")`;
|
|
| 67 |
#' see [rtables::col_paths()]. |
|
| 68 |
#' |
|
| 69 |
#' The special value `"*"` can be used as a wildcard to match any split |
|
| 70 |
#' variable name or value. |
|
| 71 |
#' |
|
| 72 |
#' `NULL` can be used to indicate that no path is specified, in which case |
|
| 73 |
#' the function returns `FALSE`, regardless of `.spl_context`. |
|
| 74 |
#' |
|
| 75 |
#' @return |
|
| 76 |
#' * `in_column()` returns a single logical value indicating whether the |
|
| 77 |
#' current column split matches the specified `col_path`. |
|
| 78 |
#' |
|
| 79 |
#' @export |
|
| 80 |
#' |
|
| 81 |
#' @examples |
|
| 82 |
#' in_column(c("ARM", "Placebo"), .spl_context_1)
|
|
| 83 |
#' in_column(c("ARM", "*"), .spl_context_1)
|
|
| 84 |
#' in_column(c("ARM", "X"), .spl_context_1)
|
|
| 85 |
#' |
|
| 86 |
#' in_column(c("ARM", "*", "desc_stat", "N"), .spl_context_2)
|
|
| 87 |
#' in_column(c("ARM", "*", "desc_stat", "Mean"), .spl_context_2)
|
|
| 88 |
#' |
|
| 89 |
#' # Use in an rtables custom analysis function. |
|
| 90 |
#' |
|
| 91 |
#' data <- formatters::ex_adsl |
|
| 92 |
#' data$desc_stat <- "phantom" |
|
| 93 |
#' |
|
| 94 |
#' # analysis function |
|
| 95 |
#' a_N_mean <- function(x, .spl_context) {
|
|
| 96 |
#' if (in_column(c("ARM", "*", "desc_stat", "N"), .spl_context)) {
|
|
| 97 |
#' rcell(length(x)) |
|
| 98 |
#' } else if (in_column(c("ARM", "*", "desc_stat", "Mean"), .spl_context)) {
|
|
| 99 |
#' rcell(mean(x), format = "xx.x") |
|
| 100 |
#' } else {
|
|
| 101 |
#' rcell(NULL) |
|
| 102 |
#' } |
|
| 103 |
#' } |
|
| 104 |
#' |
|
| 105 |
#' combodf <- data.frame( |
|
| 106 |
#' valname = c("N", "Mean"),
|
|
| 107 |
#' label = c("N", "Mean"),
|
|
| 108 |
#' levelcombo = I(list(select_all_levels, select_all_levels)), |
|
| 109 |
#' exargs = I(list(list(), list())) |
|
| 110 |
#' ) |
|
| 111 |
#' |
|
| 112 |
#' lyt <- basic_table() |> |
|
| 113 |
#' split_cols_by("ARM") |>
|
|
| 114 |
#' split_cols_by( |
|
| 115 |
#' "desc_stat", |
|
| 116 |
#' split_fun = add_combo_levels(combodf, keep_levels = c("N", "Mean"))
|
|
| 117 |
#' ) |> |
|
| 118 |
#' analyze("AGE", afun = a_N_mean)
|
|
| 119 |
#' |
|
| 120 |
#' tbl <- build_table(lyt, data) |
|
| 121 |
#' tbl |
|
| 122 |
#' |
|
| 123 |
in_column <- function(col_path, .spl_context) {
|
|
| 124 | 774x |
if (is.null(col_path)) {
|
| 125 | 2x |
return(FALSE) |
| 126 |
} |
|
| 127 | ||
| 128 | 772x |
checkmate::assert_character(col_path, min.len = 2L, names = "unnamed") |
| 129 | 770x |
checkmate::assert_true(length(col_path) %% 2 == 0) |
| 130 | 769x |
checkmate::assert_data_frame(.spl_context, min.rows = 1L) |
| 131 | ||
| 132 | 767x |
cur_colpath <- cur_col_split_path(.spl_context) |
| 133 | ||
| 134 | 767x |
if (length(col_path) == length(cur_colpath)) {
|
| 135 | 664x |
fixed <- which(col_path != "*") |
| 136 | 664x |
identical(col_path[fixed], cur_colpath[fixed]) |
| 137 |
} else {
|
|
| 138 | 103x |
FALSE |
| 139 |
} |
|
| 140 |
} |
| 1 |
#' Tabulation of Least Square Means Results |
|
| 2 |
#' |
|
| 3 |
#' @description `r lifecycle::badge('stable')`
|
|
| 4 |
#' |
|
| 5 |
#' These functions can be used to produce tables from LS means, e.g. from [fit_mmrm_j()] |
|
| 6 |
#' or [fit_ancova()]. |
|
| 7 |
#' |
|
| 8 |
#' @note These functions have been forked from the `tern.mmrm` package. Additional features |
|
| 9 |
#' are: |
|
| 10 |
#' |
|
| 11 |
#' * Additional `ref_path` argument for tern.mmrm::summarize_lsmeans(). |
|
| 12 |
#' * The function is more general in that it also works for LS means results from ANCOVA |
|
| 13 |
#' * Additional statistic `diff_mean_est_ci` is returned |
|
| 14 |
#' * P-value sidedness can be chosen |
|
| 15 |
#' |
|
| 16 |
#' @name tabulate_lsmeans |
|
| 17 |
#' @return |
|
| 18 |
#' * For `s_lsmeans`, a list containing the same statistics returned by tern.mmrm::s_mmrm_lsmeans, |
|
| 19 |
#' with the additional `diff_mean_est_ci` three-dimensional statistic. |
|
| 20 |
#' * For `a_lsmeans`, a `VertalRowsSection` as returned by [rtables::in_rows]. |
|
| 21 |
#' |
|
| 22 |
#' @examples |
|
| 23 |
#' result <- fit_mmrm_j( |
|
| 24 |
#' vars = list( |
|
| 25 |
#' response = "FEV1", |
|
| 26 |
#' covariates = c("RACE", "SEX"),
|
|
| 27 |
#' id = "USUBJID", |
|
| 28 |
#' arm = "ARMCD", |
|
| 29 |
#' visit = "AVISIT" |
|
| 30 |
#' ), |
|
| 31 |
#' data = mmrm::fev_data, |
|
| 32 |
#' cor_struct = "unstructured", |
|
| 33 |
#' weights_emmeans = "equal" |
|
| 34 |
#' ) |
|
| 35 |
#' |
|
| 36 |
#' df <- broom::tidy(result) |
|
| 37 |
#' |
|
| 38 |
#' s_lsmeans(df[8, ], .in_ref_col = FALSE) |
|
| 39 |
#' s_lsmeans(df[8, ], .in_ref_col = FALSE, alternative = "greater", show_relative = "increase") |
|
| 40 |
#' |
|
| 41 |
#' dat_adsl <- mmrm::fev_data |> |
|
| 42 |
#' dplyr::select(USUBJID, ARMCD) |> |
|
| 43 |
#' unique() |
|
| 44 |
#' |
|
| 45 |
#' basic_table() |> |
|
| 46 |
#' split_cols_by("ARMCD") |>
|
|
| 47 |
#' add_colcounts() |> |
|
| 48 |
#' split_rows_by("AVISIT") |>
|
|
| 49 |
#' analyze( |
|
| 50 |
#' "AVISIT", |
|
| 51 |
#' afun = a_lsmeans, |
|
| 52 |
#' show_labels = "hidden", |
|
| 53 |
#' na_str = tern::default_na_str(), |
|
| 54 |
#' extra_args = list( |
|
| 55 |
#' .stats = c( |
|
| 56 |
#' "n", |
|
| 57 |
#' "adj_mean_se", |
|
| 58 |
#' "adj_mean_ci", |
|
| 59 |
#' "diff_mean_se", |
|
| 60 |
#' "diff_mean_ci" |
|
| 61 |
#' ), |
|
| 62 |
#' .labels = c( |
|
| 63 |
#' adj_mean_se = "Adj. LS Mean (Std. Error)", |
|
| 64 |
#' adj_mean_ci = "95% CI", |
|
| 65 |
#' diff_mean_ci = "95% CI" |
|
| 66 |
#' ), |
|
| 67 |
#' .formats = c(adj_mean_se = jjcsformat_xx("xx.x (xx.xx)")),
|
|
| 68 |
#' alternative = "greater", |
|
| 69 |
#' ref_path = c("ARMCD", result$ref_level)
|
|
| 70 |
#' ) |
|
| 71 |
#' ) |> |
|
| 72 |
#' build_table( |
|
| 73 |
#' df = broom::tidy(result), |
|
| 74 |
#' alt_counts_df = dat_adsl |
|
| 75 |
#' ) |
|
| 76 |
NULL |
|
| 77 | ||
| 78 |
#' @describeIn tabulate_lsmeans Helper method (for [broom::tidy()]) to prepare a `data.frame` from an |
|
| 79 |
#' `tern_model` object containing the least-squares means and contrasts. |
|
| 80 |
#' @method tidy tern_model |
|
| 81 |
#' @importFrom generics tidy |
|
| 82 |
#' @export |
|
| 83 |
tidy.tern_model <- function(x, ...) {
|
|
| 84 |
# nolint |
|
| 85 | 60x |
vars <- x$vars |
| 86 | 60x |
estimates <- x$lsmeans$estimates |
| 87 | 60x |
df <- if (is.null(vars$arm)) {
|
| 88 | ! |
nams <- names(estimates) |
| 89 | ! |
to_rename <- match(c("estimate", "se", "df", "lower_cl", "upper_cl"), nams)
|
| 90 | ! |
names(estimates)[to_rename] <- paste0(names(estimates)[to_rename], "_est") |
| 91 | ! |
estimates |
| 92 |
} else {
|
|
| 93 | 60x |
contrasts <- x$lsmeans$contrasts |
| 94 | 60x |
merge( |
| 95 | 60x |
x = estimates, |
| 96 | 60x |
y = contrasts, |
| 97 | 60x |
by = c(vars$subgroup, vars$visit, vars$arm), |
| 98 | 60x |
suffixes = c("_est", "_contr"),
|
| 99 | 60x |
all = TRUE |
| 100 |
) |
|
| 101 |
} |
|
| 102 | 60x |
if (!is.null(vars$arm)) {
|
| 103 | 60x |
df[[vars$arm]] <- factor(df[[vars$arm]], levels = levels(estimates[[vars$arm]])) |
| 104 |
} |
|
| 105 | 60x |
df[[vars$visit]] <- factor(df[[vars$visit]], levels = levels(estimates[[vars$visit]])) |
| 106 | 60x |
if (!is.null(vars$subgroup)) {
|
| 107 | 2x |
df[[vars$subgroup]] <- factor(df[[vars$subgroup]], levels = levels(estimates[[vars$subgroup]])) |
| 108 |
} |
|
| 109 | 60x |
df$conf_level <- x$conf_level |
| 110 | 60x |
if (!is.null(x$mse)) {
|
| 111 | 60x |
df$mse <- x$mse[df[[vars$visit]]] |
| 112 |
} |
|
| 113 | 60x |
if (!is.null(x$df)) {
|
| 114 | 60x |
df$df <- x$df[df[[vars$visit]]] |
| 115 |
} |
|
| 116 | 60x |
df |
| 117 |
} |
|
| 118 | ||
| 119 |
#' @describeIn tabulate_lsmeans Statistics function which is extracting estimates from a tidied least-squares means |
|
| 120 |
#' data frame. |
|
| 121 |
#' @inheritParams proposal_argument_convention |
|
| 122 |
#' @export |
|
| 123 |
s_lsmeans <- function(df, |
|
| 124 |
.in_ref_col, |
|
| 125 |
alternative = c("two.sided", "less", "greater"),
|
|
| 126 |
show_relative = c("reduction", "increase")) {
|
|
| 127 | 101x |
alternative <- match.arg(alternative) |
| 128 | 101x |
show_relative <- match.arg(show_relative) |
| 129 | 101x |
if_not_ref <- function(x) if (.in_ref_col) character() else x |
| 130 | 101x |
list( |
| 131 | 101x |
n = df$n, |
| 132 | 101x |
adj_mean_se = c(df$estimate_est, df$se_est), |
| 133 | 101x |
adj_mean_ci = with_label( |
| 134 | 101x |
c(df$lower_cl_est, df$upper_cl_est), |
| 135 | 101x |
paste0("Adjusted Mean ", f_conf_level(df$conf_level))
|
| 136 |
), |
|
| 137 | 101x |
adj_mean_est_ci = with_label( |
| 138 | 101x |
c(df$estimate_est, df$lower_cl_est, df$upper_cl_est), |
| 139 | 101x |
paste0("Adjusted Mean (", f_conf_level(df$conf_level), ")")
|
| 140 |
), |
|
| 141 | 101x |
diff_mean_se = if_not_ref(c(df$estimate_contr, df$se_contr)), |
| 142 | 101x |
diff_mean_ci = with_label( |
| 143 | 101x |
if_not_ref(c(df$lower_cl_contr, df$upper_cl_contr)), |
| 144 | 101x |
paste0("Difference in Adjusted Means ", f_conf_level(df$conf_level))
|
| 145 |
), |
|
| 146 | 101x |
diff_mean_est_ci = with_label( |
| 147 | 101x |
if_not_ref(c(df$estimate_contr, df$lower_cl_contr, df$upper_cl_contr)), |
| 148 | 101x |
paste0("Difference in Adjusted Means (", f_conf_level(df$conf_level), ")")
|
| 149 |
), |
|
| 150 | 101x |
change = switch(show_relative, |
| 151 | 101x |
reduction = with_label(if_not_ref(df$relative_reduc), "Relative Reduction (%)"), |
| 152 | 101x |
increase = with_label(if_not_ref(-df$relative_reduc), "Relative Increase (%)") |
| 153 |
), |
|
| 154 | 101x |
p_value = if_not_ref(switch(alternative, |
| 155 | 101x |
two.sided = with_label(df$p_value, "2-sided p-value"), |
| 156 | 101x |
less = with_label(df$p_value_less, "1-sided p-value (less)"), |
| 157 | 101x |
greater = with_label(df$p_value_greater, "1-sided p-value (greater)") |
| 158 |
)) |
|
| 159 |
) |
|
| 160 |
} |
|
| 161 | ||
| 162 |
#' @describeIn tabulate_lsmeans Formatted Analysis function to be used as `afun` |
|
| 163 |
#' @export |
|
| 164 |
a_lsmeans <- function(df, |
|
| 165 |
ref_path, |
|
| 166 |
.spl_context, |
|
| 167 |
..., |
|
| 168 |
.stats = NULL, |
|
| 169 |
.formats = NULL, |
|
| 170 |
.labels = NULL, |
|
| 171 |
.indent_mods = NULL) {
|
|
| 172 |
# Check for additional parameters to the statistics function |
|
| 173 | 46x |
dots_extra_args <- list(...) |
| 174 | ||
| 175 |
# Only support default stats, not custom stats |
|
| 176 | 46x |
.stats <- .split_std_from_custom_stats(.stats)$default_stats |
| 177 | ||
| 178 |
# Obtain reference column information |
|
| 179 | 46x |
ref <- get_ref_info(ref_path, .spl_context) |
| 180 | ||
| 181 |
# Apply statistics function |
|
| 182 | 46x |
x_stats <- .apply_stat_functions( |
| 183 | 46x |
default_stat_fnc = s_lsmeans, |
| 184 | 46x |
custom_stat_fnc_list = NULL, |
| 185 | 46x |
args_list = c(df = list(df), .in_ref_col = ref$in_ref_col, dots_extra_args) |
| 186 |
) |
|
| 187 | ||
| 188 |
# Format according to specifications |
|
| 189 | 46x |
format_stats( |
| 190 | 46x |
x_stats, |
| 191 | 46x |
method_groups = "tabulate_lsmeans", |
| 192 | 46x |
stats_in = .stats, |
| 193 | 46x |
formats_in = .formats, |
| 194 | 46x |
labels_in = .labels, |
| 195 | 46x |
indents_in = .indent_mods |
| 196 |
) |
|
| 197 |
} |
| 1 |
#' @importFrom stats ave |
|
| 2 |
h_get_eair_df <- function( |
|
| 3 |
levii, |
|
| 4 |
df, |
|
| 5 |
denom_df, |
|
| 6 |
.var, |
|
| 7 |
id, |
|
| 8 |
occ_var, |
|
| 9 |
occ_dy, |
|
| 10 |
fup_var, |
|
| 11 |
count_multiple_events = FALSE, |
|
| 12 |
strata = NULL |
|
| 13 |
) {
|
|
| 14 | 711x |
dfii <- df[df[[.var]] == levii & !is.na(df[[.var]]), ] |
| 15 | ||
| 16 | 711x |
df_denom <- unique(denom_df[, c(id, fup_var, strata), drop = FALSE]) |
| 17 | 711x |
if (!is.null(occ_var)) {
|
| 18 | 600x |
df_num <- unique(subset(dfii, dfii[[occ_var]] == "Y")[, c(id, .var, occ_var, occ_dy, strata), drop = FALSE]) |
| 19 | 600x |
df_num[["n_events"]] <- 1L |
| 20 | 600x |
if (any(duplicated(df_num[[id]]))) {
|
| 21 | 1x |
stop("Input dataset must uniquely identify one record per subject/.var/occ_var.")
|
| 22 |
} |
|
| 23 |
} else {
|
|
| 24 | 111x |
if (count_multiple_events) {
|
| 25 | 14x |
df_num <- dfii[, c(id, .var, strata), drop = FALSE] |
| 26 | 14x |
df_num[["n_events"]] <- ave(as.character(df_num[[id]]), as.character(df_num[[id]]), FUN = length) |
| 27 | 14x |
df_num <- unique(df_num) |
| 28 |
} else {
|
|
| 29 | 97x |
df_num <- unique(dfii[, c(id, .var, strata), drop = FALSE]) |
| 30 | 97x |
df_num[["n_events"]] <- 1L |
| 31 |
} |
|
| 32 |
} |
|
| 33 | 710x |
df_num[["n_events"]] <- as.numeric(df_num[["n_events"]]) |
| 34 |
### construct modified fup var subjects not in numerator - use fup_var from df_denom |
|
| 35 | 710x |
df_denom[["mod_fup_var"]] <- df_denom[[fup_var]] |
| 36 | ||
| 37 | 710x |
if (!is.null(occ_var)) {
|
| 38 |
### add vars from df_num onto df_denom |
|
| 39 | 599x |
df_denom <- merge(df_denom, df_num, by = c(id, strata), all.x = TRUE) |
| 40 | ||
| 41 |
# subjects in numerator dataset, use occ_dy variable/365.25 |
|
| 42 | 599x |
id_to_update <- df_denom[[id]] %in% df_num[[id]] |
| 43 | 599x |
df_denom[id_to_update, "mod_fup_var"] <- df_denom[id_to_update, occ_dy] / 365.25 |
| 44 |
} |
|
| 45 | ||
| 46 | 710x |
return(list(df_denom = df_denom, df_num = df_num)) |
| 47 |
} |
|
| 48 | ||
| 49 |
# helper: get n and T per stratum for one arm's df/denom |
|
| 50 |
.get_stratum_stats <- function(strata, strata_levels, df_arm, denom_arm, |
|
| 51 |
levii, .var, id, fup_var, occ_var, occ_dy, count_multiple_events) {
|
|
| 52 | 65x |
lapply(strata_levels, function(stratum) {
|
| 53 | 189x |
df_s <- df_arm[df_arm[[strata]] == stratum, , drop = FALSE] |
| 54 | 189x |
denom_s <- denom_arm[denom_arm[[strata]] == stratum, , drop = FALSE] |
| 55 | 189x |
eair_dfs <- h_get_eair_df( |
| 56 | 189x |
levii = levii, |
| 57 | 189x |
df = df_s, |
| 58 | 189x |
denom_df = denom_s, |
| 59 | 189x |
.var = .var, |
| 60 | 189x |
id = id, |
| 61 | 189x |
fup_var = fup_var, |
| 62 | 189x |
occ_var = occ_var, |
| 63 | 189x |
occ_dy = occ_dy, |
| 64 | 189x |
count_multiple_events = count_multiple_events, |
| 65 | 189x |
strata = strata |
| 66 |
) |
|
| 67 | 189x |
list( |
| 68 | 189x |
n = sum(eair_dfs$df_num[["n_events"]]), |
| 69 | 189x |
py = sum(eair_dfs$df_denom[["mod_fup_var"]]) |
| 70 |
) |
|
| 71 |
}) |
|
| 72 |
} |
|
| 73 | ||
| 74 |
extract_x_stats <- function(list_with_stats, stat_nms) {
|
|
| 75 | 114x |
sapply( |
| 76 | 114x |
stat_nms, |
| 77 | 114x |
function(stat) {
|
| 78 | 1254x |
sapply( |
| 79 | 1254x |
names(list_with_stats), |
| 80 | 1254x |
function(x) {
|
| 81 | 4235x |
list_with_stats[[x]][[stat]] |
| 82 |
}, |
|
| 83 | 1254x |
simplify = FALSE |
| 84 |
) |
|
| 85 |
}, |
|
| 86 | 114x |
simplify = FALSE |
| 87 |
) |
|
| 88 |
} |
| 1 |
#' Get default statistical methods and their associated formats, labels, and indent modifiers |
|
| 2 |
#' |
|
| 3 |
#' @description `r lifecycle::badge('stable')`
|
|
| 4 |
#' |
|
| 5 |
#' Utility functions to get valid statistic methods for different method groups |
|
| 6 |
#' (`.stats`) and their associated formats (`.formats`), labels (`.labels`), and indent modifiers |
|
| 7 |
#' (`.indent_mods`). This utility is used across `junco`, but some of its working principles can be |
|
| 8 |
#' seen in [tern::analyze_vars()]. |
|
| 9 |
#' |
|
| 10 |
#' @param stats (`character`)\cr statistical methods to return defaults for. |
|
| 11 |
#' @param levels_per_stats (named `list` of `character` or `NULL`)\cr named list where the name of each element is a |
|
| 12 |
#' statistic from `stats` and each element is the levels of a `factor` or `character` variable (or variable name), |
|
| 13 |
#' each corresponding to a single row, for which the named statistic should be calculated for. If a statistic is only |
|
| 14 |
#' calculated once (one row), the element can be either `NULL` or the name of the statistic. Each list element will be |
|
| 15 |
#' flattened such that the names of the list elements returned by the function have the format `statistic.level` (or |
|
| 16 |
#' just `statistic` for statistics calculated for a single row). Defaults to `NULL`. |
|
| 17 |
#' |
|
| 18 |
#' @details |
|
| 19 |
#' Current choices for `type` are `counts` and `numeric` for [tern::analyze_vars()] and affect `junco_get_stats()`. |
|
| 20 |
#' |
|
| 21 |
#' @note |
|
| 22 |
#' These defaults are experimental because we use the names of functions to retrieve the default |
|
| 23 |
#' statistics. This should be generalized in groups of methods according to more reasonable groupings. |
|
| 24 |
#' |
|
| 25 |
#' These functions have been modified from the `tern` file `utils_default_stats_formats_labels.R`. |
|
| 26 |
#' This file contains `junco` specific wrappers of functions called within the `afun` functions, |
|
| 27 |
#' in order to point to `junco` specific default statistics, formats and labels. |
|
| 28 |
#' |
|
| 29 |
#' @name default_stats_formats_labels |
|
| 30 |
NULL |
|
| 31 | ||
| 32 |
#' @describeIn default_stats_formats_labels Get statistics available for a given method |
|
| 33 |
#' group (analyze function). To check available defaults see `junco_default_stats` list. |
|
| 34 |
#' |
|
| 35 |
#' @param method_groups (`character`)\cr indicates the statistical method group (`junco` analyze function) |
|
| 36 |
#' to retrieve default statistics for. A character vector can be used to specify more than one statistical |
|
| 37 |
#' method group. |
|
| 38 |
#' @param stats_in (`character`)\cr statistics to retrieve for the selected method group. If custom statistical |
|
| 39 |
#' functions are used, `stats_in` needs to have them in too. |
|
| 40 |
#' @param custom_stats_in (`character`)\cr custom statistics to add to the default statistics. |
|
| 41 |
#' @param add_pval (`flag`)\cr should `'pval'` (or `'pval_counts'` if `method_groups` contains |
|
| 42 |
#' `'analyze_vars_counts'`) be added to the statistical methods? |
|
| 43 |
#' |
|
| 44 |
#' @return |
|
| 45 |
#' * `junco_get_stats()` returns a `character` vector of statistical methods. |
|
| 46 |
#' |
|
| 47 |
#' @export |
|
| 48 |
junco_get_stats <- function( |
|
| 49 |
method_groups = "analyze_vars_numeric", |
|
| 50 |
stats_in = NULL, |
|
| 51 |
custom_stats_in = NULL, |
|
| 52 |
add_pval = FALSE |
|
| 53 |
) {
|
|
| 54 | 1720x |
tern_get_stats( |
| 55 | 1720x |
method_groups = method_groups, |
| 56 | 1720x |
stats_in = stats_in, |
| 57 | 1720x |
custom_stats_in = custom_stats_in, |
| 58 | 1720x |
add_pval = add_pval, |
| 59 | 1720x |
tern_defaults = junco_default_stats |
| 60 |
) |
|
| 61 |
} |
|
| 62 | ||
| 63 |
#' @describeIn default_stats_formats_labels Get formats corresponding to a list of statistics. |
|
| 64 |
#' To check available defaults see list `junco_default_formats`. |
|
| 65 |
#' |
|
| 66 |
#' @param formats_in (named `vector`)\cr custom formats to use instead of defaults. Can be a character vector with |
|
| 67 |
#' values from [formatters::list_valid_format_labels()] or custom format functions. Defaults to `NULL` for any rows |
|
| 68 |
#' with no value is provided. |
|
| 69 |
#' |
|
| 70 |
#' @return |
|
| 71 |
#' * `junco_get_formats_from_stats()` returns a named list of formats as strings or functions. |
|
| 72 |
#' |
|
| 73 |
#' @note Formats in `tern` or `junco` and `rtables` can be functions that take in the table cell value and |
|
| 74 |
#' return a string. This is well documented in `vignette('custom_appearance', package = 'rtables')`.
|
|
| 75 |
#' |
|
| 76 |
#' @export |
|
| 77 |
junco_get_formats_from_stats <- function(stats, formats_in = NULL, levels_per_stats = NULL) {
|
|
| 78 | 1046x |
tern_get_formats_from_stats( |
| 79 | 1046x |
stats = stats, |
| 80 | 1046x |
formats_in = formats_in, |
| 81 | 1046x |
levels_per_stats = levels_per_stats, |
| 82 | 1046x |
tern_defaults = junco_default_formats |
| 83 |
) |
|
| 84 |
} |
|
| 85 | ||
| 86 |
#' @describeIn default_stats_formats_labels Get labels corresponding to a list of statistics. |
|
| 87 |
#' To check for available defaults see list `junco_default_labels`. |
|
| 88 |
#' |
|
| 89 |
#' @param labels_in (named `character`)\cr custom labels to use instead of defaults. If no value is provided, the |
|
| 90 |
#' variable level (if rows correspond to levels of a variable) or statistic name will be used as label. |
|
| 91 |
#' @param label_attr_from_stats (named `list`)\cr if `labels_in = NULL`, then this will be used instead. It is a list |
|
| 92 |
#' of values defined in statistical functions as default labels. Values are ignored if `labels_in` is provided or `''` |
|
| 93 |
#' values are provided. |
|
| 94 |
#' |
|
| 95 |
#' @return |
|
| 96 |
#' * `junco_get_labels_from_stats()` returns a named list of labels as strings. |
|
| 97 |
#' |
|
| 98 |
#' @export |
|
| 99 |
junco_get_labels_from_stats <- function( |
|
| 100 |
stats, |
|
| 101 |
labels_in = NULL, |
|
| 102 |
levels_per_stats = NULL, |
|
| 103 |
label_attr_from_stats = NULL |
|
| 104 |
) {
|
|
| 105 | 1105x |
tern_get_labels_from_stats( |
| 106 | 1105x |
stats = stats, |
| 107 | 1105x |
labels_in = labels_in, |
| 108 | 1105x |
levels_per_stats = levels_per_stats, |
| 109 | 1105x |
label_attr_from_stats = label_attr_from_stats, |
| 110 | 1105x |
tern_defaults = junco_default_labels |
| 111 |
) |
|
| 112 |
} |
|
| 113 | ||
| 114 |
#' @describeIn default_stats_formats_labels Get label attributes from statistics list. |
|
| 115 |
#' @param x_stats (`list`)\cr with the statistics results. |
|
| 116 |
#' @keywords internal |
|
| 117 |
get_label_attr_from_stats <- function(x_stats) {
|
|
| 118 | 344x |
sapply(x_stats, obj_label) |
| 119 |
} |
|
| 120 | ||
| 121 |
#' @describeIn default_stats_formats_labels Get row indent modifiers corresponding to a list of statistics/rows. |
|
| 122 |
#' |
|
| 123 |
#' @param indents_in (named `integer`)\cr custom row indent modifiers to use instead of defaults. Defaults to `0L` for |
|
| 124 |
#' all values. |
|
| 125 |
#' |
|
| 126 |
#' @return |
|
| 127 |
#' * `junco_get_indents_from_stats()` returns a named list of indentation modifiers as integers. By default all of the |
|
| 128 |
#' indentations will be zero. |
|
| 129 |
#' |
|
| 130 |
#' @export |
|
| 131 |
junco_get_indents_from_stats <- function(stats, indents_in = NULL, levels_per_stats = NULL) {
|
|
| 132 |
# For statistics still remaining without default indentation after looking in junco_default_indents, use |
|
| 133 |
# indentation 0 as default. |
|
| 134 | 1024x |
remaining_stats <- setdiff(stats, names(junco_default_indents)) |
| 135 | 1024x |
default_indents <- c( |
| 136 | 1024x |
junco_default_indents, |
| 137 | 1024x |
as.list(rep(0L, length(remaining_stats))) |> |
| 138 | 1024x |
stats::setNames(remaining_stats) |
| 139 |
) |
|
| 140 | ||
| 141 | 1024x |
tern_get_indents_from_stats( |
| 142 | 1024x |
stats = stats, |
| 143 | 1024x |
indents_in = indents_in, |
| 144 | 1024x |
levels_per_stats = levels_per_stats, |
| 145 | 1024x |
tern_defaults = default_indents |
| 146 |
) |
|
| 147 |
} |
|
| 148 | ||
| 149 |
#' @describeIn default_stats_formats_labels |
|
| 150 |
#' Format statistics results according to format specifications. |
|
| 151 |
#' |
|
| 152 |
#' @inheritParams rtables::in_rows .cell_footnotes .row_footnotes |
|
| 153 |
#' @return |
|
| 154 |
#' * `format_stats()` returns the correspondingly formatted [rtables::in_rows()] result. |
|
| 155 |
#' |
|
| 156 |
#' @export |
|
| 157 |
format_stats <- function(x_stats, |
|
| 158 |
method_groups, |
|
| 159 |
stats_in, |
|
| 160 |
formats_in, |
|
| 161 |
labels_in, |
|
| 162 |
indents_in, |
|
| 163 |
.cell_footnotes = list(NULL), |
|
| 164 |
.row_footnotes = list(NULL)) {
|
|
| 165 | 338x |
.stats <- junco_get_stats(method_groups, stats_in = stats_in) |
| 166 | ||
| 167 | 338x |
.formats <- junco_get_formats_from_stats(stats = .stats, formats_in = formats_in) |
| 168 | ||
| 169 | 338x |
label_attr <- get_label_attr_from_stats(x_stats) |
| 170 | 338x |
.labels <- junco_get_labels_from_stats(stats = .stats, labels_in = labels_in, label_attr_from_stats = label_attr) |
| 171 | 338x |
.labels <- .unlist_keep_nulls(.labels) |
| 172 | ||
| 173 | 338x |
.indent_mods <- junco_get_indents_from_stats(stats = .stats, indents_in = indents_in) |
| 174 | 338x |
.indent_mods <- .unlist_keep_nulls(.indent_mods) |
| 175 | ||
| 176 | 338x |
x_stats <- x_stats[.stats] |
| 177 | ||
| 178 | 338x |
in_rows( |
| 179 | 338x |
.list = x_stats, |
| 180 | 338x |
.formats = .formats, |
| 181 | 338x |
.names = names(.labels), |
| 182 | 338x |
.labels = .labels, |
| 183 | 338x |
.indent_mods = .indent_mods, |
| 184 | 338x |
.cell_footnotes = .cell_footnotes, |
| 185 | 338x |
.row_footnotes = .row_footnotes |
| 186 |
) |
|
| 187 |
} |
|
| 188 | ||
| 189 | ||
| 190 |
# junco_default_stats ----------------------------------------------------------- |
|
| 191 | ||
| 192 |
#' @describeIn default_stats_formats_labels Named list of available statistics by method group for `junco`. |
|
| 193 |
#' |
|
| 194 |
#' @format |
|
| 195 |
#' * `junco_default_stats` is a named list of available statistics, with each element |
|
| 196 |
#' named for their corresponding statistical method group. |
|
| 197 |
#' |
|
| 198 |
#' @export |
|
| 199 |
junco_default_stats <- list( |
|
| 200 |
cmhrms = c("pval"),
|
|
| 201 |
event_free = c("pt_at_risk", "event_free_rate", "rate_se", "rate_ci", "event_free_ci"),
|
|
| 202 |
kaplan_meier = c("quantiles_lower", "median_ci_3d", "quantiles_upper", "range_with_cens_info"),
|
|
| 203 |
odds_ratio = c("n_tot", "or_ci", "pval"),
|
|
| 204 |
proportion_diff = c("diff", "diff_ci", "diff_est_ci"),
|
|
| 205 |
relative_risk = c("rel_risk_ci", "pval"),
|
|
| 206 |
diff_means = c( |
|
| 207 |
"diff_means_n1", |
|
| 208 |
"diff_means_n2", |
|
| 209 |
"diff_means_est", |
|
| 210 |
"diff_means_se", |
|
| 211 |
"diff_means_est_se", |
|
| 212 |
"diff_means_ci", |
|
| 213 |
"diff_means_est_ci" |
|
| 214 |
), |
|
| 215 |
estimate_proportion = c("n_prop", "prop_ci"),
|
|
| 216 |
summarize_ancova_j = c( |
|
| 217 |
"n", |
|
| 218 |
"sum", |
|
| 219 |
"mean", |
|
| 220 |
"sd", |
|
| 221 |
"se", |
|
| 222 |
"mean_sd", |
|
| 223 |
"mean_se", |
|
| 224 |
"mean_ci", |
|
| 225 |
"mean_sei", |
|
| 226 |
"mean_sdi", |
|
| 227 |
"mean_ci_3d", |
|
| 228 |
"mean_pval", |
|
| 229 |
"median", |
|
| 230 |
"mad", |
|
| 231 |
"median_ci", |
|
| 232 |
"median_ci_3d", |
|
| 233 |
"quantiles", |
|
| 234 |
"iqr", |
|
| 235 |
"range", |
|
| 236 |
"min", |
|
| 237 |
"max", |
|
| 238 |
"median_range", |
|
| 239 |
"cv", |
|
| 240 |
"geom_mean", |
|
| 241 |
"geom_sd", |
|
| 242 |
"geom_mean_sd", |
|
| 243 |
"geom_mean_ci", |
|
| 244 |
"geom_cv", |
|
| 245 |
"geom_mean_ci_3d", |
|
| 246 |
"n_fit", |
|
| 247 |
"lsmean", |
|
| 248 |
"lsmean_se", |
|
| 249 |
"lsmean_ci", |
|
| 250 |
"lsmean_diff", |
|
| 251 |
"lsmean_diff_ci", |
|
| 252 |
"lsmean_diff_with_ci", |
|
| 253 |
"pval" |
|
| 254 |
), |
|
| 255 |
summarize_mmrm = c( |
|
| 256 |
"n", |
|
| 257 |
"adj_mean_se", |
|
| 258 |
"adj_mean_ci", |
|
| 259 |
"adj_mean_est_ci", |
|
| 260 |
"diff_mean_se", |
|
| 261 |
"diff_mean_ci", |
|
| 262 |
"diff_mean_est_ci", |
|
| 263 |
"change", |
|
| 264 |
"p_value" |
|
| 265 |
), |
|
| 266 |
tabulate_lsmeans = c( |
|
| 267 |
"n", |
|
| 268 |
"adj_mean_se", |
|
| 269 |
"adj_mean_ci", |
|
| 270 |
"adj_mean_est_ci", |
|
| 271 |
"diff_mean_se", |
|
| 272 |
"diff_mean_ci", |
|
| 273 |
"diff_mean_est_ci", |
|
| 274 |
"change", |
|
| 275 |
"p_value" |
|
| 276 |
), |
|
| 277 |
tabulate_rbmi = c( |
|
| 278 |
"adj_mean_se", |
|
| 279 |
"adj_mean_ci", |
|
| 280 |
"diff_mean_se", |
|
| 281 |
"diff_mean_ci", |
|
| 282 |
"change", |
|
| 283 |
"p_value", |
|
| 284 |
"additional_title_row" |
|
| 285 |
), |
|
| 286 |
test_proportion_diff = c("pval"),
|
|
| 287 |
a_freq_j = c( |
|
| 288 |
"n_altdf", |
|
| 289 |
"n_df", |
|
| 290 |
"n_rowdf", |
|
| 291 |
"n_parentdf", |
|
| 292 |
"denom", |
|
| 293 |
"count", |
|
| 294 |
"count_unique", |
|
| 295 |
"count_unique_fraction", |
|
| 296 |
"count_unique_denom_fraction", |
|
| 297 |
"unique_fraction" |
|
| 298 |
), |
|
| 299 |
a_patyrs_j = c("patyrs"),
|
|
| 300 |
a_eair100_j = c( |
|
| 301 |
"n_event", "person_years", |
|
| 302 |
"eair", "n_eair", "eair_ci", "eair_est_ci", "eair_n", "eair_n_py", |
|
| 303 |
"eair_diff_est_ci", "eair_diff_est", "eair_diff_ci" |
|
| 304 |
), |
|
| 305 |
a_eair_strat_j = c( |
|
| 306 |
"n_event_total", "person_years_total", |
|
| 307 |
"eair_strat", "n_eair_strat", "eair_strat_ci", "eair_strat_est_ci", |
|
| 308 |
"eair_strat_n", "eair_strat_n_py", |
|
| 309 |
"eair_strat_diff_est_ci", "eair_strat_diff_est", "eair_strat_diff_ci" |
|
| 310 |
), |
|
| 311 |
analyze_vars_numeric_j = c( |
|
| 312 |
tern_default_stats["analyze_vars_numeric"][[1]], |
|
| 313 |
"mean_diff_with_ci" |
|
| 314 |
), |
|
| 315 |
aval_chg_col1 = c("count_denom_frac", "count_frac", "count"),
|
|
| 316 |
aval_chg_col23 = c("mean_ci_3d", "mean_diff_with_ci"),
|
|
| 317 |
a_sum_ratio_j = c( |
|
| 318 |
"sum", "sum_unique", "ratio", "ratio_unique" |
|
| 319 |
) |
|
| 320 |
) |
|
| 321 | ||
| 322 |
not_in_junco <- setdiff(names(tern_default_stats), names(junco_default_stats)) |
|
| 323 |
junco_default_stats <- c(junco_default_stats, tern_default_stats[not_in_junco]) |
|
| 324 | ||
| 325 | ||
| 326 |
# junco_default_formats --------------------------------------------------------- |
|
| 327 |
junco_default_formats_start <- c( |
|
| 328 |
adj_mean_se = jjcsformat_xx("xx.xx (xx.xxx)"),
|
|
| 329 |
adj_mean_ci = jjcsformat_xx("(xx.xx, xx.xx)"),
|
|
| 330 |
adj_mean_est_ci = jjcsformat_xx("xx.xx (xx.xx, xx.xx)"),
|
|
| 331 |
change = "xx.x%", |
|
| 332 |
cv = jjcsformat_xx("xx.xx"),
|
|
| 333 |
diff = jjcsformat_xx("xx.x"),
|
|
| 334 |
diff_ci = jjcsformat_xx("(xx.x, xx.x)"),
|
|
| 335 |
diff_est_ci = jjcsformat_xx("xx.x (xx.x, xx.x)"),
|
|
| 336 |
diff_mean_se = jjcsformat_xx("xx.xx (xx.xxx)"),
|
|
| 337 |
diff_mean_ci = jjcsformat_xx("(xx.xx, xx.xx)"),
|
|
| 338 |
diff_mean_est_ci = jjcsformat_xx("xx.xx (xx.xx, xx.xx)"),
|
|
| 339 |
# |
|
| 340 |
# s_diff_means stats: |
|
| 341 |
diff_means_n1 = jjcsformat_xx("xx."),
|
|
| 342 |
diff_means_n2 = jjcsformat_xx("xx."),
|
|
| 343 |
diff_means_est = jjcsformat_xx("xx.xx"),
|
|
| 344 |
diff_means_se = jjcsformat_xx("xx.xxx"),
|
|
| 345 |
diff_means_est_se = jjcsformat_xx("xx.xx (xx.xxx)"),
|
|
| 346 |
diff_means_ci = jjcsformat_xx("(xx.xx, xx.xx)"),
|
|
| 347 |
diff_means_est_ci = jjcsformat_xx("xx.xx (xx.xx, xx.xx)"),
|
|
| 348 |
# end s_diff_means stats. |
|
| 349 |
# |
|
| 350 |
event_free_ci = jjcsformat_xx("xx.xx (xx.xx, xx.xx)"),
|
|
| 351 |
event_free_rate = jjcsformat_xx("xx.xx"),
|
|
| 352 |
geom_sd = jjcsformat_xx("xx.xxx"),
|
|
| 353 |
geom_se = jjcsformat_xx("xx.xxx"),
|
|
| 354 |
geom_mean_sd = jjcsformat_xx("xx.xx (xx.xxx)"),
|
|
| 355 |
geom_mean_se = jjcsformat_xx("xx.xx (xx.xxx)"),
|
|
| 356 |
geom_cv = jjcsformat_xx("xx.xx"),
|
|
| 357 |
geom_mean = jjcsformat_xx("xx.xx"),
|
|
| 358 |
geom_mean_ci_3d = jjcsformat_xx("xx.xx (xx.xx, xx.xx)"),
|
|
| 359 |
hr = jjcsformat_xx("xx.xx"),
|
|
| 360 |
hr_ci = jjcsformat_xx("(xx.xx, xx.xx)"),
|
|
| 361 |
hr_ci_3d = jjcsformat_xx("xx.xx (xx.xx, xx.xx)"),
|
|
| 362 |
iqr = jjcsformat_xx("xx.xx"),
|
|
| 363 |
quantiles_upper = jjcsformat_xx("xx.xx (xx.xx, xx.xx)"),
|
|
| 364 |
quantiles_lower = jjcsformat_xx("xx.xx (xx.xx, xx.xx)"),
|
|
| 365 |
lsmean = jjcsformat_xx("xx.xx"),
|
|
| 366 |
lsmean_ci = jjcsformat_xx("xx.xx (xx.xx, xx.xx)"),
|
|
| 367 |
lsmean_diff = jjcsformat_xx("xx.xx"),
|
|
| 368 |
lsmean_diff_with_ci = jjcsformat_xx("xx.xx (xx.xx, xx.xx)"),
|
|
| 369 |
lsmean_diff_ci = jjcsformat_xx("(xx.xx, xx.xx)"),
|
|
| 370 |
lsmean_se = jjcsformat_xx("xx.xx (xx.xxx)"),
|
|
| 371 |
mad = jjcsformat_xx("xx.xx"),
|
|
| 372 |
mean = jjcsformat_xx("xx.xx"),
|
|
| 373 |
mean_sd = jjcsformat_xx("xx.xx (xx.xxx)"),
|
|
| 374 |
mean_se = jjcsformat_xx("xx.xx (xx.xxx)"),
|
|
| 375 |
mean_ci_3d = jjcsformat_xx("xx.xx (xx.xx, xx.xx)"),
|
|
| 376 |
mean_diff_with_ci = jjcsformat_xx("xx.xx (xx.xx, xx.xx)"),
|
|
| 377 |
mean_pval = jjcsformat_pval_fct(0), |
|
| 378 |
median = jjcsformat_xx("xx.xx"),
|
|
| 379 |
median_ci = jjcsformat_xx("(xx.xx, xx.xx)"),
|
|
| 380 |
median_ci_3d = jjcsformat_xx("xx.xx (xx.xx, xx.xx)"),
|
|
| 381 |
median_range = jjcsformat_xx("xx.xx (xx.x, xx.x)"),
|
|
| 382 |
n = jjcsformat_xx("xx."),
|
|
| 383 |
n_tot = jjcsformat_xx("xx."),
|
|
| 384 |
n_tot_events = jjcsformat_xx("xx."),
|
|
| 385 |
or_ci = jjcsformat_xx("xx.xx (xx.xx - xx.xx)"),
|
|
| 386 |
pt_at_risk = "xx", |
|
| 387 |
pval = jjcsformat_pval_fct(0), |
|
| 388 |
pvalue = jjcsformat_pval_fct(0), |
|
| 389 |
p_value = jjcsformat_pval_fct(0), |
|
| 390 |
quantiles = jjcsformat_xx("xx.xx, xx.xx"),
|
|
| 391 |
range = jjcsformat_xx("xx.x, xx.x"),
|
|
| 392 |
range_with_cens_info = jjcsformat_range_fct("xx.xx"),
|
|
| 393 |
rate_ci = jjcsformat_xx("(xx.xx, xx.xx)"),
|
|
| 394 |
rate_se = jjcsformat_xx("xx.xx"),
|
|
| 395 |
rate_diff_ci_3d = jjcsformat_xx("xx.xx (xx.xx, xx.xx)"),
|
|
| 396 |
rel_risk_ci = jjcsformat_xx("xx.xx (xx.xx - xx.xx)"),
|
|
| 397 |
sd = jjcsformat_xx("xx.xxx"),
|
|
| 398 |
se = jjcsformat_xx("xx.xxx"),
|
|
| 399 |
n_altdf = "xx", |
|
| 400 |
n_df = "xx", |
|
| 401 |
n_rowdf = "xx", |
|
| 402 |
n_parentdf = "xx", |
|
| 403 |
denom = "xx", |
|
| 404 |
count = "xx", |
|
| 405 |
count_unique = "xx", |
|
| 406 |
count_fraction = jjcsformat_count_fraction, |
|
| 407 |
count_fraction_fixed_dp = jjcsformat_count_fraction, |
|
| 408 |
count_unique_fraction = jjcsformat_count_fraction, |
|
| 409 |
count_unique_denom_fraction = jjcsformat_count_denom_fraction, |
|
| 410 |
count_denom_frac = jjcsformat_count_denom_fraction, |
|
| 411 |
count_frac = jjcsformat_count_fraction, |
|
| 412 |
unique_fraction = jjcsformat_fraction, |
|
| 413 |
rr_ci_3d = jjcsformat_xx("xx.x (xx.x, xx.x)"),
|
|
| 414 |
patyrs = jjcsformat_xx("xx.x"),
|
|
| 415 |
eair = jjcsformat_xx("xx.x"),
|
|
| 416 |
eair_ci = jjcsformat_xx("(xx.xx, xx.xx)"),
|
|
| 417 |
eair_est_ci = jjcsformat_xx("xx.xx (xx.xx, xx.xx)"),
|
|
| 418 |
eair_diff_est_ci = jjcsformat_xx("xx.xx (xx.xx, xx.xx)"),
|
|
| 419 |
eair_diff_est = jjcsformat_xx("xx.xx"),
|
|
| 420 |
eair_diff_ci = jjcsformat_xx("(xx.xx, xx.xx)"),
|
|
| 421 |
n_eair = jjcsformat_xx("xx (xx.x)"),
|
|
| 422 |
eair_n = jjcsformat_xx("xx.x (xx)"),
|
|
| 423 |
eair_n_py = jjcsformat_xx("xx.x (xx/xx.x)"),
|
|
| 424 |
n_event = "xx", |
|
| 425 |
person_years = jjcsformat_xx("xx.xx"),
|
|
| 426 |
eair_strat = jjcsformat_xx("xx.x"),
|
|
| 427 |
n_event_total = "xx", |
|
| 428 |
person_years_total = jjcsformat_xx("xx.xx"),
|
|
| 429 |
n_eair_strat = jjcsformat_xx("xx (xx.x)"),
|
|
| 430 |
eair_strat_ci = jjcsformat_xx("(xx.xx, xx.xx)"),
|
|
| 431 |
eair_strat_est_ci = jjcsformat_xx("xx.xx (xx.xx, xx.xx)"),
|
|
| 432 |
eair_strat_diff_est_ci = jjcsformat_xx("xx.xx (xx.xx, xx.xx)"),
|
|
| 433 |
total_subject_years = jjcsformat_xx("xx.x (xx.x)"),
|
|
| 434 |
n_fit = "xx", |
|
| 435 |
unique = jjcsformat_count_fraction, |
|
| 436 |
sum = "xx", |
|
| 437 |
sum_unique = "xx", |
|
| 438 |
ratio = jjcsformat_xx("xx (xx.x%)"),
|
|
| 439 |
ratio_unique = jjcsformat_xx("xx (xx.x%)")
|
|
| 440 |
) |
|
| 441 | ||
| 442 |
tern_formats_only <- setdiff(names(tern_default_formats), names(junco_default_formats_start)) |
|
| 443 |
#' @describeIn default_stats_formats_labels Named vector of default formats for `junco`. |
|
| 444 |
#' |
|
| 445 |
#' @format |
|
| 446 |
#' * `junco_default_formats` is a named vector of available default formats, with each element |
|
| 447 |
#' named for their corresponding statistic. |
|
| 448 |
#' |
|
| 449 |
#' @export |
|
| 450 |
junco_default_formats <- c(junco_default_formats_start, tern_default_formats[tern_formats_only]) |
|
| 451 | ||
| 452 |
# junco_default_labels ---------------------------------------------------------- |
|
| 453 |
junco_default_labels_start <- c( |
|
| 454 |
additional_title_row = "Additional Title", |
|
| 455 |
adj_mean_se = "Adjusted Mean (SE)", |
|
| 456 |
adj_mean_est_ci = "Adjusted Mean (CI)", |
|
| 457 |
diff = "Difference in Response rate (%)", |
|
| 458 |
diff_mean_se = "Difference in Adjusted Means (SE)", |
|
| 459 |
diff_mean_est_ci = "Difference in Adjusted Means (CI)", |
|
| 460 |
# |
|
| 461 |
# s_diff_means stats: |
|
| 462 |
diff_means_n1 = "Difference in Means Sample Size (Group 1)", |
|
| 463 |
diff_means_n2 = "Difference in Means Sample Size (Group 2)", |
|
| 464 |
diff_means_est = "Difference in Means", |
|
| 465 |
diff_means_se = "Difference in Means SE", |
|
| 466 |
diff_means_est_se = "Difference in Means (SE)", |
|
| 467 |
diff_means_ci = "Difference in Means CI", |
|
| 468 |
diff_means_est_ci = "Difference in Means (CI)", |
|
| 469 |
# end s_diff_means stats. |
|
| 470 |
# |
|
| 471 |
hr = "Hazard Ratio", |
|
| 472 |
mean_sd = "Mean (SD)", |
|
| 473 |
median = "Median", |
|
| 474 |
median_range = "Median (min, max)", |
|
| 475 |
n_tot = "Total n", |
|
| 476 |
n_tot_events = "Total events", |
|
| 477 |
pval = "p-value", |
|
| 478 |
pvalue = "p-value", |
|
| 479 |
p_value = "p-value", |
|
| 480 |
range = "Min, max", |
|
| 481 |
range_with_cens_info = "Min, max", |
|
| 482 |
n_altdf = "N", |
|
| 483 |
n_df = "N", |
|
| 484 |
n_rowdf = "N", |
|
| 485 |
n_parentdf = "N", |
|
| 486 |
denom = "N", |
|
| 487 |
patyrs = "Patient years", |
|
| 488 |
person_years = "Person years", |
|
| 489 |
eair_strat = "Stratified EAIR", |
|
| 490 |
n_event_total = "n (events)", |
|
| 491 |
person_years_total = "Total person-years", |
|
| 492 |
n_eair_strat = "n (Stratified EAIR)", |
|
| 493 |
total_subject_years = "Total treatment (subject years)", |
|
| 494 |
sum = "Sum", |
|
| 495 |
sum_unique = "Sum (unique)", |
|
| 496 |
ratio = "Ratio", |
|
| 497 |
ratio_unique = "Ratio (unique)" |
|
| 498 |
) |
|
| 499 |
tern_labels_only <- setdiff(names(tern_default_labels), names(junco_default_labels_start)) |
|
| 500 | ||
| 501 |
#' @describeIn default_stats_formats_labels Named `character` vector of default labels for `junco`. |
|
| 502 |
#' |
|
| 503 |
#' @format |
|
| 504 |
#' * `junco_default_labels` is a named `character` vector of available default labels, with each element |
|
| 505 |
#' named for their corresponding statistic. |
|
| 506 |
#' |
|
| 507 |
#' @export |
|
| 508 |
junco_default_labels <- c(junco_default_labels_start, tern_default_labels[tern_labels_only]) |
|
| 509 | ||
| 510 |
#' @describeIn default_stats_formats_labels Named `integer` vector of default indents for `junco`. |
|
| 511 |
#' |
|
| 512 |
#' @format |
|
| 513 |
#' * `junco_default_indents` is a named `integer` vector of available default indents, with each element |
|
| 514 |
#' named for their corresponding statistic. Only indentations different from zero need to be |
|
| 515 |
#' recorded here. |
|
| 516 |
#' |
|
| 517 |
#' @export |
|
| 518 |
junco_default_indents <- c( |
|
| 519 |
additional_title_row = 1L, |
|
| 520 |
adj_mean_ci = 1L, |
|
| 521 |
adj_mean_est_ci = 1L, |
|
| 522 |
change = 1L, |
|
| 523 |
diff_ci = 1L, |
|
| 524 |
diff_mean_ci = 1L, |
|
| 525 |
diff_mean_est_ci = 1L, |
|
| 526 |
# |
|
| 527 |
# s_diff_means stats: |
|
| 528 |
diff_means_n1 = 0L, |
|
| 529 |
diff_means_n2 = 0L, |
|
| 530 |
diff_means_est = 0L, |
|
| 531 |
diff_means_se = 0L, |
|
| 532 |
diff_means_est_se = 0L, |
|
| 533 |
diff_means_ci = 0L, |
|
| 534 |
diff_means_est_ci = 0L, |
|
| 535 |
# end s_diff_means stats. |
|
| 536 |
# |
|
| 537 |
hr_ci = 1L, |
|
| 538 |
or_ci = 1L, |
|
| 539 |
pval = 1L, |
|
| 540 |
p_value = 1L, |
|
| 541 |
rate_se = 1L, |
|
| 542 |
rate_ci = 1L, |
|
| 543 |
rel_risk_ci = 1L |
|
| 544 |
) |
| 1 |
#' @name jjcs_num_formats |
|
| 2 |
#' |
|
| 3 |
#' @title Numeric Formatting Function |
|
| 4 |
#' |
|
| 5 |
#' @description |
|
| 6 |
#' Formatting setter for selected numerical statistics. |
|
| 7 |
#' |
|
| 8 |
#' @param d (`numeric`)\cr precision of individual values |
|
| 9 |
#' @param cap (`numeric`)\cr cap to numerical precision (d > cap -- |
|
| 10 |
#' will use precision as if cap was specified as precision) |
|
| 11 |
#' |
|
| 12 |
#' @return list: |
|
| 13 |
#' - fmt : named vector with formatting function (jjcsformat_xx) for numerical stats: range, median, mean_sd, sd |
|
| 14 |
#' - spec : named vector with formatting specifications for numerical stats: range, median, mean_sd, sd |
|
| 15 |
#' @export |
|
| 16 |
#' @examples |
|
| 17 |
#' P1_precision <- jjcs_num_formats(d = 0)$fmt |
|
| 18 |
#' jjcs_num_formats(2)$fmt |
|
| 19 |
#' jjcs_num_formats(2)$spec |
|
| 20 |
jjcs_num_formats <- function(d, cap = 4) {
|
|
| 21 | 6x |
fmt_xx <- function(d, cap) {
|
| 22 | 22x |
prec <- NULL |
| 23 |
### report as is |
|
| 24 | 22x |
if (is.na(d)) {
|
| 25 | 5x |
return("xx")
|
| 26 |
} |
|
| 27 | 17x |
checkmate::assertCount(d) |
| 28 | ||
| 29 |
### set a cap to number of decimal precision |
|
| 30 | 15x |
d <- min(d, cap) |
| 31 | 14x |
if (d > 0) prec <- paste0(prec, paste0(rep("x", d), collapse = ""))
|
| 32 | 15x |
if (d >= 0) prec <- paste0("xx.", prec)
|
| 33 | ||
| 34 | 15x |
return(prec) |
| 35 |
} |
|
| 36 | ||
| 37 | 6x |
fmt_rng <- function(d, cap) {
|
| 38 | 6x |
prec <- fmt_xx(d, cap = cap) |
| 39 | 4x |
fmt <- list() |
| 40 | 4x |
fmt$spec <- paste0(prec, ", ", prec) |
| 41 | 4x |
fmt$fmt <- jjcsformat_xx(fmt$spec) |
| 42 | ||
| 43 | 4x |
return(fmt) |
| 44 |
} |
|
| 45 | ||
| 46 | 6x |
fmt_median <- function(d, cap) {
|
| 47 | 4x |
prec <- fmt_xx(d, cap = cap) |
| 48 | 4x |
fmt <- list() |
| 49 | 4x |
fmt$spec <- prec |
| 50 | 4x |
fmt$fmt <- jjcsformat_xx(fmt$spec) |
| 51 | 4x |
return(fmt) |
| 52 |
} |
|
| 53 | ||
| 54 | 6x |
fmt_meansd <- function(d, cap) {
|
| 55 | 4x |
prec1 <- fmt_xx(d, cap = cap) |
| 56 | 4x |
prec2 <- fmt_xx(d + 1, cap = cap + 1) |
| 57 | 4x |
fmt <- list() |
| 58 | 4x |
fmt$spec <- paste0(prec1, " (", prec2, ")")
|
| 59 | 4x |
fmt$fmt <- jjcsformat_xx(fmt$spec) |
| 60 | 4x |
return(fmt) |
| 61 |
} |
|
| 62 | 6x |
fmt_sd <- function(d, cap) {
|
| 63 | 4x |
prec <- fmt_xx(d, cap) |
| 64 | 4x |
fmt <- list() |
| 65 | 4x |
fmt$spec <- prec |
| 66 | 4x |
fmt$fmt <- jjcsformat_xx(fmt$spec) |
| 67 | 4x |
return(fmt) |
| 68 |
} |
|
| 69 | ||
| 70 |
## apply formats for each of the stats with appropriate d and cap |
|
| 71 | 6x |
xfmt_rng <- fmt_rng(d, cap = cap) |
| 72 | 4x |
xfmt_meansd <- fmt_meansd(d + 1, cap = cap + 1) |
| 73 | 4x |
xfmt_sd <- fmt_sd(d + 2, cap = cap + 2) |
| 74 | 4x |
xfmt_median <- fmt_median(d + 1, cap = cap + 1) |
| 75 | ||
| 76 | 4x |
fmts <- list() |
| 77 | ||
| 78 | 4x |
fmts$fmt <- c(range = xfmt_rng$fmt, mean_sd = xfmt_meansd$fmt, sd = xfmt_sd$fmt, median = xfmt_median$fmt) |
| 79 | ||
| 80 | 4x |
fmts$spec <- c(range = xfmt_rng$spec, mean_sd = xfmt_meansd$spec, sd = xfmt_sd$spec, median = xfmt_median$spec) |
| 81 | ||
| 82 | 4x |
return(fmts) |
| 83 |
} |
| 1 |
#' @keywords internal |
|
| 2 |
invert_list <- function(x, simplify = FALSE) {
|
|
| 3 | ! |
stopifnot(is.list(x), !is.null(names(x))) |
| 4 | ! |
vals <- unlist(x, use.names = FALSE) |
| 5 | ! |
parents <- rep(names(x), lengths(x)) |
| 6 | ! |
out <- split(parents, vals) |
| 7 | ! |
if (simplify && all(lengths(out) == 1L)) {
|
| 8 | ! |
out <- unlist(out, use.names = TRUE) |
| 9 |
} |
|
| 10 | ! |
out |
| 11 |
} |
|
| 12 | ||
| 13 |
#' @keywords internal |
|
| 14 |
all_stats_junco_method <- junco_default_stats |
|
| 15 | ||
| 16 |
#' @keywords internal |
|
| 17 |
all_stats_junco_stat <- invert_list(all_stats_junco_method) |
|
| 18 | ||
| 19 |
#' @keywords internal |
|
| 20 |
junco_def_d_all_start <- get_fmt_details(junco_default_formats) |
|
| 21 | ||
| 22 |
# add method_group information |
|
| 23 |
#' @keywords internal |
|
| 24 |
junco_def_d_all_start$method_group <- all_stats_junco_stat[junco_def_d_all_start$stat] |
|
| 25 | ||
| 26 |
#' @keywords internal |
|
| 27 |
str_not_d_style <- c( |
|
| 28 |
"xx%", "xx.x%", "xx.xx%", "xx.xxx%", "(N=xx)", ">999.9", ">999.99", "x.xxxx | (<0.0001)", |
|
| 29 |
"N=xx (xx%)", "xx (xx%)", "xx (xx.%)", "xx (xx.x%)", "xx (xx.xx%)", "xx. (xx.%)", "xx.x (xx.x%)", "xx.xx (xx.xx%)", |
|
| 30 |
"xx / xx (xx.%)", "xx / xx (xx.x%)", "xx / xx (xx.xx%)", |
|
| 31 |
"xx.", "xx" |
|
| 32 |
) |
|
| 33 | ||
| 34 |
#' @keywords internal |
|
| 35 |
stat_not_d_style <- c( |
|
| 36 |
"additional_title_row", |
|
| 37 |
"all", |
|
| 38 |
"count", |
|
| 39 |
"count_fraction", |
|
| 40 |
"count_fraction_fixed_dp", |
|
| 41 |
"count_unique", |
|
| 42 |
"count_unique_denom_fraction", |
|
| 43 |
"count_unique_fraction", |
|
| 44 |
"denom", |
|
| 45 |
"diff_means_n1", |
|
| 46 |
"diff_means_n2", |
|
| 47 |
"eair", |
|
| 48 |
"eair_ci", |
|
| 49 |
"eair_diff_ci", |
|
| 50 |
"eair_diff_est", |
|
| 51 |
"eair_diff_est_ci", |
|
| 52 |
"eair_n", |
|
| 53 |
"eair_strat_diff_est_ci", |
|
| 54 |
"eair_strat_est_ci", |
|
| 55 |
"eair_strat_n", |
|
| 56 |
"eair_strat_n_py", |
|
| 57 |
"eair_est_ci", |
|
| 58 |
"eair_n_py", |
|
| 59 |
"eair_strat", |
|
| 60 |
"eair_strat_ci", |
|
| 61 |
"event_free_ci", |
|
| 62 |
"event_free_rate", |
|
| 63 |
"event_free_rate_3d", |
|
| 64 |
"fraction", |
|
| 65 |
"hr", |
|
| 66 |
"hr_ci", |
|
| 67 |
"hr_ci_3d", |
|
| 68 |
"lr_stat_df", |
|
| 69 |
"mean_pval", |
|
| 70 |
"n", |
|
| 71 |
"n_altdf", |
|
| 72 |
"n_blq", |
|
| 73 |
"n_df", |
|
| 74 |
"n_eair", |
|
| 75 |
"n_eair_strat", |
|
| 76 |
"n_event", |
|
| 77 |
"n_event_total", |
|
| 78 |
"n_events", |
|
| 79 |
"n_fit", |
|
| 80 |
"n_parentdf", |
|
| 81 |
"n_patients", |
|
| 82 |
"n_prop", |
|
| 83 |
"n_rate", |
|
| 84 |
"n_rowdf", |
|
| 85 |
"n_rsp", |
|
| 86 |
"n_tot", |
|
| 87 |
"n_tot_events", |
|
| 88 |
"n_unique", |
|
| 89 |
"nonunique", |
|
| 90 |
"or", |
|
| 91 |
"or_ci", |
|
| 92 |
"p_value", |
|
| 93 |
"patyrs", |
|
| 94 |
"person_years", |
|
| 95 |
"person_years_total", |
|
| 96 |
"prop", |
|
| 97 |
"prop_ci", |
|
| 98 |
"pt_at_risk", |
|
| 99 |
"pval", |
|
| 100 |
"pval_inter", |
|
| 101 |
"pvalue", |
|
| 102 |
"rate", |
|
| 103 |
"rate_ci", |
|
| 104 |
"rate_diff", |
|
| 105 |
"rate_diff_ci", |
|
| 106 |
"rate_diff_ci_3d", |
|
| 107 |
"rate_ratio", |
|
| 108 |
"rate_ratio_ci", |
|
| 109 |
"rate_se", |
|
| 110 |
"rel_risk_ci", |
|
| 111 |
"riskdiff", |
|
| 112 |
"rr_ci_3d", |
|
| 113 |
"total_subject_years", |
|
| 114 |
"unique", |
|
| 115 |
"unique_count", |
|
| 116 |
"ztest_pval" |
|
| 117 |
) |
|
| 118 | ||
| 119 | ||
| 120 |
#' @keywords internal |
|
| 121 |
target_d_style <- within(junco_def_d_all_start, {
|
|
| 122 |
target_d <- fun_fact == "jjcsformat_xx" | |
|
| 123 |
(fmt_package == "formatters" & !(str %in% str_not_d_style)) |
|
| 124 | ||
| 125 |
target_d[stat %in% stat_not_d_style] <- FALSE |
|
| 126 | ||
| 127 |
# d = 1 corresponds to default |
|
| 128 |
str_d <- ifelse( |
|
| 129 |
target_d, |
|
| 130 |
gsub("xx.x", "xx.d", str, fixed = TRUE),
|
|
| 131 |
ifelse(!is_fun, str, NA) |
|
| 132 |
) |
|
| 133 |
}) |
|
| 134 | ||
| 135 |
# proposed default formats for d-style processing ---------------- |
|
| 136 |
junco_def_d_all1 <- target_d_style[["fun"]] |
|
| 137 |
junco_def_d_all1[!is.na(target_d_style[["str_d"]])] <- |
|
| 138 |
target_d_style[["str_d"]][!is.na(target_d_style[["str_d"]])] |
|
| 139 |
names(junco_def_d_all1) <- target_d_style[["stat"]] |
|
| 140 | ||
| 141 |
#' @describeIn fmt_spec_d Named vector of default format specification using d-style notation. |
|
| 142 |
#' |
|
| 143 |
#' @format * `junco_def_d_all` Used as default input vector for argument `fmt_d_def` |
|
| 144 |
#' in functions `fmt_spec_single_d()`, `fmt_spec_df_d()` and `fmt_spec_var_d()` |
|
| 145 |
#' (corresponds to the named vector of with default available formats [junco_default_formats] |
|
| 146 |
#' with d = 1) |
|
| 147 |
#' @export |
|
| 148 |
junco_def_d_all <- junco_def_d_all1 |
|
| 149 | ||
| 150 |
# proposed alternative 1 ---------------- |
|
| 151 |
junco_def_d_alt1 <- junco_def_d_all |
|
| 152 |
junco_def_d_alt1[!is.na(target_d_style[["str_d"]])] <- |
|
| 153 |
gsub("xx.dxx", "xx.dx", junco_def_d_alt1[!is.na(target_d_style[["str_d"]])], fixed = TRUE)
|
|
| 154 | ||
| 155 | ||
| 156 |
#' @describeIn fmt_spec_d Alternative vector of d-style specification defaults. |
|
| 157 |
#' @format * `junco_def_d_alt1` Alternative vector of d-style specification defaults. |
|
| 158 |
#' @export |
|
| 159 |
junco_def_d_alt1 <- junco_def_d_alt1 |
|
| 160 | ||
| 161 |
# proposed alternative 2 ---------------- |
|
| 162 |
junco_def_d_alt2 <- junco_def_d_alt1 |
|
| 163 |
junco_def_d_alt2[!is.na(target_d_style[["str_d"]])] <- |
|
| 164 |
gsub("xx.dx", "xx.d", junco_def_d_alt2[!is.na(target_d_style[["str_d"]])], fixed = TRUE)
|
|
| 165 | ||
| 166 | ||
| 167 |
#' @describeIn fmt_spec_d Another alternative vector of d-style specification defaults. |
|
| 168 |
#' @format * `junco_def_d_alt2` Another alternative vector of d-style specification defaults. |
|
| 169 |
#' @export |
|
| 170 |
junco_def_d_alt2 <- junco_def_d_alt2 |
| 1 |
#' Helper function to normalize p-value categories |
|
| 2 |
#' |
|
| 3 |
#' @description Converts a named list of p-value category bounds into a matrix |
|
| 4 |
#' and retains the category labels in their supplied order. |
|
| 5 |
#' |
|
| 6 |
#' @param pvalcat (`named list`)\cr A non-empty named list. Each element must be |
|
| 7 |
#' a numeric vector of length two specifying the lower and upper bounds of a |
|
| 8 |
#' p-value category. |
|
| 9 |
#' |
|
| 10 |
#' @return A named `list` with the following elements: |
|
| 11 |
#' |
|
| 12 |
#' * `bounds`: a two-column numeric matrix of lower and upper category bounds. |
|
| 13 |
#' * `cats`: a character vector of category labels. |
|
| 14 |
#' |
|
| 15 |
#' @keywords internal |
|
| 16 |
h_normalize_pvalcat <- function(pvalcat) {
|
|
| 17 | 8x |
checkmate::assert_list(pvalcat, min.len = 1, names = "unique") |
| 18 | 7x |
checkmate::assert_character(names(pvalcat), any.missing = FALSE, unique = TRUE) |
| 19 | 7x |
checkmate::assert_true(all(nzchar(names(pvalcat)))) |
| 20 | ||
| 21 | 7x |
bounds <- lapply(pvalcat, function(x) {
|
| 22 | 15x |
checkmate::assert_numeric(x, len = 2, any.missing = FALSE, finite = TRUE) |
| 23 | 15x |
checkmate::assert_true(x[1] <= x[2]) |
| 24 | 14x |
x |
| 25 |
}) |
|
| 26 | ||
| 27 | 6x |
list( |
| 28 | 6x |
bounds = matrix(unlist(bounds, use.names = FALSE), ncol = 2, byrow = TRUE), |
| 29 | 6x |
cats = names(pvalcat) |
| 30 |
) |
|
| 31 |
} |
|
| 32 | ||
| 33 |
#' Categorize p-values |
|
| 34 |
#' |
|
| 35 |
#' @description Assigns each p-value to a category defined by a named list of |
|
| 36 |
#' lower and upper bounds. |
|
| 37 |
#' |
|
| 38 |
#' @details Categories are evaluated in the order supplied by `pvalcat`. Bounds |
|
| 39 |
#' are lower-inclusive and upper-exclusive, except that the upper bound of |
|
| 40 |
#' the final category is inclusive. P-values that do not fall in a category, |
|
| 41 |
#' including missing values, are returned as `NA_character_`. |
|
| 42 |
#' |
|
| 43 |
#' @param p (`numeric`)\cr A vector of p-values to categorize. Missing values are |
|
| 44 |
#' permitted. |
|
| 45 |
#' @param pvalcat (`named list`)\cr A non-empty named list of p-value category |
|
| 46 |
#' bounds. See [h_normalize_pvalcat()] for the required structure. |
|
| 47 |
#' |
|
| 48 |
#' @return A character vector of category labels, with one element per value in |
|
| 49 |
#' `p`. |
|
| 50 |
#' |
|
| 51 |
#' @examples |
|
| 52 |
#' pvalcat <- list( |
|
| 53 |
#' "<0.001" = c(0, 0.001), |
|
| 54 |
#' "0.001 to <0.05" = c(0.001, 0.05), |
|
| 55 |
#' ">=0.05" = c(0.05, 1) |
|
| 56 |
#' ) |
|
| 57 |
#' |
|
| 58 |
#' categorize_pval(c(0, 0.001, 0.049, 0.05, 1, NA), pvalcat) |
|
| 59 |
#' @export |
|
| 60 |
categorize_pval <- function(p, pvalcat) {
|
|
| 61 | 5x |
checkmate::assert_numeric(p, any.missing = TRUE) |
| 62 | 4x |
checkmate::assert_list(pvalcat, min.len = 1, names = "unique") |
| 63 | ||
| 64 | 4x |
info <- h_normalize_pvalcat(pvalcat) |
| 65 | 4x |
bounds <- info$bounds |
| 66 | 4x |
cats <- info$cats |
| 67 | 4x |
last_row <- nrow(bounds) |
| 68 | ||
| 69 | 4x |
vapply( |
| 70 | 4x |
p, |
| 71 | 4x |
function(x) {
|
| 72 | 13x |
if (is.na(x)) {
|
| 73 | 1x |
return(NA_character_) |
| 74 |
} |
|
| 75 | ||
| 76 | 12x |
idx <- which( |
| 77 | 12x |
bounds[, 1] <= x & |
| 78 | 12x |
(x < bounds[, 2] | (seq_len(last_row) == last_row & x <= bounds[, 2])) |
| 79 |
) |
|
| 80 | 12x |
if (length(idx) == 0) {
|
| 81 | 3x |
return(NA_character_) |
| 82 |
} |
|
| 83 | 9x |
cats[idx[1]] |
| 84 |
}, |
|
| 85 | 4x |
FUN.VALUE = character(1) |
| 86 |
) |
|
| 87 |
} |
| 1 |
#' `ANCOVA` Analysis |
|
| 2 |
#' |
|
| 3 |
#' Performs the `ANCOVA` analysis, separately for each visit. |
|
| 4 |
#' |
|
| 5 |
#' @param vars (named `list` of `string` or `character`)\cr specifying the variables in the `ANCOVA` analysis. |
|
| 6 |
#' The following elements need to be included as character vectors and match corresponding columns |
|
| 7 |
#' in `data`: |
|
| 8 |
#' |
|
| 9 |
#' - `response`: the response variable. |
|
| 10 |
#' - `covariates`: the additional covariate terms (might also include interactions). |
|
| 11 |
#' - `id`: the subject ID variable (not really needed for the computations but for internal logistics). |
|
| 12 |
#' - `arm`: the treatment group variable (factor). |
|
| 13 |
#' - `visit`: the visit variable (factor). |
|
| 14 |
#' |
|
| 15 |
#' Note that the `arm` variable is by default included in the model, thus should not be part of `covariates`. |
|
| 16 |
#' @param data (`data.frame`)\cr with all the variables specified in |
|
| 17 |
#' `vars`. Records with missing values in any independent variables |
|
| 18 |
#' will be excluded. |
|
| 19 |
#' @param conf_level (`proportion`)\cr confidence level of the interval. |
|
| 20 |
#' @param weights_emmeans (`string`)\cr argument from [emmeans::emmeans()], `'counterfactual'` by default. |
|
| 21 |
#' |
|
| 22 |
#' @return A `tern_model` object which is a list with model results: |
|
| 23 |
#' |
|
| 24 |
#' - `fit`: A list with a fitted [stats::lm()] result for each visit. |
|
| 25 |
#' - `mse`: Mean squared error, i.e. variance estimate, for each visit. |
|
| 26 |
#' - `df`: Degrees of freedom for the variance estimate for each visit. |
|
| 27 |
#' - `lsmeans`: This is a list with data frames `estimates` and `contrasts`. |
|
| 28 |
#' The attribute `weights` savse the settings used (`weights_emmeans`). |
|
| 29 |
#' - `vars`: The variable list. |
|
| 30 |
#' - `labels`: Corresponding list with variable labels extracted from `data`. |
|
| 31 |
#' - `ref_level`: The reference level for the arm variable, which is always the first level. |
|
| 32 |
#' - `treatment_levels`: The treatment levels for the arm variable. |
|
| 33 |
#' - `conf_level`: The confidence level which was used to construct the `lsmeans` confidence intervals. |
|
| 34 |
#' |
|
| 35 |
#' @export |
|
| 36 |
#' @examples |
|
| 37 |
#' library(mmrm) |
|
| 38 |
#' |
|
| 39 |
#' fit <- fit_ancova( |
|
| 40 |
#' vars = list( |
|
| 41 |
#' response = "FEV1", |
|
| 42 |
#' covariates = c("RACE", "SEX"),
|
|
| 43 |
#' arm = "ARMCD", |
|
| 44 |
#' id = "USUBJID", |
|
| 45 |
#' visit = "AVISIT" |
|
| 46 |
#' ), |
|
| 47 |
#' data = fev_data, |
|
| 48 |
#' conf_level = 0.9, |
|
| 49 |
#' weights_emmeans = "equal" |
|
| 50 |
#' ) |
|
| 51 |
#' |
|
| 52 |
fit_ancova <- function( |
|
| 53 |
vars = list(response = "AVAL", covariates = c(), arm = "ARM", visit = "AVISIT", id = "USUBJID"), |
|
| 54 |
data, |
|
| 55 |
conf_level = 0.95, |
|
| 56 |
weights_emmeans = "proportional") {
|
|
| 57 | 5x |
labels <- h_labels(vars, data) |
| 58 | ||
| 59 | 5x |
arm_levels <- levels(data[[vars$arm]]) |
| 60 | 5x |
ref_level <- arm_levels[1] |
| 61 | 5x |
trt_levels <- arm_levels[-1] |
| 62 | 5x |
visit_levels <- levels(data[[vars$visit]]) |
| 63 | 5x |
grid <- list( |
| 64 | 5x |
factor(arm_levels, levels = arm_levels), |
| 65 | 5x |
factor(rep(visit_levels, each = length(arm_levels)), levels = visit_levels) |
| 66 |
) |> |
|
| 67 | 5x |
stats::setNames(c(vars$arm, vars$visit)) |> |
| 68 | 5x |
as.data.frame() |
| 69 | 5x |
checkmate::assert_disjunct(vars$arm, vars$covariates) |
| 70 | 5x |
covariates_part <- paste(vars$covariates, collapse = " + ") |
| 71 | 5x |
formula <- if (covariates_part != "") {
|
| 72 | 5x |
stats::as.formula(paste0(vars$response, " ~ ", covariates_part, " + ", vars$arm)) |
| 73 |
} else {
|
|
| 74 | ! |
stats::as.formula(paste0(vars$response, " ~ ", vars$arm)) |
| 75 |
} |
|
| 76 | 5x |
results_by_visit <- Map( |
| 77 | 5x |
data = split(data, data[[vars$visit]]), |
| 78 | 5x |
grid = split(grid, grid[[vars$visit]]), |
| 79 | 5x |
f = function(data, grid) {
|
| 80 | 20x |
fit <- stats::lm(formula = formula, data = data) |
| 81 | 20x |
summary_fit <- summary(fit) |
| 82 | 20x |
emmeans_res <- h_get_emmeans_res(fit, vars = vars["arm"], weights = weights_emmeans) |
| 83 | 20x |
estimates <- h_get_single_visit_estimates(emmeans_res, conf_level) |
| 84 | 20x |
estimates[[vars$visit]] <- grid[[vars$visit]] |
| 85 | 20x |
estimates <- estimates[, c(vars$arm, vars$visit, |
| 86 | 20x |
setdiff(names(estimates), c(vars$arm, vars$visit))), drop = FALSE] |
| 87 | 20x |
contrasts <- h_get_spec_visit_estimates( |
| 88 | 20x |
emmeans_res, |
| 89 | 20x |
specs = list( |
| 90 | 20x |
coefs = "trt.vs.ctrl", |
| 91 | 20x |
grid = grid[-1, ] # Without the reference level row. |
| 92 |
), |
|
| 93 | 20x |
conf_level = conf_level, |
| 94 | 20x |
tests = TRUE, |
| 95 | 20x |
ref = ref_level |
| 96 |
) |
|
| 97 | 20x |
relative_reduc_df <- h_get_relative_reduc_df(estimates, vars) |
| 98 | 20x |
contrasts <- merge(contrasts, relative_reduc_df, by = c(vars$arm, vars$visit), sort = FALSE) |
| 99 | 20x |
list(fit = fit, estimates = estimates, contrasts = contrasts, mse = summary_fit$sigma^2, df = summary_fit$df[2]) |
| 100 |
} |
|
| 101 |
) |
|
| 102 | 5x |
lsmeans <- structure( |
| 103 | 5x |
list( |
| 104 | 5x |
estimates = do.call(rbind, c(lapply(results_by_visit, "[[", "estimates"), make.row.names = FALSE)), |
| 105 | 5x |
contrasts = do.call(rbind, c(lapply(results_by_visit, "[[", "contrasts"), make.row.names = FALSE)) |
| 106 |
), |
|
| 107 | 5x |
weights = weights_emmeans |
| 108 |
) |
|
| 109 | 5x |
results <- list( |
| 110 | 5x |
fit = lapply(results_by_visit, "[[", "fit"), |
| 111 | 5x |
mse = do.call(c, lapply(results_by_visit, "[[", "mse")), |
| 112 | 5x |
df = do.call(c, lapply(results_by_visit, "[[", "df")), |
| 113 | 5x |
lsmeans = lsmeans, |
| 114 | 5x |
vars = vars, |
| 115 | 5x |
labels = labels, |
| 116 | 5x |
ref_level = ref_level, |
| 117 | 5x |
treatment_levels = trt_levels, |
| 118 | 5x |
conf_level = conf_level |
| 119 |
) |
|
| 120 | 5x |
class(results) <- "tern_model" |
| 121 | 5x |
results |
| 122 |
} |
| 1 |
#' Cochran-Mantel-Haenszel Row Mean Scores test |
|
| 2 |
#' |
|
| 3 |
#' See <https://psiaims.github.io/CAMIS/Comp/r-sas_cmh.html> for a general comparison |
|
| 4 |
#' overview between R and SAS. |
|
| 5 |
#' |
|
| 6 |
#' @inheritParams proposal_argument_convention |
|
| 7 |
#' |
|
| 8 |
#' @name cmhrms |
|
| 9 |
#' @return |
|
| 10 |
#' * `s_cmhrms_j` a single element list containing the p-value from row mean score test. |
|
| 11 |
#' * `a_cmhrms_j` a `VerticalRowsSection` object (single row). |
|
| 12 |
#' @order 1 |
|
| 13 |
NULL |
|
| 14 | ||
| 15 | ||
| 16 |
#' @describeIn cmhrms Statistics function for the calculation of the p-value |
|
| 17 |
#' based upon the row mean scores test. |
|
| 18 |
#' |
|
| 19 |
#' @export |
|
| 20 |
#' @importFrom tern get_covariates |
|
| 21 |
#' @order 3 |
|
| 22 |
#' @inheritParams proposal_argument_convention |
|
| 23 |
#' @param variables (`list`)\cr list with arm and strata variable names. |
|
| 24 |
#' @param collapse_combo (`logical`)\cr If TRUE, multiple arm levels from df |
|
| 25 |
#' will be combined into 1 level. |
|
| 26 |
#' |
|
| 27 |
s_cmhrms_j <- function(df, .var, .ref_group, .in_ref_col, ..., .df_row, variables, collapse_combo = FALSE) {
|
|
| 28 | 8x |
checkmate::assert_list(variables) |
| 29 | 8x |
checkmate::assert_names(names(variables), must.include = "arm") |
| 30 | ||
| 31 | 8x |
colvar <- variables$arm |
| 32 | 8x |
if (collapse_combo && length(unique(df[[colvar]])) >= 2) {
|
| 33 | ! |
df[[colvar]] <- "Combined" |
| 34 |
} |
|
| 35 | 8x |
pwdf <- rbind(df, .ref_group) |
| 36 | 8x |
inputdf <- if (.in_ref_col) {
|
| 37 |
# overall p-value -- however not needed on output? |
|
| 38 | 3x |
.df_row |
| 39 |
} else {
|
|
| 40 |
# pairwise p-value |
|
| 41 | 5x |
pwdf |
| 42 |
} |
|
| 43 | ||
| 44 | 8x |
inputdf[[colvar]] <- as.character(inputdf[[colvar]]) |
| 45 | 8x |
inputdf[[.var]] <- droplevels(factor(inputdf[[.var]])) |
| 46 | ||
| 47 | 8x |
strata <- variables$strata |
| 48 | ||
| 49 | 8x |
if (!is.null(strata) && length(strata) > 0) {
|
| 50 | 5x |
var_list <- get_covariates(strata) |
| 51 | 5x |
assert_df_with_variables(.df_row, var_list) |
| 52 |
} |
|
| 53 | 8x |
strata_part <- paste(strata, collapse = " + ") |
| 54 | 8x |
formula <- if (strata_part != "") {
|
| 55 | 5x |
stats::as.formula(paste0("Freq ~ ", colvar, " + ", .var, " | ", strata_part))
|
| 56 |
} else {
|
|
| 57 | 3x |
stats::as.formula(paste0("Freq ~ ", colvar, " + ", .var))
|
| 58 |
} |
|
| 59 | ||
| 60 | 8x |
pval <- if (length(stats::na.omit(df[[.var]])) == 0 || length(unique(inputdf[[colvar]])) < 2) {
|
| 61 | 2x |
numeric(0) |
| 62 |
} else {
|
|
| 63 | 6x |
x_stats_cmh <- |
| 64 | 6x |
tryCatch( |
| 65 | 6x |
vcdExtra::CMHtest( |
| 66 | 6x |
formula, |
| 67 | 6x |
data = inputdf, |
| 68 | 6x |
overall = TRUE, details = TRUE |
| 69 |
), |
|
| 70 | 6x |
error = function(cond) {
|
| 71 | ! |
message(conditionMessage(cond)) |
| 72 |
# Choose a return value in case of error |
|
| 73 | ! |
NULL |
| 74 |
} |
|
| 75 |
) |
|
| 76 | ||
| 77 | 6x |
x_stats_cmh <- if ((strata_part != "")) {
|
| 78 | 5x |
x_stats_cmh$ALL$table |
| 79 |
} else {
|
|
| 80 | 1x |
x_stats_cmh$table |
| 81 |
} |
|
| 82 | ||
| 83 | 6x |
rmeans <- x_stats_cmh["rmeans", , drop = TRUE] |
| 84 | 6x |
rmeans_pval <- rmeans[["Prob"]] |
| 85 | ||
| 86 | 6x |
if (is.null(x_stats_cmh)) {
|
| 87 | ! |
rmeans_pval <- NA |
| 88 |
} |
|
| 89 | 6x |
if (.in_ref_col) {
|
| 90 | 2x |
numeric(0) |
| 91 |
} else {
|
|
| 92 | 4x |
rmeans_pval |
| 93 |
} |
|
| 94 |
} |
|
| 95 | 8x |
list(pval = with_label(pval, "p-value")) |
| 96 |
} |
|
| 97 | ||
| 98 |
#' @describeIn cmhrms Formatted analysis function which is used as `afun`. |
|
| 99 |
#' @export |
|
| 100 |
#' @order 2 |
|
| 101 |
a_cmhrms_j <- function(df, .var, |
|
| 102 |
ref_path, |
|
| 103 |
.spl_context, |
|
| 104 |
.ref_group, |
|
| 105 |
.in_ref_col, |
|
| 106 |
.df_row, |
|
| 107 |
..., |
|
| 108 |
variables, |
|
| 109 |
collapse_combo = TRUE, |
|
| 110 |
.stats = NULL, |
|
| 111 |
.formats = NULL, |
|
| 112 |
.indent_mods = NULL, |
|
| 113 |
.labels = NULL) {
|
|
| 114 | 4x |
dots_extra_args <- list(...) |
| 115 | 4x |
ref <- get_ref_info(ref_path, .spl_context) |
| 116 | 4x |
.in_ref_col <- ref$in_ref_col |
| 117 | ||
| 118 | 4x |
x_stats <- |
| 119 | 4x |
.apply_stat_functions( |
| 120 | 4x |
default_stat_fnc = s_cmhrms_j, |
| 121 | 4x |
custom_stat_fnc_list = NULL, |
| 122 | 4x |
args_list = c( |
| 123 | 4x |
df = list(df), |
| 124 | 4x |
.var = .var, |
| 125 | 4x |
.ref_group = list(ref$ref_group), |
| 126 | 4x |
.in_ref_col = .in_ref_col, |
| 127 | 4x |
.df_row = list(.df_row), |
| 128 | 4x |
variables = list(variables), |
| 129 | 4x |
collapse_combo = collapse_combo, |
| 130 | 4x |
dots_extra_args |
| 131 |
) |
|
| 132 |
) |
|
| 133 | ||
| 134 |
# Format according to specifications |
|
| 135 | 4x |
format_stats( |
| 136 | 4x |
x_stats, |
| 137 | 4x |
method_groups = "cmhrms", |
| 138 | 4x |
stats_in = .stats, |
| 139 | 4x |
formats_in = .formats, |
| 140 | 4x |
labels_in = .labels, |
| 141 | 4x |
indents_in = .indent_mods |
| 142 |
) |
|
| 143 |
} |
|
| 144 | ||
| 145 |
#' @describeIn cmhrms Wrapper for the `afun` which can exclude |
|
| 146 |
#' row split levels from producing the analysis. These have to be specified in the |
|
| 147 |
#' `exclude_levels` argument, see `?do_exclude_split` for details. |
|
| 148 |
#' @export |
|
| 149 |
#' @order 3 |
|
| 150 |
a_cmhrms_j_with_exclude <- function( |
|
| 151 |
df, |
|
| 152 |
exclude_levels, |
|
| 153 |
.var, |
|
| 154 |
.spl_context, |
|
| 155 |
.ref_group, |
|
| 156 |
.in_ref_col, |
|
| 157 |
.df_row, |
|
| 158 |
..., |
|
| 159 |
.stats = NULL, |
|
| 160 |
.formats = NULL, |
|
| 161 |
.indent_mods = NULL, |
|
| 162 |
.labels = NULL |
|
| 163 |
) {
|
|
| 164 | 4x |
if (do_exclude_split(exclude_levels, .spl_context)) {
|
| 165 | 2x |
NULL |
| 166 |
} else {
|
|
| 167 | 2x |
a_cmhrms_j( |
| 168 | 2x |
df = df, |
| 169 | 2x |
.var = .var, |
| 170 | 2x |
.spl_context = .spl_context, |
| 171 | 2x |
.ref_group = .ref_group, |
| 172 | 2x |
.in_ref_col = .in_ref_col, |
| 173 | 2x |
.df_row = .df_row, |
| 174 |
..., |
|
| 175 | 2x |
.stats = .stats, |
| 176 | 2x |
.formats = .formats, |
| 177 | 2x |
.indent_mods = .indent_mods, |
| 178 | 2x |
.labels = .labels |
| 179 |
) |
|
| 180 |
} |
|
| 181 |
} |
| 1 |
#' @title Obtain reference group information from split context. |
|
| 2 |
#' |
|
| 3 |
#' @description `r lifecycle::badge("stable")`
|
|
| 4 |
#' |
|
| 5 |
#' `get_ref_info()` identifies a reference group defined by a column-split |
|
| 6 |
#' path and returns both the reference-group data and an indicator of whether |
|
| 7 |
#' the current column is the reference column. It is intended for use inside |
|
| 8 |
#' custom `rtables` analysis functions. |
|
| 9 |
#' |
|
| 10 |
#' The reference group is specified using `ref_path`, which consists of |
|
| 11 |
#' alternating column-split variable names and its corresponding levels. |
|
| 12 |
#' For example, `c("SEX", "F", "ARM", "Placebo")` specifies the column-split
|
|
| 13 |
#' path where `SEX` is `"F"` and `ARM` is `"Placebo"`. |
|
| 14 |
#' |
|
| 15 |
#' @param ref_path (`character`) \cr |
|
| 16 |
#' Reference group specification as an `rtables` `colpath`; see Details. |
|
| 17 |
#' @param .spl_context (`data.frame`) \cr |
|
| 18 |
#' Ancestor split-state information passed by `rtables`. |
|
| 19 |
#' @param .var (`character(1)`) \cr |
|
| 20 |
#' The variable being analyzed; see [rtables::additional_fun_params]. |
|
| 21 |
#' If supplied, the corresponding column is extracted from the reference-group |
|
| 22 |
#' data. If `NULL`, the complete reference-group data frame is returned. |
|
| 23 |
#' |
|
| 24 |
#' @return |
|
| 25 |
#' A list with the following elements: |
|
| 26 |
#' \itemize{
|
|
| 27 |
#' \item `in_ref_col` (`logical(1)` or `NULL`) indicates whether the |
|
| 28 |
#' current column matches the reference path. |
|
| 29 |
#' This corresponds to `.in_ref_col` in [rtables::additional_fun_params]. |
|
| 30 |
#' \item `ref_group` (`data.frame`, vector, or `NULL`) contains the |
|
| 31 |
#' observations belonging to the reference group. If `.var` is `NULL`, |
|
| 32 |
#' the complete data frame is returned; otherwise, the column specified |
|
| 33 |
#' by `.var` is returned. |
|
| 34 |
#' This corresponds to `.ref_group` in [rtables::additional_fun_params]. |
|
| 35 |
#' } |
|
| 36 |
#' |
|
| 37 |
#' If the reference path is not present in the current column-split |
|
| 38 |
#' hierarchy, both elements are `NULL`. |
|
| 39 |
#' |
|
| 40 |
#' @export |
|
| 41 |
#' |
|
| 42 |
#' @examples |
|
| 43 |
#' dm <- DM |
|
| 44 |
#' dm$colspan_trt <- factor( |
|
| 45 |
#' ifelse(dm$ARM == "B: Placebo", " ", "Active Study Agent"), |
|
| 46 |
#' levels = c("Active Study Agent", " ")
|
|
| 47 |
#' ) |
|
| 48 |
#' colspan_trt_map <- create_colspan_map( |
|
| 49 |
#' dm, |
|
| 50 |
#' non_active_grp = "B: Placebo", |
|
| 51 |
#' non_active_grp_span_lbl = " ", |
|
| 52 |
#' active_grp_span_lbl = "Active Study Agent", |
|
| 53 |
#' colspan_var = "colspan_trt", |
|
| 54 |
#' trt_var = "ARM" |
|
| 55 |
#' ) |
|
| 56 |
#' |
|
| 57 |
#' # A standard analysis function which uses a reference group. |
|
| 58 |
#' standard_afun <- function(x, .ref_group, .in_ref_col) {
|
|
| 59 |
#' diff_means <- if (isFALSE(.in_ref_col)) {
|
|
| 60 |
#' mean(x) - mean(.ref_group) |
|
| 61 |
#' } else {
|
|
| 62 |
#' NULL |
|
| 63 |
#' } |
|
| 64 |
#' in_rows( |
|
| 65 |
#' m = rcell(mean(x), label = "Mean"), |
|
| 66 |
#' dm = rcell(diff_means, label = "Difference in Means vs Placebo"), |
|
| 67 |
#' .formats = "xx.xx" |
|
| 68 |
#' ) |
|
| 69 |
#' } |
|
| 70 |
#' |
|
| 71 |
#' # The custom analysis function which can work with a global reference group. |
|
| 72 |
#' result_afun <- function(x, ref_path, .spl_context, .var) {
|
|
| 73 |
#' ref <- get_ref_info(ref_path, .spl_context, .var) |
|
| 74 |
#' standard_afun(x, .ref_group = ref$ref_group, .in_ref_col = ref$in_ref_col) |
|
| 75 |
#' } |
|
| 76 |
#' |
|
| 77 |
#' ref_path <- c("colspan_trt", " ", "ARM", "B: Placebo")
|
|
| 78 |
#' |
|
| 79 |
#' lyt <- basic_table() |> |
|
| 80 |
#' split_cols_by("colspan_trt", split_fun = trim_levels_to_map(colspan_trt_map)) |>
|
|
| 81 |
#' split_cols_by("ARM") |>
|
|
| 82 |
#' add_overall_col("Total") |>
|
|
| 83 |
#' analyze("AGE", afun = result_afun, extra_args = list(ref_path = ref_path))
|
|
| 84 |
#' |
|
| 85 |
#' build_table(lyt, dm) |
|
| 86 |
get_ref_info <- function(ref_path, .spl_context, .var = NULL) {
|
|
| 87 | 382x |
if (is.null(ref_path)) {
|
| 88 | 1x |
return(NULL) |
| 89 |
} |
|
| 90 | ||
| 91 | 381x |
checkmate::assert_character(ref_path, min.len = 2L, names = "unnamed") |
| 92 | 381x |
checkmate::assert_true(length(ref_path) %% 2L == 0L) |
| 93 | 381x |
checkmate::assert_data_frame(.spl_context) |
| 94 | 381x |
checkmate::assert_subset("full_parent_df", colnames(.spl_context))
|
| 95 | 381x |
checkmate::assert_string(.var, min.chars = 1L, null.ok = TRUE) |
| 96 | ||
| 97 |
# Compare column split names while ignoring split values. |
|
| 98 | 381x |
ref_path_val_pos <- seq(2L, length(ref_path), by = 2L) |
| 99 | 381x |
ref_path_any_val <- replace(ref_path, ref_path_val_pos, "*") |
| 100 | 381x |
if (!in_column(ref_path_any_val, .spl_context)) {
|
| 101 | 111x |
return(list(in_ref_col = NULL, ref_group = NULL)) |
| 102 |
} |
|
| 103 | ||
| 104 | 270x |
leaf_sc <- .spl_context[nrow(.spl_context), ] |
| 105 | 270x |
full_df <- leaf_sc$full_parent_df[[1L]] |
| 106 | 270x |
ref_path_vals <- paste(ref_path[ref_path_val_pos], collapse = ".") |
| 107 | 270x |
ref_group_rows <- leaf_sc[[ref_path_vals]][[1L]] |
| 108 | 270x |
ref_group <- full_df[ref_group_rows, ] |
| 109 | 270x |
if (!is.null(.var)) {
|
| 110 | 9x |
ref_group <- ref_group[[.var]] |
| 111 |
} |
|
| 112 | ||
| 113 | 270x |
list( |
| 114 | 270x |
in_ref_col = in_column(ref_path, .spl_context), |
| 115 | 270x |
ref_group = ref_group |
| 116 |
) |
|
| 117 |
} |
| 1 |
#' @name response_by_var |
|
| 2 |
#' @title Count denom fraction statistic |
|
| 3 |
#' |
|
| 4 |
#' @description Derives the count_denom_fraction statistic (i.e., 'xx /xx (xx.x percent)' )\cr |
|
| 5 |
#' Summarizes the number of unique subjects with a response = 'Y' for a given variable |
|
| 6 |
#' (e.g. TRTEMFL) within each category of another variable (e.g., SEX). |
|
| 7 |
#' Note that the denominator is derived using input df, |
|
| 8 |
#' in order to have these aligned with alt_source_df, it is expected that df includes all subjects. |
|
| 9 |
#' |
|
| 10 |
#' @details This is an analysis function for use within `analyze`. Arguments |
|
| 11 |
#' `df`, `.var` will be populated automatically by rtables during |
|
| 12 |
#' the tabulation process. |
|
| 13 |
#' |
|
| 14 |
#' @param df (`data.frame`)\cr Name of dataframe being analyzed. |
|
| 15 |
#' @param labelstr (`character vector`)\cr Custom label for the variable being analyzed. |
|
| 16 |
#' @param .var (`character`)\cr Name of the variable being analyzed. Records with non-missing |
|
| 17 |
#' values will be counted in the denominator. |
|
| 18 |
#' @param .N_col (`numeric`)\cr The total for the current column. |
|
| 19 |
#' @param resp_var (`character`)\cr Name of variable, for which, records with a value of 'Y' |
|
| 20 |
#' will be counted in the numerator. |
|
| 21 |
#' @param id (`character`)\cr Name of column in df which will have patient identifiers |
|
| 22 |
#' @param .format (`character`)\cr Format for the count/denominator/fraction output. |
|
| 23 |
#' @param ... Additional arguments passed to the function. |
|
| 24 |
#' @examples |
|
| 25 |
#' library(dplyr) |
|
| 26 |
#' |
|
| 27 |
#' ADAE <- data.frame( |
|
| 28 |
#' USUBJID = c( |
|
| 29 |
#' "XXXXX01", "XXXXX02", "XXXXX03", "XXXXX04", "XXXXX05", |
|
| 30 |
#' "XXXXX06", "XXXXX07", "XXXXX08", "XXXXX09", "XXXXX10" |
|
| 31 |
#' ), |
|
| 32 |
#' SEX_DECODE = c( |
|
| 33 |
#' "Female", "Female", "Male", "Female", "Male", |
|
| 34 |
#' "Female", "Male", "Female", "Male", "Female" |
|
| 35 |
#' ), |
|
| 36 |
#' TRT01A = c( |
|
| 37 |
#' "ARMA", "ARMB", "ARMA", "ARMB", "ARMB", |
|
| 38 |
#' "Placebo", "Placebo", "Placebo", "ARMA", "ARMB" |
|
| 39 |
#' ), |
|
| 40 |
#' TRTEMFL = c("Y", "Y", "N", "Y", "Y", "Y", "Y", "N", "Y", "Y")
|
|
| 41 |
#' ) |
|
| 42 |
#' |
|
| 43 |
#' ADAE <- ADAE |> |
|
| 44 |
#' mutate( |
|
| 45 |
#' TRT01A = as.factor(TRT01A), |
|
| 46 |
#' SEX_DECODE = as.factor(SEX_DECODE) |
|
| 47 |
#' ) |
|
| 48 |
#' |
|
| 49 |
#' lyt <- basic_table() |> |
|
| 50 |
#' split_cols_by("TRT01A") |>
|
|
| 51 |
#' analyze( |
|
| 52 |
#' vars = "SEX_DECODE", |
|
| 53 |
#' var_labels = "Sex, n/Ns (%)", |
|
| 54 |
#' show_labels = "visible", |
|
| 55 |
#' afun = response_by_var, |
|
| 56 |
#' extra_args = list(resp_var = "TRTEMFL"), |
|
| 57 |
#' nested = FALSE |
|
| 58 |
#' ) |
|
| 59 |
#' |
|
| 60 |
#' result <- build_table(lyt, ADAE) |
|
| 61 |
#' |
|
| 62 |
#' result |
|
| 63 |
#' @return a `RowsVerticalSection` for use by the internal tabulation machinery of `rtables` |
|
| 64 |
#' @export |
|
| 65 |
response_by_var <- function( |
|
| 66 |
df, |
|
| 67 |
labelstr = NULL, |
|
| 68 |
.var, |
|
| 69 |
.N_col, |
|
| 70 |
resp_var = NULL, |
|
| 71 |
id = "USUBJID", |
|
| 72 |
.format = jjcsformat_count_denom_fraction, |
|
| 73 |
... |
|
| 74 |
) {
|
|
| 75 |
# Derive statistics: xx / xx (xx.x%) |
|
| 76 | ||
| 77 | 15x |
if (is.null(resp_var)) {
|
| 78 | ! |
stop("afun response_by_var: resp_var cannot be NULL.")
|
| 79 |
} |
|
| 80 | ||
| 81 | 15x |
resp_var_values <- unique(df[[resp_var]][!is.na(df[[resp_var]])]) |
| 82 | 15x |
if (is.character(df[[resp_var]]) && any(is.na(df[[resp_var]])) && all(resp_var_values == "Y")) {
|
| 83 | ! |
stop(paste( |
| 84 | ! |
"afun response_by_var: not clear if missing resp_var should be considered", |
| 85 | ! |
"non-response. Please make it a factor with appropriate Y(/N) levels." |
| 86 |
)) |
|
| 87 |
} |
|
| 88 | ||
| 89 | 15x |
if (length(setdiff(resp_var_values, c("Y", "N"))) > 0) {
|
| 90 | ! |
stop("afun response_by_var: resp_var must contain only Y/N values.")
|
| 91 |
} |
|
| 92 | ||
| 93 | 15x |
df <- df[!is.na(df[[.var]]), ] |
| 94 | ||
| 95 |
if ( # nolint start |
|
| 96 | 15x |
(is.factor(df[[resp_var]]) && |
| 97 | 15x |
(identical(levels(df[[resp_var]]), c("Y", "N")) || identical(levels(df[[resp_var]]), c("N", "Y")))) ||
|
| 98 | 15x |
is.character(df[[resp_var]]) |
| 99 |
) { # nolint end
|
|
| 100 |
# missing values in resp_var should be excluded, not considered as not met response subject will then not |
|
| 101 |
# contribute to denominator |
|
| 102 | 9x |
df <- df[!is.na(df[[resp_var]]), ] |
| 103 |
} |
|
| 104 | ||
| 105 |
## in other cases, missing values in resp_var will be considered not met response and subject will contribute to |
|
| 106 |
## denominator |
|
| 107 | 15x |
varvec <- df[[.var]] |
| 108 | ||
| 109 |
# For group summaries, varvec will be a singular value, equal to the current split, whereas for afun summaries, |
|
| 110 |
# varvec will consist of all possible values of var. |
|
| 111 | 15x |
if (!is.null(labelstr)) {
|
| 112 | ! |
levs <- labelstr |
| 113 |
} else {
|
|
| 114 | 15x |
levs <- if (is.factor(varvec)) levels(varvec) else unique(varvec) |
| 115 |
} |
|
| 116 | ||
| 117 | 15x |
fn <- function(levii) {
|
| 118 | 63x |
dfii <- df[df[[.var]] == levii, ] |
| 119 | ||
| 120 | 63x |
den <- NROW(unique(dfii[, id])) |
| 121 | 63x |
num <- NROW(unique(dfii[dfii[[resp_var]] == "Y" & !is.na(dfii[[resp_var]]), id])) |
| 122 | ||
| 123 | 63x |
rcell(c(num * 1, den * 1, num * 1 / den), format = .format) |
| 124 |
} |
|
| 125 | ||
| 126 | 15x |
cls <- lapply(levs, fn) |
| 127 | 15x |
names(cls) <- levs |
| 128 | ||
| 129 |
# Hand off results to analyze |
|
| 130 | ||
| 131 | 15x |
in_rows(.list = cls) |
| 132 |
} |
| 1 |
#' @name a_summary_j |
|
| 2 |
#' |
|
| 3 |
#' @title Wrappers Around `tern::a_summary()` with Row Split Exclusion |
|
| 4 |
#' |
|
| 5 |
#' @description `r lifecycle::badge("stable")`
|
|
| 6 |
#' |
|
| 7 |
#' These wrappers around [tern::a_summary()] allow selected row split levels to |
|
| 8 |
#' be excluded from producing the analysis. |
|
| 9 |
#' |
|
| 10 |
#' @details |
|
| 11 |
#' [tern::a_summary()] is a formatted analysis function that computes summary |
|
| 12 |
#' statistics for a single variable. In typical use within the [rtables] |
|
| 13 |
#' framework, the analysis variable is first extracted from a data frame and |
|
| 14 |
#' then passed to [tern::a_summary()] as a standalone vector. |
|
| 15 |
#' |
|
| 16 |
#' `a_summary_j_with_exclude()` extends this behavior by: |
|
| 17 |
#' - accepting a data frame (`df`) as input, |
|
| 18 |
#' - checking whether the current row split path should be excluded, |
|
| 19 |
#' - returning `NULL` for excluded row split levels, |
|
| 20 |
#' - extracting the analysis variable `.var` for all other row split levels, |
|
| 21 |
#' - and passing the resulting vector to [tern::a_summary()]. |
|
| 22 |
#' |
|
| 23 |
#' @inheritParams proposal_argument_convention |
|
| 24 |
#' @param ... Additional arguments passed to [tern::a_summary()], excluding |
|
| 25 |
#' the `x` argument, which is constructed internally from `df[[.var]]`. |
|
| 26 |
#' |
|
| 27 |
#' @return |
|
| 28 |
#' Returns the same type of object as [tern::a_summary()], or `NULL` when the |
|
| 29 |
#' current row split path is excluded by `exclude_levels`. |
|
| 30 |
#' |
|
| 31 |
#' @seealso |
|
| 32 |
#' [tern::a_summary()], [do_exclude_split()] |
|
| 33 |
#' |
|
| 34 |
NULL |
|
| 35 | ||
| 36 |
#' @describeIn a_summary_j Analysis function which can exclude row split levels |
|
| 37 |
#' from producing the analysis. These have to be specified in the |
|
| 38 |
#' `exclude_levels` argument, see `?do_exclude_split` for details. |
|
| 39 |
#' |
|
| 40 |
#' @importFrom tern a_summary |
|
| 41 |
#' @export |
|
| 42 |
#' |
|
| 43 |
#' @examples |
|
| 44 |
#' df <- data.frame( |
|
| 45 |
#' USUBJID = 1:8, |
|
| 46 |
#' AVISIT = factor(rep(c("Baseline", "Week 1"), 4)),
|
|
| 47 |
#' ARM = factor(rep(c("A", "B"), each = 4)),
|
|
| 48 |
#' AVAL = c(1.2, 3, -1.5, 0.8, 3.5, 2, -1.1, 0.3) |
|
| 49 |
#' ) |
|
| 50 |
#' |
|
| 51 |
#' lyt <- basic_table() |> |
|
| 52 |
#' split_cols_by("ARM") |>
|
|
| 53 |
#' split_rows_by("AVISIT") |>
|
|
| 54 |
#' analyze( |
|
| 55 |
#' "AVAL", |
|
| 56 |
#' afun = a_summary_j_with_exclude, |
|
| 57 |
#' extra_args = list(exclude_levels = list(AVISIT = "Baseline")) |
|
| 58 |
#' ) |
|
| 59 |
#' |
|
| 60 |
#' build_table(lyt, df) |
|
| 61 |
a_summary_j_with_exclude <- function( |
|
| 62 |
df, |
|
| 63 |
.var, |
|
| 64 |
exclude_levels, |
|
| 65 |
.spl_context, |
|
| 66 |
... |
|
| 67 |
) {
|
|
| 68 | 6x |
checkmate::assert_data_frame(df) |
| 69 | 6x |
checkmate::assert_string(.var) |
| 70 | 6x |
checkmate::assert_names(colnames(df), must.include = .var) |
| 71 | ||
| 72 | 6x |
if (do_exclude_split(exclude_levels, .spl_context)) {
|
| 73 | 3x |
NULL |
| 74 |
} else {
|
|
| 75 | 3x |
tern::a_summary(df[[.var]], ...) |
| 76 |
} |
|
| 77 |
} |
| 1 |
calc_one_visit_j <- function(datvec, statnm, visit, varnm, exclude_visits, |
|
| 2 |
var_names = c("AVAL", "CHG", "BASE")) {
|
|
| 3 | 31x |
checkmate::assert_string(statnm) |
| 4 | 31x |
checkmate::assert_choice(statnm, tern::get_stats(method_groups = "analyze_vars_numeric")) |
| 5 | 30x |
if ((varnm == var_names[2] || varnm == var_names[3]) && (visit %in% exclude_visits)) {
|
| 6 | 2x |
stat <- list(NULL) |
| 7 | 2x |
names(stat) <- statnm |
| 8 |
} else {
|
|
| 9 | 28x |
stats <- tern::s_summary(datvec) |
| 10 | 28x |
stat <- stats[statnm] |
| 11 |
} |
|
| 12 | 30x |
stat |
| 13 |
} |
|
| 14 | ||
| 15 |
#' @name column_stats |
|
| 16 |
#' @title Statistics within the column space |
|
| 17 |
#' @description |
|
| 18 |
#' A function used for obtaining statistics within the columns of your table. |
|
| 19 |
#' Used in change from baseline tables. This takes the visit names as its row labels. |
|
| 20 |
#' @importFrom assertthat is.string |
|
| 21 |
#' @param exclude_visits (`character vector`)\cr Vector of visit(s) for which you do not want the statistics displayed |
|
| 22 |
#' in the baseline mean or change from baseline sections of the table. |
|
| 23 |
#' @param var_names (`character vector`)\cr Vector of variable names to use instead of the default AVAL, CHG, BASE. |
|
| 24 |
#' By default, the function expects these specific variable names in your data, |
|
| 25 |
#' but you can customize them to match your dataset's column names. |
|
| 26 |
#' @param .formats (named `character` or `list`)\cr formats for the statistics. |
|
| 27 |
#' When set to `"default"` `formats_var` argument will be used for format setup. |
|
| 28 |
#' @param formats_var (`string` or `NULL`)\cr `NULL` (the default) or the name of the list column containing named |
|
| 29 |
#' lists of default formats to use. These will not override .formats (other than `"default"`). |
|
| 30 |
#' @param exclude_visits Visits to exclude for the second/third part of the columns ("BASE", "CHG").
|
|
| 31 |
#' @param .spl_context (`data.frame`)\cr gives information about ancestor split states. |
|
| 32 |
#' @param df (`data.frame` or `tibble`)\cr dataset. |
|
| 33 |
#' @param .var (`string`)\cr single variable name that is passed by `rtables` |
|
| 34 |
#' @return An analysis function (for use with [rtables::analyze]) implementing |
|
| 35 |
#' the specified statistics.\cr |
|
| 36 |
#' Typically used in a layout that has been setup by [stats_in_cols_setup()] |
|
| 37 |
#' @examples |
|
| 38 |
#' # example code |
|
| 39 |
#' library(dplyr) |
|
| 40 |
#' advs <- ex_advs |> |
|
| 41 |
#' filter(AVISIT %in% toupper(c("Screening", "Baseline", "Week 1 Day 8", "Week 2 Day 15"))) |>
|
|
| 42 |
#' mutate(AVISIT = droplevels(AVISIT)) |> |
|
| 43 |
#' filter(PARAMCD %in% c("DIABP", "SYSBP"))
|
|
| 44 |
#' advs_fmt <- tribble( |
|
| 45 |
#' ~PARAMCD, ~fmt_d, |
|
| 46 |
#' "DIABP", list( |
|
| 47 |
#' mean = "xx.xx", sd = "xx.xxx", se = "xx.xxx", median = "xx.xx", |
|
| 48 |
#' min = "xx.x", max = "xx.x", mean_sd = "xx.xx (xx.xxx)" |
|
| 49 |
#' ), |
|
| 50 |
#' "SYSBP", list( |
|
| 51 |
#' mean = "xx.x", sd = "xx.xx", se = "xx.xx", median = "xx.x", |
|
| 52 |
#' min = "xx.", max = "xx.", mean_sd = "xx.x (xx.xx)" |
|
| 53 |
#' ) |
|
| 54 |
#' ) |
|
| 55 |
#' |
|
| 56 |
#' advs <- left_join(advs, advs_fmt) |
|
| 57 |
#' |
|
| 58 |
#' mysplitfun <- make_split_fun( |
|
| 59 |
#' post = list(stats_in_cols_setup()) |
|
| 60 |
#' ) |
|
| 61 |
#' |
|
| 62 |
#' lyt <- basic_table() |> |
|
| 63 |
#' split_cols_by_multivar( |
|
| 64 |
#' c("AVAL", "BASE", "CHG"),
|
|
| 65 |
#' varlabels = c("Visit Values", " ", "Change from Baseline")
|
|
| 66 |
#' ) |> |
|
| 67 |
#' split_cols_by("STUDYID", split_fun = mysplitfun) |>
|
|
| 68 |
#' split_rows_by("PARAM", split_fun = drop_split_levels) |>
|
|
| 69 |
#' split_rows_by("ARMCD", labels_var = "ARM") |>
|
|
| 70 |
#' analyze("AVISIT", afun = column_stats, extra_args = list(
|
|
| 71 |
#' exclude_visits = toupper(c("Baseline", "Screening")),
|
|
| 72 |
#' .formats = "default", |
|
| 73 |
#' formats_var = "fmt_d" |
|
| 74 |
#' )) |
|
| 75 |
#' result <- build_table(lyt, advs, round_type = "sas") |
|
| 76 |
#' head(result, 20) |
|
| 77 |
#' @export |
|
| 78 |
column_stats <- function(df, .var, .spl_context, |
|
| 79 |
.formats = NULL, |
|
| 80 |
formats_var = NULL, |
|
| 81 |
exclude_visits = c("Baseline (DB)"),
|
|
| 82 |
var_names = c("AVAL", "CHG", "BASE")) {
|
|
| 83 | 11x |
allcolsplvals <- .spl_context[nrow(.spl_context), "cur_col_split_val"][[1]] |
| 84 | 11x |
statnm <- utils::tail(allcolsplvals, 1) |
| 85 | 11x |
varnm <- allcolsplvals[length(allcolsplvals) - 1] |
| 86 | 11x |
datvec <- df[[varnm]] |
| 87 | 11x |
datpervis <- split(datvec, df[[.var]]) ## ,var is AVISIT |
| 88 |
# derivation of statistics - current column - different rows for avisit |
|
| 89 | 11x |
stats_rows <- mapply( |
| 90 | 11x |
calc_one_visit_j, |
| 91 | 11x |
datvec = datpervis, |
| 92 | 11x |
visit = names(datpervis), |
| 93 | 11x |
MoreArgs = list( |
| 94 | 11x |
statnm = statnm, |
| 95 | 11x |
varnm = varnm, |
| 96 | 11x |
exclude_visits = exclude_visits, |
| 97 | 11x |
var_names = var_names |
| 98 |
) |
|
| 99 |
) |
|
| 100 |
# get appropriate format for statistic - |
|
| 101 | 10x |
if (identical(.formats, "default") && assertthat::is.string(formats_var)) {
|
| 102 | ! |
checkmate::assert_names(colnames(df), must.include = formats_var) |
| 103 | ! |
myfmt <- df[[formats_var]][[1]][[statnm]] |
| 104 |
} else {
|
|
| 105 | 10x |
.formats <- junco_get_formats_from_stats( |
| 106 | 10x |
stats = statnm, |
| 107 | 10x |
formats_in = .formats |
| 108 |
) |
|
| 109 | 10x |
myfmt <- .formats[[statnm]] |
| 110 |
} |
|
| 111 |
# inrows with appropriate formatting |
|
| 112 | 10x |
in_rows( |
| 113 | 10x |
.list = stats_rows, |
| 114 | 10x |
.names = names(datpervis), |
| 115 | 10x |
.formats = myfmt, |
| 116 | 10x |
.labels = names(datpervis) |
| 117 |
) |
|
| 118 |
} |
|
| 119 | ||
| 120 |
calc_N <- function(datvec, statnm, trt, varnm) {
|
|
| 121 | 1x |
if (varnm != "AVAL") {
|
| 122 | 1x |
return(NULL) |
| 123 |
} |
|
| 124 | ! |
length(stats::na.omit(datvec)) |
| 125 |
} |
|
| 126 | ||
| 127 |
#' Function factory to be used as Post-processing split function for setup of stats in column structure |
|
| 128 |
#' |
|
| 129 |
#' @description |
|
| 130 |
#' This helper is designed to be used in the `post` argument of |
|
| 131 |
#' [rtables::make_split_fun()] to expand a column facet (e.g. AVAL/BASE/CHG) |
|
| 132 |
#' into the specific statistics to be analyzed for each subfacet. It returns a |
|
| 133 |
#' split result instructing rtables which values/labels/subsets to create. |
|
| 134 |
#' |
|
| 135 |
#' Typical usage: |
|
| 136 |
#' |
|
| 137 |
#' - construct a split function like `mysplitfun <- rtables::make_split_fun(post = list(stats_in_cols_setup()))`\cr |
|
| 138 |
#' Then use `mysplitfun` in your table layout where you split columns by a |
|
| 139 |
#' variable whose levels are one of "AVAL", "BASE", or "CHG" and want to |
|
| 140 |
#' analyze different statistics for each. |
|
| 141 |
#' |
|
| 142 |
#' - column layout setup from |
|
| 143 |
#' [rtables::split_cols_by_multivar()], followed by split_cols_by using `stats_in_cols_setup` in |
|
| 144 |
#' conjunction with an analyze call with `afun` = [column_stats()] |
|
| 145 |
#' |
|
| 146 |
#' See examples for typical usage. |
|
| 147 |
#' |
|
| 148 |
#' @param stats_list `(list)` Named list with statistics to present for each variable. |
|
| 149 |
#' Any statistic from tern::s_summary can be selected. |
|
| 150 |
#' @param statlbls_list `` Named vector with labels to use for the statistic in column header. |
|
| 151 |
#' If NULL, or a statistic name is not included, the default label for the statistic will be used. |
|
| 152 |
#' @return A function that can be used inside [rtables::make_split_result()] |
|
| 153 |
#' statistics for the current column level. |
|
| 154 |
#' @seealso [rtables::make_split_fun()], [rtables::make_split_result()]. |
|
| 155 |
#' @examples |
|
| 156 |
#' # example code |
|
| 157 |
#' library(dplyr) |
|
| 158 |
#' advs <- ex_advs |> |
|
| 159 |
#' filter(AVISIT %in% toupper(c("Screening", "Baseline", "Week 1 Day 8", "Week 2 Day 15"))) |>
|
|
| 160 |
#' mutate(AVISIT = droplevels(AVISIT)) |
|
| 161 |
#' |
|
| 162 |
#' mysplitfun <- make_split_fun( |
|
| 163 |
#' post = list(stats_in_cols_setup()) |
|
| 164 |
#' ) |
|
| 165 | ||
| 166 |
#' lyt <- basic_table() |> |
|
| 167 |
#' split_cols_by_multivar( |
|
| 168 |
#' c("AVAL", "BASE", "CHG"),
|
|
| 169 |
#' varlabels = c("Visit Values", " ", "Change from Baseline")
|
|
| 170 |
#' ) |> |
|
| 171 |
#' split_cols_by("STUDYID", split_fun = mysplitfun) |>
|
|
| 172 |
#' split_rows_by("PARAM") |>
|
|
| 173 |
#' split_rows_by("ARMCD", labels_var = "ARM") |>
|
|
| 174 |
#' analyze("AVISIT",
|
|
| 175 |
#' afun = column_stats, |
|
| 176 |
#' extra_args = list(exclude_visits = toupper(c("Baseline", "Screening")))
|
|
| 177 |
#' ) |
|
| 178 |
#' result <- build_table(lyt, advs, round_type = "sas") |
|
| 179 |
#' head(result, 20) |
|
| 180 |
#' |
|
| 181 |
#' @export |
|
| 182 |
stats_in_cols_setup <- function( |
|
| 183 |
stats_list = list( |
|
| 184 |
"AVAL" = c("n", "mean", "sd", "se", "median", "min", "max"),
|
|
| 185 |
"BASE" = c("mean_sd"),
|
|
| 186 |
"CHG" = c("n", "mean", "sd", "se", "median", "min", "max")
|
|
| 187 |
), |
|
| 188 |
statlbls_list = c(median = "Med", min = "Min", max = "Max", mean_sd = "Base Mean (SD)", n = "N") |
|
| 189 |
) {
|
|
| 190 | ! |
function(ret, spl, fulldf, .spl_context) {
|
| 191 | ! |
all_expr <- expression(TRUE) |
| 192 | ! |
colset <- .spl_context[nrow(.spl_context), "value"][[1]] |
| 193 | ||
| 194 | ! |
if (!(colset %in% names(stats_list))) {
|
| 195 | ! |
stop(paste("something bad happened :(",
|
| 196 | ! |
paste0("incoming Variable to analyze (", colset, ")"),
|
| 197 | ! |
paste0("is not named in stats_list - names: ", paste0(names(stats_list), collapse = ", ")),
|
| 198 | ! |
sep = "\n" |
| 199 |
)) |
|
| 200 |
} |
|
| 201 | ||
| 202 | ! |
stats <- stats_list[[colset]] |
| 203 | ! |
statlbl <- junco_get_labels_from_stats( |
| 204 | ! |
stats = stats, |
| 205 | ! |
labels_in = statlbls_list, |
| 206 | ! |
levels_per_stats = NULL, |
| 207 | ! |
label_attr_from_stats = NULL |
| 208 |
) |
|
| 209 | ||
| 210 | ! |
vals <- stats |
| 211 | ! |
lbls <- statlbl[stats] |
| 212 | ! |
datasplit <- rep(list(fulldf), length(stats)) |
| 213 | ! |
names(datasplit) <- stats |
| 214 | ! |
subset_exprs <- rep(list(all_expr), length(stats)) |
| 215 | ||
| 216 | ! |
ret <- make_split_result( |
| 217 | ! |
values = vals, |
| 218 | ! |
labels = lbls, |
| 219 | ! |
datasplit = datasplit, |
| 220 | ! |
subset_exprs = subset_exprs |
| 221 |
) |
|
| 222 | ||
| 223 | ! |
ret |
| 224 |
} |
|
| 225 |
} |
| 1 |
#' @name a_coxph_hr |
|
| 2 |
#' @title a_coxph_hr |
|
| 3 |
#' @description a_coxph_hr `r lifecycle::badge("deprecated")` Use
|
|
| 4 |
#' tern:::a_coxph_pairwise()` instead, and use `tern::coxph_pairwise()` as |
|
| 5 |
#' the layout-level wrapper. |
|
| 6 |
#' @inheritParams odds_ratio |
|
| 7 |
#' @export |
|
| 8 |
a_coxph_hr <- function( |
|
| 9 |
df, |
|
| 10 |
.var, |
|
| 11 |
ref_path, |
|
| 12 |
.spl_context, |
|
| 13 |
..., |
|
| 14 |
.stats = NULL, |
|
| 15 |
.formats = NULL, |
|
| 16 |
.labels = NULL, |
|
| 17 |
.indent_mods = NULL |
|
| 18 |
) {
|
|
| 19 | 1x |
lifecycle::deprecate_stop( |
| 20 | 1x |
when = "0.1.7", |
| 21 | 1x |
what = "a_coxph_hr()", |
| 22 | 1x |
with = "tern:::a_coxph_pairwise()", |
| 23 | 1x |
details = "Use `tern:::coxph_pairwise()` as the layout-level wrapper." |
| 24 |
) |
|
| 25 |
} |
|
| 26 | ||
| 27 |
#' @name s_coxph_hr |
|
| 28 |
#' @title s_coxph_hr |
|
| 29 |
#' @description s_coxph_hr `r lifecycle::badge("deprecated")` Use
|
|
| 30 |
#' `tern::s_coxph_pairwise` instead, and use `tern::coxph_pairwise()` as |
|
| 31 |
#' the layout-level wrapper. |
|
| 32 |
#' @inheritParams odds_ratio |
|
| 33 |
#' @keywords internal |
|
| 34 |
s_coxph_hr <- function( |
|
| 35 |
df, |
|
| 36 |
.var, |
|
| 37 |
ref_path, |
|
| 38 |
.spl_context, |
|
| 39 |
..., |
|
| 40 |
.stats = NULL, |
|
| 41 |
.formats = NULL, |
|
| 42 |
.labels = NULL, |
|
| 43 |
.indent_mods = NULL |
|
| 44 |
) {
|
|
| 45 | 1x |
lifecycle::deprecate_stop( |
| 46 | 1x |
when = "0.1.7", |
| 47 | 1x |
what = "s_coxph_hr()", |
| 48 | 1x |
with = "tern:::s_coxph_pairwise()", |
| 49 | 1x |
details = "Use `tern:::coxph_pairwise()` as the layout-level wrapper." |
| 50 |
) |
|
| 51 |
} |
|
| 52 | ||
| 53 |
#' @title s_kaplan_meier |
|
| 54 |
#' @description `r lifecycle::badge("deprecated")` Use [tern::s_surv_time()] instead.
|
|
| 55 |
#' @keywords internal |
|
| 56 |
s_kaplan_meier <- function(df, .var, is_event, control = control_surv_time()) {
|
|
| 57 | ! |
lifecycle::deprecate_stop( |
| 58 | ! |
when = "0.1.7", |
| 59 | ! |
what = "s_kaplan_meier()", |
| 60 | ! |
with = "tern::s_surv_time()", |
| 61 | ! |
details = paste0( |
| 62 | ! |
"Note: the label for `range_with_cens_info` has changed from ", |
| 63 | ! |
"'Min, max' (junco) to 'Min - Max (with censoring)' (tern)." |
| 64 |
) |
|
| 65 |
) |
|
| 66 |
} |
|
| 67 | ||
| 68 |
#' @title a_kaplan_meier |
|
| 69 |
#' @description `r lifecycle::badge("deprecated")` Use [tern::a_surv_time()] instead.
|
|
| 70 |
#' @param df (`data.frame`) input data. |
|
| 71 |
#' @param .var (`string`) variable name. |
|
| 72 |
#' @param ... additional arguments (ignored). |
|
| 73 |
#' @param .stats (`character`) statistics to compute (ignored). |
|
| 74 |
#' @param .formats (`list`) formats for statistics (ignored). |
|
| 75 |
#' @param .labels (`list`) labels for statistics (ignored). |
|
| 76 |
#' @param .indent_mods (`list`) indentation modifications (ignored). |
|
| 77 |
#' @export |
|
| 78 |
a_kaplan_meier <- function(df, .var, ..., .stats = NULL, .formats = NULL, .labels = NULL, .indent_mods = NULL) {
|
|
| 79 | 1x |
lifecycle::deprecate_stop( |
| 80 | 1x |
when = "0.1.7", |
| 81 | 1x |
what = "a_kaplan_meier()", |
| 82 | 1x |
with = "tern::a_surv_time()", |
| 83 | 1x |
details = paste0( |
| 84 | 1x |
"Note: the label for `range_with_cens_info` has changed from ", |
| 85 | 1x |
"'Min, max' (junco) to 'Min - Max (with censoring)' (tern)." |
| 86 |
) |
|
| 87 |
) |
|
| 88 |
} |
|
| 89 | ||
| 90 | ||
| 91 |
#' Create a `rbmi` ready cluster |
|
| 92 |
#' |
|
| 93 |
#' @description |
|
| 94 |
#' `r lifecycle::badge("deprecated")` Use `rbmi::make_rbmi_cluster()` instead.
|
|
| 95 |
#' |
|
| 96 |
#' @param cluster_or_cores (`integer` or `cluster object`)\cr |
|
| 97 |
#' Number of parallel processes to use or an existing cluster to make use of |
|
| 98 |
#' @param objects (`list`)\cr A named list of objects to export into the sub-processes |
|
| 99 |
#' @param packages (`character vector`)\cr |
|
| 100 |
#' A character vector of libraries to load in the sub-processes |
|
| 101 |
#' |
|
| 102 |
#' @return See `rbmi::make_rbmi_cluster()`. |
|
| 103 |
#' @export |
|
| 104 |
make_rbmi_cluster <- function( |
|
| 105 |
cluster_or_cores = 1, |
|
| 106 |
objects = NULL, |
|
| 107 |
packages = NULL |
|
| 108 |
) {
|
|
| 109 | 1x |
lifecycle::deprecate_stop( |
| 110 | 1x |
when = "0.1.7", |
| 111 | 1x |
what = "make_rbmi_cluster()", |
| 112 | 1x |
with = "rbmi::make_rbmi_cluster()" |
| 113 |
) |
|
| 114 |
} |
|
| 115 | ||
| 116 |
#' Parallelise Lapply |
|
| 117 |
#' |
|
| 118 |
#' @description |
|
| 119 |
#' `r lifecycle::badge("deprecated")` This internal helper is no longer exported.
|
|
| 120 |
#' Use `rbmi::analyse()` directly. |
|
| 121 |
#' |
|
| 122 |
#' @param cl (`cluster object`)\cr Cluster created by [`parallel::makeCluster()`] or `NULL` |
|
| 123 |
#' @param fun (`functions`)\cr Function to be run |
|
| 124 |
#' @param x (`object`)\cr Object to be looped over |
|
| 125 |
#' @param ... Extra arguments passed to `fun` |
|
| 126 |
#' @return `list` of results of calling `fun` on elements of `x`. |
|
| 127 |
par_lapply <- function(cl, fun, x, ...) {
|
|
| 128 | 1x |
lifecycle::deprecate_stop( |
| 129 | 1x |
when = "0.1.6", |
| 130 | 1x |
what = "par_lapply()", |
| 131 | 1x |
details = "Use `rbmi::analyse()` directly." |
| 132 |
) |
|
| 133 |
} |
|
| 134 | ||
| 135 |
#' Analyse Multiple Imputed Datasets |
|
| 136 |
#' |
|
| 137 |
#' @description |
|
| 138 |
#' This function takes multiple imputed datasets (as generated by |
|
| 139 |
#' the impute() function from the rbmi package) and runs an analysis function on |
|
| 140 |
#' each of them. |
|
| 141 |
#' |
|
| 142 |
#' |
|
| 143 |
#' @details |
|
| 144 |
#' This function works by performing the following steps: |
|
| 145 |
#' |
|
| 146 |
#' 1. Extract a dataset from the `imputations` object. |
|
| 147 |
#' 2. Apply any delta adjustments as specified by the `delta` argument. |
|
| 148 |
#' 3. Run the analysis function `fun` on the dataset. |
|
| 149 |
#' 4. Repeat steps 1-3 across all of the datasets inside the `imputations` |
|
| 150 |
#' object. |
|
| 151 |
#' 5. Collect and return all of the analysis results. |
|
| 152 |
#' |
|
| 153 |
#' The analysis function `fun` must take a `data.frame` as its first |
|
| 154 |
#' argument. All other options to [rbmi_analyse()] are passed onto `fun` |
|
| 155 |
#' |
|
| 156 |
#' @param imputations An `imputations` object as created by the impute() function from the rbmi package. |
|
| 157 |
#' @param fun An analysis function to be applied to each imputed dataset. See details. |
|
| 158 |
#' @param delta A `data.frame` containing the delta transformation to be applied to the imputed |
|
| 159 |
#' datasets prior to running `fun`. See details. |
|
| 160 |
#' @param ... Additional arguments passed onto `fun`. |
|
| 161 |
#' @param cluster_or_cores (`numeric` or `cluster object`)\cr |
|
| 162 |
#' The number of parallel processes to use when running this function. Can also be a |
|
| 163 |
#' cluster object created by [`make_rbmi_cluster()`]. See the parallelisation section below. |
|
| 164 |
#' @param .validate (`logical`)\cr |
|
| 165 |
#' Should `imputations` be checked to ensure it conforms to the required format |
|
| 166 |
#' (default = `TRUE`) ? Can gain a small performance increase if this is set to `FALSE` when |
|
| 167 |
#' analysing a large number of samples. |
|
| 168 |
#' @return An `analysis` object, as defined by `rbmi`, representing the desired |
|
| 169 |
#' analysis applied to each of the imputed datasets in `imputations`. |
|
| 170 |
#' @description |
|
| 171 |
#' `r lifecycle::badge("deprecated")` Use `rbmi::analyse()` instead.
|
|
| 172 |
#' |
|
| 173 |
#' @export |
|
| 174 |
rbmi_analyse <- function( |
|
| 175 |
imputations, |
|
| 176 |
fun = rbmi_ancova, |
|
| 177 |
delta = NULL, |
|
| 178 |
..., |
|
| 179 |
cluster_or_cores = 1, |
|
| 180 |
.validate = TRUE |
|
| 181 |
) {
|
|
| 182 | 1x |
lifecycle::deprecate_stop( |
| 183 | 1x |
when = "0.1.6", |
| 184 | 1x |
what = "rbmi_analyse()", |
| 185 | 1x |
with = "rbmi::analyse()" |
| 186 |
) |
|
| 187 |
} |
| 1 |
#' Proportion difference estimation |
|
| 2 |
#' |
|
| 3 |
#' The analysis function [a_proportion_diff_j()] can be used to create a layout element to estimate |
|
| 4 |
#' the difference in proportion of responders within a studied population. The primary analysis variable, |
|
| 5 |
#' `vars`, is a logical variable indicating whether a response has occurred for each record. See the `method` |
|
| 6 |
#' parameter for options of methods to use when constructing the confidence interval of the proportion difference. |
|
| 7 |
#' A stratification variable can be supplied via the `strata` element of the `variables` argument. |
|
| 8 |
#' |
|
| 9 |
#' @param df (`data.frame`)\cr input data frame. |
|
| 10 |
#' @param .var (`string`)\cr name of the response variable. |
|
| 11 |
#' @param ref_path (`character`)\cr path to the reference group. |
|
| 12 |
#' @param .spl_context (`environment`)\cr split context environment. |
|
| 13 |
#' @param ... Additional arguments passed to the statistics function. |
|
| 14 |
#' @param .stats (`character`)\cr statistics to calculate. |
|
| 15 |
#' @param .formats (`list`)\cr formats for the statistics. |
|
| 16 |
#' @param .labels (`list`)\cr labels for the statistics. |
|
| 17 |
#' @param .indent_mods (`list`)\cr indentation modifications for the statistics. |
|
| 18 |
#' @param conf_level (`numeric`)\cr confidence level for the confidence interval. |
|
| 19 |
#' @param label (`string`)\cr label for the `diff_est_ci` statistic in [a_proportion_diff_j()]. Defaults to |
|
| 20 |
#' `"% Difference (conf_level CI)"`. Can be overridden by the caller. |
|
| 21 |
#' |
|
| 22 |
#' @name prop_diff |
|
| 23 |
#' @order 1 |
|
| 24 |
#' |
|
| 25 |
#' @note The [a_proportion_diff_j()] function has the `_j` suffix to distinguish it |
|
| 26 |
#' from [tern::a_proportion_diff()]. It is a thin wrapper around |
|
| 27 |
#' `tern::s_proportion_diff()` with the following additions: |
|
| 28 |
#' |
|
| 29 |
#' * The `diff_est_ci` statistic (returned natively by tern since tern#1523) is |
|
| 30 |
#' relabelled with a shorter junco-style label (`"% Difference (conf_level CI)"`). |
|
| 31 |
#' * `ref_path` needs to be provided as extra argument to specify the control group column. |
|
| 32 |
#' |
|
| 33 |
NULL |
|
| 34 | ||
| 35 |
#' @describeIn prop_diff Formatted analysis function which is used as `afun` in `estimate_proportion_diff()`. |
|
| 36 |
#' |
|
| 37 |
#' @return |
|
| 38 |
#' * `a_proportion_diff_j()` returns the corresponding list with formatted [rtables::CellValue()]. |
|
| 39 |
#' |
|
| 40 |
#' @examples |
|
| 41 |
#' nex <- 100 |
|
| 42 |
#' dta <- data.frame( |
|
| 43 |
#' "rsp" = sample(c(TRUE, FALSE), nex, TRUE), |
|
| 44 |
#' "grp" = sample(c("A", "B"), nex, TRUE),
|
|
| 45 |
#' "f1" = sample(c("a1", "a2"), nex, TRUE),
|
|
| 46 |
#' "f2" = sample(c("x", "y", "z"), nex, TRUE),
|
|
| 47 |
#' stringsAsFactors = TRUE |
|
| 48 |
#' ) |
|
| 49 |
#' |
|
| 50 |
#' l <- basic_table() |> |
|
| 51 |
#' split_cols_by(var = "grp") |> |
|
| 52 |
#' analyze( |
|
| 53 |
#' vars = "rsp", |
|
| 54 |
#' afun = a_proportion_diff_j, |
|
| 55 |
#' show_labels = "hidden", |
|
| 56 |
#' na_str = tern::default_na_str(), |
|
| 57 |
#' extra_args = list( |
|
| 58 |
#' conf_level = 0.9, |
|
| 59 |
#' method = "ha", |
|
| 60 |
#' ref_path = c("grp", "B")
|
|
| 61 |
#' ) |
|
| 62 |
#' ) |
|
| 63 |
#' |
|
| 64 |
#' build_table(l, df = dta) |
|
| 65 |
#' @export |
|
| 66 |
#' @order 2 |
|
| 67 |
a_proportion_diff_j <- function( |
|
| 68 |
df, |
|
| 69 |
.var, |
|
| 70 |
ref_path, |
|
| 71 |
.spl_context, |
|
| 72 |
..., |
|
| 73 |
conf_level = 0.95, |
|
| 74 |
label = paste0("% Difference (", f_conf_level(conf_level), ")"),
|
|
| 75 |
.stats = NULL, |
|
| 76 |
.formats = NULL, |
|
| 77 |
.labels = NULL, |
|
| 78 |
.indent_mods = NULL |
|
| 79 |
) {
|
|
| 80 |
# Check for additional parameters to the statistics function |
|
| 81 | 8x |
dots_extra_args <- list(...) |
| 82 | ||
| 83 |
# Only support default stats, not custom stats |
|
| 84 | 8x |
.stats <- .split_std_from_custom_stats(.stats)$default_stats |
| 85 | ||
| 86 |
# Obtain reference column information |
|
| 87 | 8x |
ref <- get_ref_info(ref_path, .spl_context) |
| 88 | ||
| 89 |
# Apply statistics function |
|
| 90 | 8x |
x_stats <- .apply_stat_functions( |
| 91 | 8x |
default_stat_fnc = tern::s_proportion_diff, |
| 92 | 8x |
custom_stat_fnc_list = NULL, |
| 93 | 8x |
args_list = c( |
| 94 | 8x |
df = list(df), |
| 95 | 8x |
.var = .var, |
| 96 | 8x |
.ref_group = list(ref$ref_group), |
| 97 | 8x |
.in_ref_col = ref$in_ref_col, |
| 98 | 8x |
conf_level = conf_level, |
| 99 | 8x |
dots_extra_args |
| 100 |
) |
|
| 101 |
) |
|
| 102 | ||
| 103 |
# Relabel diff_est_ci with junco-style shorter label |
|
| 104 | 8x |
x_stats$diff_est_ci <- with_label(x_stats$diff_est_ci, label) |
| 105 | ||
| 106 |
# Format according to specifications |
|
| 107 | 8x |
format_stats( |
| 108 | 8x |
x_stats, |
| 109 | 8x |
method_groups = "proportion_diff", |
| 110 | 8x |
stats_in = .stats, |
| 111 | 8x |
formats_in = .formats, |
| 112 | 8x |
labels_in = .labels, |
| 113 | 8x |
indents_in = .indent_mods |
| 114 |
) |
|
| 115 |
} |
| 1 |
#' Difference test for two proportions |
|
| 2 |
#' |
|
| 3 |
#' @description `r lifecycle::badge('stable')`
|
|
| 4 |
#' |
|
| 5 |
#' The analysis function [a_test_proportion_diff()] can be used to create a layout element to test |
|
| 6 |
#' the difference between two proportions. The primary analysis variable, `vars`, indicates whether a |
|
| 7 |
#' response has occurred for each record. See the `method` parameter for options of methods to use |
|
| 8 |
#' to calculate the p-value. Additionally, a stratification variable can be supplied via the `strata` |
|
| 9 |
#' element of the `variables` argument. The argument `alternative` specifies the direction of the |
|
| 10 |
#' alternative hypothesis. |
|
| 11 |
#' |
|
| 12 |
#' @inheritParams proposal_argument_convention |
|
| 13 |
#' @param ... Additional arguments passed to [tern::s_test_proportion_diff()], including: |
|
| 14 |
#' * `method` (`string`)\cr one of `chisq`, `cmh`, `cmh_wh`, `fisher` or `schouten`; |
|
| 15 |
#' specifies the test used to calculate the p-value. |
|
| 16 |
#' @param .stats (`character`)\cr statistics to select for the table. |
|
| 17 |
#' |
|
| 18 |
#' @note This function has been forked from the `tern` package. Additional features are: |
|
| 19 |
#' |
|
| 20 |
#' * Additional `ref_path` argument for flexible reference column path specification. |
|
| 21 |
#' |
|
| 22 |
#' @name prop_diff_test |
|
| 23 |
#' @order 1 |
|
| 24 |
NULL |
|
| 25 | ||
| 26 |
#' @describeIn prop_diff_test Formatted analysis function which is used as `afun` |
|
| 27 |
#' |
|
| 28 |
#' @return |
|
| 29 |
#' * `a_test_proportion_diff()` returns the corresponding list with formatted [rtables::CellValue()]. |
|
| 30 |
#' |
|
| 31 |
#' @examples |
|
| 32 |
#' dta <- data.frame( |
|
| 33 |
#' rsp = sample(c(TRUE, FALSE), 100, TRUE), |
|
| 34 |
#' grp = factor(rep(c("A", "B"), each = 50)),
|
|
| 35 |
#' strata = factor(rep(c("V", "W", "X", "Y", "Z"), each = 20))
|
|
| 36 |
#' ) |
|
| 37 |
#' |
|
| 38 |
#' l <- basic_table() |> |
|
| 39 |
#' split_cols_by(var = "grp") |> |
|
| 40 |
#' analyze( |
|
| 41 |
#' vars = "rsp", |
|
| 42 |
#' afun = a_test_proportion_diff, |
|
| 43 |
#' show_labels = "hidden", |
|
| 44 |
#' extra_args = list( |
|
| 45 |
#' method = "cmh", |
|
| 46 |
#' variables = list(strata = "strata"), |
|
| 47 |
#' ref_path = c("grp", "B")
|
|
| 48 |
#' ) |
|
| 49 |
#' ) |
|
| 50 |
#' |
|
| 51 |
#' build_table(l, df = dta) |
|
| 52 |
#' |
|
| 53 |
#' @export |
|
| 54 |
#' @order 1 |
|
| 55 |
a_test_proportion_diff <- function( |
|
| 56 |
df, |
|
| 57 |
.var, |
|
| 58 |
ref_path, |
|
| 59 |
.spl_context, |
|
| 60 |
..., |
|
| 61 |
.stats = NULL, |
|
| 62 |
.formats = NULL, |
|
| 63 |
.labels = NULL, |
|
| 64 |
.indent_mods = NULL |
|
| 65 |
) {
|
|
| 66 |
# Check for additional parameters to the statistics function |
|
| 67 | 2x |
dots_extra_args <- list(...) |
| 68 | ||
| 69 |
# Only support default stats, not custom stats |
|
| 70 | 2x |
.stats <- .split_std_from_custom_stats(.stats)$default_stats |
| 71 | ||
| 72 |
# Obtain reference column information |
|
| 73 | 2x |
ref <- get_ref_info(ref_path, .spl_context) |
| 74 | ||
| 75 |
# Apply statistics function |
|
| 76 | 2x |
x_stats <- .apply_stat_functions( |
| 77 | 2x |
default_stat_fnc = s_test_proportion_diff, |
| 78 | 2x |
custom_stat_fnc_list = NULL, |
| 79 | 2x |
args_list = c( |
| 80 | 2x |
df = list(df), |
| 81 | 2x |
.var = .var, |
| 82 | 2x |
.ref_group = list(ref$ref_group), |
| 83 | 2x |
.in_ref_col = ref$in_ref_col, |
| 84 | 2x |
dots_extra_args |
| 85 |
) |
|
| 86 |
) |
|
| 87 | ||
| 88 |
# Format according to specifications |
|
| 89 | 2x |
format_stats( |
| 90 | 2x |
x_stats, |
| 91 | 2x |
method_groups = "test_proportion_diff", |
| 92 | 2x |
stats_in = .stats, |
| 93 | 2x |
formats_in = .formats, |
| 94 | 2x |
labels_in = .labels, |
| 95 | 2x |
indents_in = .indent_mods |
| 96 |
) |
|
| 97 |
} |
| 1 |
#' Workaround statistics function to time point survival estimate with CI |
|
| 2 |
#' |
|
| 3 |
#' This is a workaround for [tern::s_surv_timepoint()], which adds a statistic |
|
| 4 |
#' containing the time point specific survival estimate together with the |
|
| 5 |
#' confidence interval. |
|
| 6 |
#' |
|
| 7 |
#' @inheritParams proposal_argument_convention |
|
| 8 |
#' |
|
| 9 |
#' @return |
|
| 10 |
#' * `s_event_free` returns a list as returned by the [tern::s_surv_timepoint()] |
|
| 11 |
#' with an additional three-dimensional statistic `event_free_ci` which |
|
| 12 |
#' combines the `event_free_rate` and `rate_ci` statistics. |
|
| 13 |
#' * `a_event_free` is analogous to [tern::a_surv_timepoint] but with the additional |
|
| 14 |
#' three-dimensional statistic described above available via `.stats`. |
|
| 15 |
#' |
|
| 16 |
#' @name event_free |
|
| 17 |
#' @order 1 |
|
| 18 |
NULL |
|
| 19 | ||
| 20 |
#' @describeIn event_free Statistics function which works like [tern::s_surv_timepoint()], |
|
| 21 |
#' the difference is that it returns the additional statistic `event_free_ci`. |
|
| 22 |
#' @param time_point (`numeric`)\cr time point at which to estimate survival. |
|
| 23 |
#' @param time_unit (`string`)\cr unit of time for the time point. |
|
| 24 |
#' @param percent (`flag`)\cr whether to return in percent or not. |
|
| 25 |
#' @export |
|
| 26 |
#' |
|
| 27 |
#' @examples |
|
| 28 |
#' adtte_f <- tern::tern_ex_adtte |> |
|
| 29 |
#' dplyr::filter(PARAMCD == "OS") |> |
|
| 30 |
#' dplyr::mutate( |
|
| 31 |
#' AVAL = tern::day2month(AVAL), |
|
| 32 |
#' is_event = CNSR == 0 |
|
| 33 |
#' ) |
|
| 34 |
#' |
|
| 35 |
#' s_event_free( |
|
| 36 |
#' df = adtte_f, |
|
| 37 |
#' .var = "AVAL", |
|
| 38 |
#' time_point = 6, |
|
| 39 |
#' is_event = "is_event", |
|
| 40 |
#' time_unit = "month" |
|
| 41 |
#' ) |
|
| 42 |
#' @order 3 |
|
| 43 |
s_event_free <- function( |
|
| 44 |
df, |
|
| 45 |
.var, |
|
| 46 |
time_point, |
|
| 47 |
time_unit, |
|
| 48 |
is_event, |
|
| 49 |
percent = FALSE, |
|
| 50 |
control = control_surv_timepoint() |
|
| 51 |
) {
|
|
| 52 | 20x |
checkmate::assert_string(time_unit, min.chars = 1L) |
| 53 | 20x |
start <- s_surv_timepoint( |
| 54 | 20x |
df = df, |
| 55 | 20x |
.var = .var, |
| 56 | 20x |
time_point = time_point, |
| 57 | 20x |
is_event = is_event, |
| 58 | 20x |
control = control |
| 59 |
) |
|
| 60 | 20x |
rates <- c(start$event_free_rate, start$rate_ci) |
| 61 | 10x |
if (!percent) rates <- rates / 100 |
| 62 | 20x |
unit_label <- if (percent) "(%) " else "" |
| 63 | 20x |
c( |
| 64 | 20x |
start, |
| 65 | 20x |
list( |
| 66 | 20x |
event_free_ci = with_label( |
| 67 | 20x |
rates, |
| 68 | 20x |
paste0( |
| 69 | 20x |
time_point, |
| 70 |
"-", |
|
| 71 | 20x |
time_unit, |
| 72 | 20x |
" event-free rate ", |
| 73 | 20x |
unit_label, |
| 74 |
"(",
|
|
| 75 | 20x |
obj_label(start$rate_ci), |
| 76 |
")" |
|
| 77 |
) |
|
| 78 |
) |
|
| 79 |
) |
|
| 80 |
) |
|
| 81 |
} |
|
| 82 | ||
| 83 |
#' @describeIn event_free Formatted analysis function which is used as `afun`. |
|
| 84 |
#' |
|
| 85 |
#' @export |
|
| 86 |
#' @order 2 |
|
| 87 |
#' |
|
| 88 |
#' @examples |
|
| 89 |
#' adtte_f <- tern::tern_ex_adtte |> |
|
| 90 |
#' dplyr::filter(PARAMCD == "OS") |> |
|
| 91 |
#' dplyr::mutate( |
|
| 92 |
#' AVAL = tern::day2month(AVAL), |
|
| 93 |
#' is_event = CNSR == 0 |
|
| 94 |
#' ) |
|
| 95 |
#' |
|
| 96 |
#' basic_table() |> |
|
| 97 |
#' split_cols_by(var = "ARMCD") |> |
|
| 98 |
#' analyze( |
|
| 99 |
#' vars = "AVAL", |
|
| 100 |
#' afun = a_event_free, |
|
| 101 |
#' show_labels = "hidden", |
|
| 102 |
#' na_str = tern::default_na_str(), |
|
| 103 |
#' extra_args = list( |
|
| 104 |
#' time_unit = "week", |
|
| 105 |
#' time_point = 3, |
|
| 106 |
#' is_event = "is_event" |
|
| 107 |
#' ) |
|
| 108 |
#' ) |> |
|
| 109 |
#' build_table(df = adtte_f) |
|
| 110 |
a_event_free <- function(df, .var, ..., .stats = NULL, .formats = NULL, .labels = NULL, .indent_mods = NULL) {
|
|
| 111 |
# Check for additional parameters to the statistics function |
|
| 112 | 18x |
dots_extra_args <- list(...) |
| 113 | ||
| 114 |
# Only support default stats, not custom stats |
|
| 115 | 18x |
.stats <- .split_std_from_custom_stats(.stats)$default_stats |
| 116 | ||
| 117 |
# Apply statistics function |
|
| 118 | 18x |
x_stats <- .apply_stat_functions( |
| 119 | 18x |
default_stat_fnc = s_event_free, |
| 120 | 18x |
custom_stat_fnc_list = NULL, |
| 121 | 18x |
args_list = c(df = list(df), .var = .var, dots_extra_args) |
| 122 |
) |
|
| 123 | ||
| 124 |
# Format according to specifications |
|
| 125 | 18x |
format_stats( |
| 126 | 18x |
x_stats, |
| 127 | 18x |
method_groups = "event_free", |
| 128 | 18x |
stats_in = .stats, |
| 129 | 18x |
formats_in = .formats, |
| 130 | 18x |
labels_in = .labels, |
| 131 | 18x |
indents_in = .indent_mods |
| 132 |
) |
|
| 133 |
} |
| 1 |
#' Get Visit Levels in Order Defined by Numeric Version |
|
| 2 |
#' |
|
| 3 |
#' @param visit_cat (`character`)\cr the categorical version. |
|
| 4 |
#' @param visit_n (`numeric`)\cr the numeric version. |
|
| 5 |
#' |
|
| 6 |
#' @return The unique visit levels in the order defined by the numeric version. |
|
| 7 |
#' @export |
|
| 8 |
#' |
|
| 9 |
#' @examples |
|
| 10 |
#' get_visit_levels( |
|
| 11 |
#' visit_cat = c("Week 1", "Week 11", "Week 2"),
|
|
| 12 |
#' visit_n = c(1, 5, 2) |
|
| 13 |
#' ) |
|
| 14 |
get_visit_levels <- function(visit_cat, visit_n) {
|
|
| 15 | 8x |
checkmate::assert_character(visit_cat) |
| 16 | 7x |
checkmate::assert_numeric(visit_n) |
| 17 | ||
| 18 | 6x |
nvar <- "n" |
| 19 | 6x |
visit_levels_df <- unique(data.frame(cat = visit_cat, n = visit_n)) |
| 20 | 6x |
visit_levels_df <- visit_levels_df[order(visit_levels_df$n), ] |
| 21 | 6x |
visit_levels <- visit_levels_df$cat |
| 22 | ||
| 23 | 6x |
checkmate::assert_character(visit_levels, unique = TRUE) |
| 24 | ||
| 25 | 5x |
visit_levels |
| 26 |
} |
| 1 |
#' @name create_colspan_var |
|
| 2 |
#' @title Creation of Column Spanning Variables |
|
| 3 |
#' @description |
|
| 4 |
#' A function used for creating a spanning variable for treatment groups. |
|
| 5 |
#' @details |
|
| 6 |
#' This function creates a spanning variable for treatment groups that is intended to |
|
| 7 |
#' be used within the column space. |
|
| 8 |
#' @param df (`data.frame`)\cr The name of the data frame in which the spanning variable is to be appended to |
|
| 9 |
#' @param non_active_grp (`character`)\cr The value(s) of the treatments that represent the non-active or comparator |
|
| 10 |
#' treatment groups |
|
| 11 |
#' default value = c('Placebo')
|
|
| 12 |
#' @param non_active_grp_span_lbl (`character`)\cr The assigned value of the spanning |
|
| 13 |
#' variable for the non-active or comparator |
|
| 14 |
#' treatment groups |
|
| 15 |
#' default value = '' |
|
| 16 |
#' @param active_grp_span_lbl (`character`)\cr The assigned value of the spanning |
|
| 17 |
#' variable for the active treatment group(s) |
|
| 18 |
#' default value = 'Active Study Agent' |
|
| 19 |
#' @param colspan_var (`character`)\cr The desired name of the newly created spanning variable |
|
| 20 |
#' default value = 'colspan_trt' |
|
| 21 |
#' @param trt_var (`character`)\cr The name of the treatment variable that is used to determine which |
|
| 22 |
#' spanning treatment group value to apply. |
|
| 23 |
#' default value = 'TRT01A' |
|
| 24 |
#' @returns A data frame that contains the new variable as specified in colspan_var. |
|
| 25 |
#' @rdname colspan_var |
|
| 26 |
#' @export |
|
| 27 |
#' @aliases create_colspan_var |
|
| 28 |
#' @examples |
|
| 29 |
#' |
|
| 30 |
#' library(tibble) |
|
| 31 |
#' |
|
| 32 |
#' df <- tribble( |
|
| 33 |
#' ~TRT01A, |
|
| 34 |
#' "Placebo", |
|
| 35 |
#' "Active 1", |
|
| 36 |
#' "Active 2" |
|
| 37 |
#' ) |
|
| 38 |
#' |
|
| 39 |
#' df$TRT01A <- factor(df$TRT01A, levels = c("Placebo", "Active 1", "Active 2"))
|
|
| 40 |
#' |
|
| 41 |
#' colspan_var <- create_colspan_var( |
|
| 42 |
#' df = df, |
|
| 43 |
#' non_active_grp = c("Placebo"),
|
|
| 44 |
#' non_active_grp_span_lbl = " ", |
|
| 45 |
#' active_grp_span_lbl = "Active Treatment", |
|
| 46 |
#' colspan_var = "colspan_trt", |
|
| 47 |
#' trt_var = "TRT01A" |
|
| 48 |
#' ) |
|
| 49 |
#' |
|
| 50 |
#' colspan_var |
|
| 51 |
create_colspan_var <- function( |
|
| 52 |
df, |
|
| 53 |
non_active_grp = c("Placebo"),
|
|
| 54 |
non_active_grp_span_lbl = " ", |
|
| 55 |
active_grp_span_lbl = "Active Study Agent", |
|
| 56 |
colspan_var = "colspan_trt", |
|
| 57 |
trt_var = "TRT01A" |
|
| 58 |
) {
|
|
| 59 |
# Create a new column with the specified name using base R |
|
| 60 | 5x |
df[[colspan_var]] <- factor( |
| 61 | 5x |
ifelse(df[[trt_var]] %in% non_active_grp, non_active_grp_span_lbl, active_grp_span_lbl), |
| 62 | 5x |
levels = c(active_grp_span_lbl, non_active_grp_span_lbl) |
| 63 |
) |
|
| 64 | ||
| 65 | 5x |
return(df) |
| 66 |
} |
|
| 67 | ||
| 68 |
#' @name create_colspan_map |
|
| 69 |
#' @title Creation of Column Spanning Mapping Dataframe |
|
| 70 |
#' @description |
|
| 71 |
#' A function used for creating a data frame containing the map that is compatible with |
|
| 72 |
#' the rtables split function [rtables::trim_levels_to_map()]. |
|
| 73 |
#' @details |
|
| 74 |
#' This function creates a data frame containing the map that is compatible with the rtables split function |
|
| 75 |
#' [rtables::trim_levels_to_map()]. |
|
| 76 |
#' The levels of the specified trt_var variable will be stored within the trt_var variable |
|
| 77 |
#' and the colspan_var variable will contain the corresponding spanning header value for each treatment group. |
|
| 78 |
#' @inheritParams create_colspan_var |
|
| 79 |
#' @param active_first (`logical`)\cr whether the active columns come first. |
|
| 80 |
#' @returns A data frame that contains the map to be used with the rtables split function |
|
| 81 |
#' [rtables::trim_levels_to_map()]. |
|
| 82 |
#' @seealso [rtables::add_combo_levels()] for creating split functions that utilize combination levels. |
|
| 83 |
#' @rdname colspan_map |
|
| 84 |
#' @export |
|
| 85 |
#' @examples |
|
| 86 |
#' library(tibble) |
|
| 87 |
#' |
|
| 88 |
#' df <- tribble( |
|
| 89 |
#' ~TRT01A, |
|
| 90 |
#' "Placebo", |
|
| 91 |
#' "Active 1", |
|
| 92 |
#' "Active 2" |
|
| 93 |
#' ) |
|
| 94 |
#' |
|
| 95 |
#' df$TRT01A <- factor(df$TRT01A, levels = c("Placebo", "Active 1", "Active 2"))
|
|
| 96 |
#' |
|
| 97 |
#' colspan_map <- create_colspan_map( |
|
| 98 |
#' df = df, |
|
| 99 |
#' non_active_grp = c("Placebo"),
|
|
| 100 |
#' non_active_grp_span_lbl = " ", |
|
| 101 |
#' active_grp_span_lbl = "Active Study Agent", |
|
| 102 |
#' colspan_var = "colspan_trt", |
|
| 103 |
#' trt_var = "TRT01A" |
|
| 104 |
#' ) |
|
| 105 |
#' |
|
| 106 |
#' colspan_map |
|
| 107 |
create_colspan_map <- function( |
|
| 108 |
df, |
|
| 109 |
non_active_grp = c("Placebo"),
|
|
| 110 |
non_active_grp_span_lbl = " ", |
|
| 111 |
active_grp_span_lbl = "Active Study Agent", |
|
| 112 |
colspan_var = "colspan_trt", |
|
| 113 |
trt_var = "TRT01A", |
|
| 114 |
active_first = TRUE |
|
| 115 |
) {
|
|
| 116 | 20x |
act_trtlv <- setdiff(levels(df[[trt_var]]), non_active_grp) |
| 117 | 20x |
act_map_df <- data.frame(a = active_grp_span_lbl, b = act_trtlv) |
| 118 | 20x |
nact_map_df <- data.frame(a = non_active_grp_span_lbl, b = non_active_grp) |
| 119 | 20x |
df <- if (active_first) {
|
| 120 | 19x |
rbind(act_map_df, nact_map_df) |
| 121 |
} else {
|
|
| 122 | 1x |
rbind(nact_map_df, act_map_df) |
| 123 |
} |
|
| 124 | 20x |
names(df) <- c(colspan_var, trt_var) |
| 125 | ||
| 126 | 20x |
return(df) |
| 127 |
} |
| 1 |
#' Simple Content Row Function to Count Rows |
|
| 2 |
#' |
|
| 3 |
#' @return a `VertalRowsSection` object (as returned by [rtables::in_rows()] |
|
| 4 |
#' containing counts from the data. |
|
| 5 |
#' @keywords internal |
|
| 6 |
c_row_counts <- function(df, labelstr, label_fstr) {
|
|
| 7 | 18x |
in_rows(count = nrow(df), .formats = "xx", .labels = sprintf(label_fstr, labelstr)) |
| 8 |
} |
|
| 9 | ||
| 10 |
#' Simple Content Row Function to Count Rows from Alternative Data |
|
| 11 |
#' @return a `VertalRowsSection` object (as returned by [rtables::in_rows()] |
|
| 12 |
#' containing counts from the alt data. |
|
| 13 |
#' @keywords internal |
|
| 14 |
c_row_counts_alt <- function(df, labelstr, label_fstr, .alt_df) {
|
|
| 15 | 9x |
c_row_counts(df = .alt_df, labelstr = labelstr, label_fstr = label_fstr) |
| 16 |
} |
|
| 17 | ||
| 18 |
#' Layout Creating Function Adding Row Counts |
|
| 19 |
#' |
|
| 20 |
#' This is a simple wrapper of [rtables::summarize_row_groups()] and the main |
|
| 21 |
#' additional value is that we can choose whether we want to use the alternative |
|
| 22 |
#' (usually ADSL) data set for the counts (default) or use the original data set. |
|
| 23 |
#' |
|
| 24 |
#' @inheritParams proposal_argument_convention |
|
| 25 |
#' @param label_fstr (`string`)\cr a `sprintf` style format string. |
|
| 26 |
#' It can contain up to one `%s` which takes the current split value and |
|
| 27 |
#' generates the row label. |
|
| 28 |
#' @param alt_counts (`flag`)\cr whether row counts should be taken from |
|
| 29 |
#' `alt_counts_df` (`TRUE`) or from `df` (`FALSE`). |
|
| 30 |
#' |
|
| 31 |
#' @return A modified layout where the latest row split now has a row group |
|
| 32 |
#' summaries (as created by [rtables::summarize_row_groups] for the counts). |
|
| 33 |
#' |
|
| 34 |
#' @export |
|
| 35 |
#' @examples |
|
| 36 |
#' basic_table() |> |
|
| 37 |
#' split_cols_by("ARM") |>
|
|
| 38 |
#' add_colcounts() |> |
|
| 39 |
#' split_rows_by("RACE", split_fun = drop_split_levels) |>
|
|
| 40 |
#' summarize_row_counts(label_fstr = "RACE value - %s") |> |
|
| 41 |
#' analyze("AGE", afun = list_wrap_x(summary), format = "xx.xx") |>
|
|
| 42 |
#' build_table(DM, alt_counts_df = rbind(DM, DM)) |
|
| 43 |
#' |
|
| 44 |
summarize_row_counts <- function(lyt, label_fstr = "%s", alt_counts = TRUE) {
|
|
| 45 | 2x |
checkmate::assert_flag(alt_counts) |
| 46 | ||
| 47 | 2x |
summarize_row_groups( |
| 48 | 2x |
lyt, |
| 49 | 2x |
cfun = if (alt_counts) c_row_counts_alt else c_row_counts, |
| 50 | 2x |
extra_args = list(label_fstr = label_fstr) |
| 51 |
) |
|
| 52 |
} |