diff --git a/NAMESPACE b/NAMESPACE index 07530394a7..e4a0065981 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -137,6 +137,7 @@ export(format_extreme_values_ci) export(format_fraction) export(format_fraction_fixed_dp) export(format_fraction_threshold) +export(format_range_cens) export(format_sigfig) export(format_xx) export(g_bland_altman) @@ -265,6 +266,7 @@ export(range_noinf) export(reapply_varlabels) export(ref_group_position) export(rtable2gg) +export(s_ancova) export(s_bland_altman) export(s_compare) export(s_count_occurrences) @@ -280,6 +282,7 @@ export(s_odds_ratio) export(s_proportion) export(s_proportion_diff) export(s_summary) +export(s_surv_time) export(s_surv_timepoint) export(s_test_proportion_diff) export(sas_na) diff --git a/NEWS.md b/NEWS.md index 8a9f28d61a..2b41f2b002 100644 --- a/NEWS.md +++ b/NEWS.md @@ -2,11 +2,15 @@ ### Miscellaneous * Updated `roxygen2` to 8.0.0 and added `@exportS3Method` tags for S3 methods in `decorate_grob.R` and `utils_grid.R`. +* Converted `s_surv_time()`to exported functions. ### Enhancements * Added `alternative` argument to `s_coxph_pairwise()` to allow one-sided hypothesis testing. * Added `lr_stat_df` to the parameters return list of `s_coxph_pairwise()`. * Added `uncond_exact_diff` method to `estimate_proportion_diff()` for the unconditional exact confidence interval for the difference in proportions by inverting one-sided tail tests over a nuisance parameter. +* Added `range_with_cens_info` statistic to `s_surv_time()`. +* Added `lsmean_se`, `lsmean_ci`, and `lsmean_diffci` statistics to `s_ancova()`. +* Added `s_ancova()` to exported functions. ### Bug Fixes * Fixed bug in `prop_diff_cmh()` which previously failed when strata combinations had 0 observations. diff --git a/R/formatting_functions.R b/R/formatting_functions.R index f18409810c..c09d2628a7 100644 --- a/R/formatting_functions.R +++ b/R/formatting_functions.R @@ -42,7 +42,7 @@ format_fraction <- function(x, ...) { ) } - return(result) + result } #' Format fraction and percentage with fixed single decimal place @@ -77,7 +77,7 @@ format_fraction_fixed_dp <- function(x, ...) { " (", sprintf("%.1f", round(x["num"] / x["denom"] * 100, 1)), "%)" ) } - return(result) + result } #' Format count and fraction @@ -114,7 +114,7 @@ format_count_fraction <- function(x, ...) { paste0(x[1], " (", round(x[2] * 100, 1), "%)") } - return(result) + result } #' Format count and percentage with fixed single decimal place @@ -153,7 +153,7 @@ format_count_fraction_fixed_dp <- function(x, ...) { sprintf("%d (%.1f%%)", x[1], x[2] * 100) } - return(result) + result } #' Format count and fraction with special case for count < 10 @@ -190,7 +190,7 @@ format_count_fraction_lt10 <- function(x, ...) { paste0(x[1], " (", round(x[2] * 100, 1), "%)") } - return(result) + result } #' Format XX as a formatting function @@ -229,17 +229,17 @@ format_xx <- function(str) { rounding <- function(x) { round(x, digits = ifelse(length(y) > 1, nchar(y[2]), 0)) } - return(rounding) + rounding } ) rtable_format <- function(x, output) { values <- Map(y = x, fun = roundings, function(y, fun) fun(y)) regmatches(x = str, m = positions)[[1]] <- values - return(str) + str } - return(rtable_format) + rtable_format } #' Format numeric values by significant figures @@ -534,7 +534,7 @@ format_auto <- function(dt_var, x_stat) { out[is_even] <- str_vals out[!is_even] <- inv_str_fmt - return(paste0(out, collapse = "")) + paste0(out, collapse = "") } } @@ -546,11 +546,11 @@ str_extract <- function(string, pattern = "xx|xx\\.|xx\\.x+", invert = FALSE) { # Helper function count_decimalplaces <- function(dec) { if (is.na(dec)) { - return(0) + 0 } else if (abs(dec - round(dec)) > .Machine$double.eps^0.5) { # For precision nchar(strsplit(format(dec, scientific = FALSE, trim = FALSE), ".", fixed = TRUE)[[1]][[2]]) } else { - return(0) + 0 } } @@ -573,3 +573,34 @@ apply_auto_formatting <- function(.formats, x_stats, .df_row, .var) { } .formats } + +#' Format range with censoring indicators +#' +#' @description `r lifecycle::badge("experimental")` +#' +#' Formats a survival time range where the minimum and/or maximum may be a censored observation. +#' A `+` suffix is appended to a bound when the corresponding censoring flag is `TRUE`. +#' +#' @param digits (`integer(1)`)\cr number of decimal places to display. Defaults to `1L`. +#' +#' @return An `rtables` formatting function that takes a `numeric(4)` vector of the form +#' `c(min, max, lower_censored, upper_censored)`, where `lower_censored` and `upper_censored` +#' are `0`/`1` (or `FALSE`/`TRUE`) flags, and returns a string in the format `"min to max"`, +#' with `+` appended to `min` and/or `max` when the corresponding censoring flag is non-zero. +#' +#' @examples +#' fmt <- format_range_cens(1L) +#' fmt(c(1.23, 9.87, 1, 0)) +#' fmt(c(1.23, 9.87, 0, 0)) +#' +#' @family formatting functions +#' @export +format_range_cens <- function(digits = 1L) { + checkmate::assert_integerish(digits) + function(x, ...) { + checkmate::assert_numeric(x, len = 4) + l_result <- paste0(round(x[1], digits), if (x[3] != 0) "+" else "") + h_result <- paste0(round(x[2], digits), if (x[4] != 0) "+" else "") + paste(l_result, "to", h_result) + } +} diff --git a/R/summarize_ancova.R b/R/summarize_ancova.R index f740c43e46..96f4106acb 100644 --- a/R/summarize_ancova.R +++ b/R/summarize_ancova.R @@ -101,16 +101,20 @@ h_ancova <- function(.var, #' of the investigated linear model. #' #' @return -#' * `s_ancova()` returns a named list of 5 statistics: +#' * `s_ancova()` returns a named list of 8 statistics: #' * `n`: Count of complete sample size for the group. #' * `lsmean`: Estimated marginal means in the group. +#' * `lsmean_se`: Adjusted mean with standard error as a 2-element vector `c(emmean, SE)`. +#' * `lsmean_ci`: Adjusted mean with confidence interval as a 3-element vector `c(emmean, lower.CL, upper.CL)`. #' * `lsmean_diff`: Difference in estimated marginal means in comparison to the reference group. #' If working with the reference group, this will be empty. #' * `lsmean_diff_ci`: Confidence level for difference in estimated marginal means in comparison #' to the reference group. +#' * `lsmean_diff_with_ci`: Difference in adjusted means with confidence interval as a 3-element vector +#' `c(estimate, lower.CL, upper.CL)`. #' * `pval`: p-value (not adjusted for multiple comparisons). #' -#' @keywords internal +#' @export s_ancova <- function(df, .var, .df_row, @@ -165,8 +169,20 @@ s_ancova <- function(df, list( n = length(y[!is.na(y)]), lsmean = formatters::with_label(sum_fit_level$emmean, "Adjusted Mean"), + lsmean_se = formatters::with_label( + c(sum_fit_level$emmean, sum_fit_level$SE), + "Adjusted Mean (SE)" + ), + lsmean_ci = formatters::with_label( + c(sum_fit_level$emmean, sum_fit_level$lower.CL, sum_fit_level$upper.CL), + f_conf_level(conf_level) + ), lsmean_diff = formatters::with_label(numeric(), "Difference in Adjusted Means"), lsmean_diff_ci = formatters::with_label(numeric(), f_conf_level(conf_level)), + lsmean_diff_with_ci = formatters::with_label( + numeric(), + paste0("Difference in Adjusted Means (", f_conf_level(conf_level), ")") + ), pval = formatters::with_label(numeric(), "p-value") ) } else { @@ -202,11 +218,23 @@ s_ancova <- function(df, list( n = length(y[!is.na(y)]), lsmean = formatters::with_label(sum_fit_level$emmean, "Adjusted Mean"), + lsmean_se = formatters::with_label( + c(sum_fit_level$emmean, sum_fit_level$SE), + "Adjusted Mean (SE)" + ), + lsmean_ci = formatters::with_label( + c(sum_fit_level$emmean, sum_fit_level$lower.CL, sum_fit_level$upper.CL), + f_conf_level(conf_level) + ), lsmean_diff = formatters::with_label(sum_contrasts_level$estimate, "Difference in Adjusted Means"), lsmean_diff_ci = formatters::with_label( c(sum_contrasts_level$lower.CL, sum_contrasts_level$upper.CL), f_conf_level(conf_level) ), + lsmean_diff_with_ci = formatters::with_label( + c(sum_contrasts_level$estimate, sum_contrasts_level$lower.CL, sum_contrasts_level$upper.CL), + paste0("Difference in Adjusted Means (", f_conf_level(conf_level), ")") + ), pval = formatters::with_label(sum_contrasts_level$p.value, "p-value") ) } diff --git a/R/survival_coxph_pairwise.R b/R/survival_coxph_pairwise.R index 1132646768..96dc41e0d1 100644 --- a/R/survival_coxph_pairwise.R +++ b/R/survival_coxph_pairwise.R @@ -112,37 +112,37 @@ s_coxph_pairwise <- function(df, pval <- if (pval_method == "wald") { if (alternative == "two.sided") { - sum_cox$waldtest["pvalue"] + sum_cox$waldtest["pvalue"] } else { - # Need to calculate the signed Wald statistic. - beta_est <- unname(cox_fit$coefficients) - beta_se <- sqrt(cox_fit$var[1, 1]) - signed_wald_stat <- beta_est / beta_se - if (alternative == "less") { - stats::pnorm(signed_wald_stat) - } else { - stats::pnorm(signed_wald_stat, lower.tail = FALSE) - } - } + # Need to calculate the signed Wald statistic. + beta_est <- unname(cox_fit$coefficients) + beta_se <- sqrt(cox_fit$var[1, 1]) + signed_wald_stat <- beta_est / beta_se + if (alternative == "less") { + stats::pnorm(signed_wald_stat) + } else { + stats::pnorm(signed_wald_stat, lower.tail = FALSE) + } + } } else if (pval_method == "log-rank") { if (alternative == "two.sided") { - original_survdiff$pvalue + original_survdiff$pvalue } else { - # Need to calculate the signed log-rank statistic, - # which is not included in the original survdiff output. - otmp <- rowSums(original_survdiff$obs) - etmp <- rowSums(original_survdiff$exp) - signed_lr_stat <- (otmp[2] - etmp[2]) / sqrt(original_survdiff$var[2, 2]) + # Need to calculate the signed log-rank statistic, + # which is not included in the original survdiff output. + otmp <- rowSums(original_survdiff$obs) + etmp <- rowSums(original_survdiff$exp) + signed_lr_stat <- (otmp[2] - etmp[2]) / sqrt(original_survdiff$var[2, 2]) - if (alternative == "less") { - stats::pnorm(signed_lr_stat) - } else { - stats::pnorm(signed_lr_stat, lower.tail = FALSE) - } + if (alternative == "less") { + stats::pnorm(signed_lr_stat) + } else { + stats::pnorm(signed_lr_stat, lower.tail = FALSE) + } } } else if (pval_method == "likelihood") { if (alternative != "two.sided") { - stop("Likelihood ratio test does not support one-sided alternatives") + stop("Likelihood ratio test does not support one-sided alternatives") } sum_cox$logtest["pvalue"] } else { diff --git a/R/survival_time.R b/R/survival_time.R index ba7b1e6b28..357cea6592 100644 --- a/R/survival_time.R +++ b/R/survival_time.R @@ -51,8 +51,9 @@ NULL #' * `range_censor`: Survival time range for censored observations. #' * `range_event`: Survival time range for observations with events. #' * `range`: Survival time range for all observations. +#' * `range_with_cens_info`: Survival time range for all observations, with `+` suffix on censored bounds. #' -#' @keywords internal +#' @export s_surv_time <- function(df, .var, ..., @@ -81,6 +82,11 @@ s_surv_time <- function(df, range_event <- range_noinf(df[[.var]][df[[is_event]]], na.rm = TRUE) range <- range_noinf(df[[.var]], na.rm = TRUE) + any_censored <- !all(df[[is_event]]) + no_event <- !any(df[[is_event]]) + lower_censored <- any_censored && (no_event || range_censor[1] < range_event[1]) + upper_censored <- any_censored && (no_event || range_censor[2] > range_event[2]) + names(quantiles) <- as.character(100 * quantiles) srv_qt_tab_pre <- unlist(srv_qt_tab_pre) srv_qt_ci <- lapply(quantiles, function(x) { @@ -116,6 +122,10 @@ s_surv_time <- function(df, ), quantiles_upper = formatters::with_label( unname(srv_qt_ci[[2]]), paste0(quantiles[2] * 100, "%-ile (", f_conf_level(conf_level), ")") + ), + range_with_cens_info = formatters::with_label( + c(range, lower_censored, upper_censored), + "Min - Max (with censoring)" ) ) } diff --git a/R/utils_default_stats_formats_labels.R b/R/utils_default_stats_formats_labels.R index 937abc8892..cbd0f885bc 100644 --- a/R/utils_default_stats_formats_labels.R +++ b/R/utils_default_stats_formats_labels.R @@ -580,14 +580,16 @@ tern_default_stats <- list( estimate_odds_ratio = c("or_ci", "n_tot"), estimate_proportion = c("n_prop", "prop_ci"), estimate_proportion_diff = c("diff", "diff_ci"), - summarize_ancova = c("n", "lsmean", "lsmean_diff", "lsmean_diff_ci", "pval"), + summarize_ancova = c("n", "lsmean", "lsmean_se", "lsmean_ci", + "lsmean_diff", "lsmean_diff_ci", "lsmean_diff_with_ci", "pval"), summarize_coxreg = c("n", "hr", "ci", "pval", "pval_inter"), summarize_glm_count = c("n", "rate", "rate_ci", "rate_ratio", "rate_ratio_ci", "pval"), summarize_num_patients = c("unique", "nonunique", "unique_count"), summarize_patients_events_in_cols = c("unique", "all"), surv_time = c( "median", "median_ci", "median_ci_3d", "quantiles", - "quantiles_lower", "quantiles_upper", "range_censor", "range_event", "range" + "quantiles_lower", "quantiles_upper", "range_censor", "range_event", "range", + "range_with_cens_info" ), surv_timepoint = c("pt_at_risk", "event_free_rate", "rate_se", "rate_ci", "event_free_rate_3d"), surv_timepoint_diff = c("rate_diff", "rate_diff_ci", "ztest_pval", "rate_diff_ci_3d"), @@ -626,8 +628,11 @@ tern_default_formats <- c( lr_stat_df = "xx.xx (xx.)", iqr = "xx.x", lsmean = "xx.xx", + lsmean_ci = "xx.xx (xx.xx - xx.xx)", lsmean_diff = "xx.xx", lsmean_diff_ci = "(xx.xx, xx.xx)", + lsmean_diff_with_ci = "xx.xx (xx.xx - xx.xx)", + lsmean_se = "xx.xx (xx.xx)", mad = "xx.x", max = "xx.x", mean = "xx.x", @@ -677,6 +682,7 @@ tern_default_formats <- c( rate_ratio = "xx.xxxx", rate_ratio_ci = "(xx.xxxx, xx.xxxx)", rate_se = "xx.xx", + range_with_cens_info = list(format_range_cens(1L)), riskdiff = "xx.x (xx.x - xx.x)", sd = "xx.x", se = "xx.x", @@ -730,6 +736,7 @@ tern_default_labels <- c( range = "Min - Max", range_censor = "Range (censored)", range_event = "Range (event)", + range_with_cens_info = "Min - Max (with censoring)", rate = "Adjusted Rate", rate_ratio = "Adjusted Rate Ratio", sd = "SD", diff --git a/_pkgdown.yml b/_pkgdown.yml index 8d4ecb1552..fe2e4ce89c 100644 --- a/_pkgdown.yml +++ b/_pkgdown.yml @@ -64,6 +64,7 @@ reference: - -estimate_coef - -summarize_functions - s_surv_timepoint + - s_surv_time - s_test_proportion_diff - title: Model-Specific Functions diff --git a/man/format_range_cens.Rd b/man/format_range_cens.Rd new file mode 100644 index 0000000000..d5e4c15d3d --- /dev/null +++ b/man/format_range_cens.Rd @@ -0,0 +1,46 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/formatting_functions.R +\name{format_range_cens} +\alias{format_range_cens} +\title{Format range with censoring indicators} +\usage{ +format_range_cens(digits = 1L) +} +\arguments{ +\item{digits}{(\code{integer(1)})\cr number of decimal places to display. Defaults to \code{1L}.} +} +\value{ +An \code{rtables} formatting function that takes a \code{numeric(4)} vector of the form +\code{c(min, max, lower_censored, upper_censored)}, where \code{lower_censored} and \code{upper_censored} +are \code{0}/\code{1} (or \code{FALSE}/\code{TRUE}) flags, and returns a string in the format \code{"min to max"}, +with \code{+} appended to \code{min} and/or \code{max} when the corresponding censoring flag is non-zero. +} +\description{ +\ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#experimental}{\figure{lifecycle-experimental.svg}{options: alt='[Experimental]'}}}{\strong{[Experimental]}} + +Formats a survival time range where the minimum and/or maximum may be a censored observation. +A \code{+} suffix is appended to a bound when the corresponding censoring flag is \code{TRUE}. +} +\examples{ +fmt <- format_range_cens(1L) +fmt(c(1.23, 9.87, 1, 0)) +fmt(c(1.23, 9.87, 0, 0)) + +} +\seealso{ +Other formatting functions: +\code{\link{extreme_format}}, +\code{\link{format_auto}()}, +\code{\link{format_count_fraction}()}, +\code{\link{format_count_fraction_fixed_dp}()}, +\code{\link{format_count_fraction_lt10}()}, +\code{\link{format_extreme_values}()}, +\code{\link{format_extreme_values_ci}()}, +\code{\link{format_fraction}()}, +\code{\link{format_fraction_fixed_dp}()}, +\code{\link{format_fraction_threshold}()}, +\code{\link{format_sigfig}()}, +\code{\link{format_xx}()}, +\code{\link{formatting_functions}} +} +\concept{formatting functions} diff --git a/man/summarize_ancova.Rd b/man/summarize_ancova.Rd index 2b246a9b88..bb044b9281 100644 --- a/man/summarize_ancova.Rd +++ b/man/summarize_ancova.Rd @@ -91,7 +91,7 @@ times, to avoid warnings from \code{rtables}.} \item{.stats}{(\code{character})\cr statistics to select for the table. -Options are: \verb{'n', 'lsmean', 'lsmean_diff', 'lsmean_diff_ci', 'pval'}} +Options are: \verb{'n', 'lsmean', 'lsmean_se', 'lsmean_ci', 'lsmean_diff', 'lsmean_diff_ci', 'lsmean_diff_with_ci', 'pval'}} \item{.stat_names}{(\code{character})\cr names of the statistics that are passed directly to name single statistics (\code{.stats}). This option is visible when producing \code{\link[rtables:as_result_df]{rtables::as_result_df()}} with \code{make_ard = TRUE}.} @@ -123,14 +123,18 @@ the statistics from \code{s_ancova()} to the table layout. } \itemize{ -\item \code{s_ancova()} returns a named list of 5 statistics: +\item \code{s_ancova()} returns a named list of 8 statistics: \itemize{ \item \code{n}: Count of complete sample size for the group. \item \code{lsmean}: Estimated marginal means in the group. +\item \code{lsmean_se}: Adjusted mean with standard error as a 2-element vector \code{c(emmean, SE)}. +\item \code{lsmean_ci}: Adjusted mean with confidence interval as a 3-element vector \code{c(emmean, lower.CL, upper.CL)}. \item \code{lsmean_diff}: Difference in estimated marginal means in comparison to the reference group. If working with the reference group, this will be empty. \item \code{lsmean_diff_ci}: Confidence level for difference in estimated marginal means in comparison to the reference group. +\item \code{lsmean_diff_with_ci}: Difference in adjusted means with confidence interval as a 3-element vector +\code{c(estimate, lower.CL, upper.CL)}. \item \code{pval}: p-value (not adjusted for multiple comparisons). } } diff --git a/man/survival_time.Rd b/man/survival_time.Rd index 13735de25b..20d9c0d671 100644 --- a/man/survival_time.Rd +++ b/man/survival_time.Rd @@ -77,7 +77,7 @@ times, to avoid warnings from \code{rtables}.} \item{.stats}{(\code{character})\cr statistics to select for the table. -Options are: \verb{'median', 'median_ci', 'median_ci_3d', 'quantiles', 'quantiles_lower', 'quantiles_upper', 'range_censor', 'range_event', 'range'}} +Options are: \verb{'median', 'median_ci', 'median_ci_3d', 'quantiles', 'quantiles_lower', 'quantiles_upper', 'range_censor', 'range_event', 'range', 'range_with_cens_info'}} \item{.stat_names}{(\code{character})\cr names of the statistics that are passed directly to name single statistics (\code{.stats}). This option is visible when producing \code{\link[rtables:as_result_df]{rtables::as_result_df()}} with \code{make_ard = TRUE}.} @@ -119,6 +119,7 @@ the statistics from \code{s_surv_time()} to the table layout. \item \code{range_censor}: Survival time range for censored observations. \item \code{range_event}: Survival time range for observations with events. \item \code{range}: Survival time range for all observations. +\item \code{range_with_cens_info}: Survival time range for all observations, with \code{+} suffix on censored bounds. } } @@ -174,4 +175,3 @@ a_surv_time( ) } -\keyword{internal} diff --git a/tests/testthat/_snaps/summarize_ancova.md b/tests/testthat/_snaps/summarize_ancova.md index c9f1e45fd9..2f39e02ca7 100644 --- a/tests/testthat/_snaps/summarize_ancova.md +++ b/tests/testthat/_snaps/summarize_ancova.md @@ -23,6 +23,16 @@ attr(,"label") [1] "Adjusted Mean" + $lsmean_se + [1] 5.71740917 0.06680849 + attr(,"label") + [1] "Adjusted Mean (SE)" + + $lsmean_ci + [1] 5.717409 5.542996 5.891823 + attr(,"label") + [1] "99% CI" + $lsmean_diff [1] -0.4374138 attr(,"label") @@ -33,6 +43,11 @@ attr(,"label") [1] "99% CI" + $lsmean_diff_with_ci + [1] -0.4374138 -1.4268150 0.5519873 + attr(,"label") + [1] "Difference in Adjusted Means (99% CI)" + $pval [1] 0.2503574 attr(,"label") @@ -52,6 +67,16 @@ attr(,"label") [1] "Adjusted Mean" + $lsmean_se + [1] 4.36264203 0.08598191 + attr(,"label") + [1] "Adjusted Mean (SE)" + + $lsmean_ci + [1] 4.362642 4.192651 4.532633 + attr(,"label") + [1] "95% CI" + $lsmean_diff numeric(0) attr(,"label") @@ -62,6 +87,11 @@ attr(,"label") [1] "95% CI" + $lsmean_diff_with_ci + numeric(0) + attr(,"label") + [1] "Difference in Adjusted Means (95% CI)" + $pval numeric(0) attr(,"label") diff --git a/tests/testthat/_snaps/survival_time.md b/tests/testthat/_snaps/survival_time.md index e0325a1c54..0416fb55f7 100644 --- a/tests/testthat/_snaps/survival_time.md +++ b/tests/testthat/_snaps/survival_time.md @@ -48,6 +48,11 @@ attr(,"label") [1] "75%-ile (95% CI)" + $range_with_cens_info + [1] 0.07143141 154.08901021 0.00000000 0.00000000 + attr(,"label") + [1] "Min - Max (with censoring)" + # s_surv_time works with customized arguments @@ -99,6 +104,11 @@ attr(,"label") [1] "80%-ile (99% CI)" + $range_with_cens_info + [1] 0.07143141 154.08901021 0.00000000 0.00000000 + attr(,"label") + [1] "Min - Max (with censoring)" + # a_surv_time works with default arguments @@ -107,16 +117,28 @@ Output RowsVerticalSection (in_rows) object print method: ---------------------------- - row_name formatted_cell indent_mod row_label - 1 median 24.8 0 Median - 2 median_ci (21.10, 31.35) 0 Median 95% CI - 3 median_ci_3d 24.76 (21.10 - 31.35) 0 Median (95% CI) - 4 quantiles 10.8 - 47.6 0 25% and 75%-ile - 5 quantiles_lower 10.81 (6.65 - 13.43) 0 25%-ile (95% CI) - 6 quantiles_upper 47.60 (39.27 - 57.82) 0 75%-ile (95% CI) - 7 range_censor 0.8 to 78.9 0 Range (censored) - 8 range_event 0.1 to 155.5 0 Range (event) - 9 range 0.1 - 155.5 0 Min - Max + row_name formatted_cell indent_mod + 1 median 24.8 0 + 2 median_ci (21.10, 31.35) 0 + 3 median_ci_3d 24.76 (21.10 - 31.35) 0 + 4 quantiles 10.8 - 47.6 0 + 5 quantiles_lower 10.81 (6.65 - 13.43) 0 + 6 quantiles_upper 47.60 (39.27 - 57.82) 0 + 7 range_censor 0.8 to 78.9 0 + 8 range_event 0.1 to 155.5 0 + 9 range 0.1 - 155.5 0 + 10 range_with_cens_info 0.1 to 155.5 0 + row_label + 1 Median + 2 Median 95% CI + 3 Median (95% CI) + 4 25% and 75%-ile + 5 25%-ile (95% CI) + 6 75%-ile (95% CI) + 7 Range (censored) + 8 Range (event) + 9 Min - Max + 10 Min - Max (with censoring) # a_surv_time works with customized arguments @@ -148,18 +170,19 @@ Code res Output - ARM A ARM B ARM C - ——————————————————————————————————————————————————————————————————————————————————————————————————————————————————— - Survival Time (Months) - Median 32.0 23.9 20.8 - 90% CI (25.557055515, 49.309164814) (18.861684287, 32.147869886) (12.954083786, 26.023348062) - Median (90% CI) 32.02 (25.56 - 49.31) 23.91 (18.86 - 32.15) 20.77 (12.95 - 26.02) - 40% and 60%-ile 25.6 - 46.5 18.3 - 29.2 13.0 - 25.7 - 40%-ile (90% CI) 25.56 (20.73 - 33.39) 18.26 (12.77 - 23.91) 12.95 (10.10 - 24.76) - 60%-ile (90% CI) 46.51 (32.02 - 57.82) 29.19 (23.91 - 41.30) 25.75 (20.77 - 37.10) - Range (censored) 0.8 to 63.5 6.2 to 78.9 3.4 to 52.4 - Range (event) 0.3 to 155.5 0.1 to 154.1 0.6 to 80.7 - Range 0.3 - 155.5 0.1 - 154.1 0.6 - 80.7 + ARM A ARM B ARM C + ————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————— + Survival Time (Months) + Median 32.0 23.9 20.8 + 90% CI (25.557055515, 49.309164814) (18.861684287, 32.147869886) (12.954083786, 26.023348062) + Median (90% CI) 32.02 (25.56 - 49.31) 23.91 (18.86 - 32.15) 20.77 (12.95 - 26.02) + 40% and 60%-ile 25.6 - 46.5 18.3 - 29.2 13.0 - 25.7 + 40%-ile (90% CI) 25.56 (20.73 - 33.39) 18.26 (12.77 - 23.91) 12.95 (10.10 - 24.76) + 60%-ile (90% CI) 46.51 (32.02 - 57.82) 29.19 (23.91 - 41.30) 25.75 (20.77 - 37.10) + Range (censored) 0.8 to 63.5 6.2 to 78.9 3.4 to 52.4 + Range (event) 0.3 to 155.5 0.1 to 154.1 0.6 to 80.7 + Range 0.3 - 155.5 0.1 - 154.1 0.6 - 80.7 + Min - Max (with censoring) 0.3 to 155.5 0.1 to 154.1 0.6 to 80.7 # surv_time works with referential footnotes diff --git a/tests/testthat/test-formatting_functions.R b/tests/testthat/test-formatting_functions.R index 89749ba57a..1b74c1a26e 100644 --- a/tests/testthat/test-formatting_functions.R +++ b/tests/testthat/test-formatting_functions.R @@ -251,3 +251,28 @@ testthat::test_that("auto formatting works with NA values", { testthat::expect_snapshot(result) }) + +testthat::test_that("format_range_cens works with no censoring", { + fmt <- format_range_cens(1L) + testthat::expect_identical(fmt(c(1.2, 9.8, 0, 0)), "1.2 to 9.8") +}) + +testthat::test_that("format_range_cens appends + to lower bound when lower censored", { + fmt <- format_range_cens(1L) + testthat::expect_identical(fmt(c(1.2, 9.8, 1, 0)), "1.2+ to 9.8") +}) + +testthat::test_that("format_range_cens appends + to upper bound when upper censored", { + fmt <- format_range_cens(1L) + testthat::expect_identical(fmt(c(1.2, 9.8, 0, 1)), "1.2 to 9.8+") +}) + +testthat::test_that("format_range_cens appends + to both bounds when both censored", { + fmt <- format_range_cens(1L) + testthat::expect_identical(fmt(c(1.2, 9.8, 1, 1)), "1.2+ to 9.8+") +}) + +testthat::test_that("format_range_cens respects digits argument", { + fmt <- format_range_cens(2L) + testthat::expect_identical(fmt(c(1.234, 9.876, 1, 0)), "1.23+ to 9.88") +}) diff --git a/tests/testthat/test-summarize_ancova.R b/tests/testthat/test-summarize_ancova.R index df6bc820f4..e1c49df4cb 100644 --- a/tests/testthat/test-summarize_ancova.R +++ b/tests/testthat/test-summarize_ancova.R @@ -231,3 +231,21 @@ testthat::test_that("summarize_ancova works with irregular arm levels", { res <- testthat::expect_silent(result3) testthat::expect_snapshot(res) }) + +testthat::test_that("s_ancova returns lsmean_se and lsmean_ci for ref column", { + df_ref <- iris %>% dplyr::filter(Species == "setosa") + result <- s_ancova( + df = df_ref, + .var = "Sepal.Length", + .df_row = iris, + variables = list(arm = "Species", covariates = "Petal.Length"), + .ref_group = df_ref, + .in_ref_col = TRUE, + conf_level = 0.95 + ) + + testthat::expect_length(result$lsmean_se, 2) + testthat::expect_length(result$lsmean_ci, 3) + testthat::expect_true(all(is.na(result$lsmean_diff_with_ci))) + testthat::expect_length(result$lsmean_diff, 0) +}) diff --git a/tests/testthat/test-survival_time.R b/tests/testthat/test-survival_time.R index e2d3635889..ebaf7b5717 100644 --- a/tests/testthat/test-survival_time.R +++ b/tests/testthat/test-survival_time.R @@ -196,3 +196,59 @@ testthat::test_that("a_surv_time works when `is_event` only has FALSE observatio ) ) }) + +testthat::test_that("s_surv_time includes range_with_cens_info with no censoring at boundaries (0, 0)", { + anl <- tibble::tribble( + ~AVAL, ~is_event, + 2, TRUE, + 5, TRUE, + 8, TRUE + ) + result <- s_surv_time(anl, .var = "AVAL", is_event = "is_event") + testthat::expect_named(result, c( + "median", "median_ci", "quantiles", "range_censor", "range_event", + "range", "median_ci_3d", "quantiles_lower", "quantiles_upper", "range_with_cens_info" + )) + rwci <- result$range_with_cens_info + testthat::expect_equal(rwci[1:2], c(2, 8)) + testthat::expect_equal(as.numeric(rwci[3:4]), c(0, 0)) +}) + +testthat::test_that("s_surv_time range_with_cens_info flags upper censored (0, 1)", { + anl <- tibble::tribble( + ~AVAL, ~is_event, + 2, TRUE, + 5, TRUE, + 10, FALSE + ) + result <- s_surv_time(anl, .var = "AVAL", is_event = "is_event") + rwci <- result$range_with_cens_info + testthat::expect_equal(as.numeric(rwci[3]), 0) # lower not censored + testthat::expect_equal(as.numeric(rwci[4]), 1) # upper censored +}) + +testthat::test_that("s_surv_time range_with_cens_info flags lower censored (1, 0)", { + anl <- tibble::tribble( + ~AVAL, ~is_event, + 1, FALSE, + 5, TRUE, + 8, TRUE + ) + result <- s_surv_time(anl, .var = "AVAL", is_event = "is_event") + rwci <- result$range_with_cens_info + testthat::expect_equal(as.numeric(rwci[3]), 1) + testthat::expect_equal(as.numeric(rwci[4]), 0) +}) + +testthat::test_that("s_surv_time range_with_cens_info flags both bounds censored when all censored (1, 1)", { + # All observations are censored + anl <- tibble::tribble( + ~AVAL, ~is_event, + 2, FALSE, + 5, FALSE, + 8, FALSE + ) + result <- s_surv_time(anl, .var = "AVAL", is_event = "is_event") + rwci <- result$range_with_cens_info + testthat::expect_equal(as.numeric(rwci[3:4]), c(1, 1)) +})