diff --git a/DESCRIPTION b/DESCRIPTION index ff84f8d014..23d0254bd6 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -48,7 +48,7 @@ Imports: rlang (>= 1.1.0), scales (>= 1.2.0), stats, - survival (>= 3.2-13), + survival (>= 3.7-0), tibble (>= 2.0.0), tidyr (>= 0.8.3), utils diff --git a/NEWS.md b/NEWS.md index 06ba194bfe..2bb03424ff 100644 --- a/NEWS.md +++ b/NEWS.md @@ -12,6 +12,7 @@ * Refactored `estimate_incidence_rate` to work as both an analyze function and a summarize function, controlled by the added `summarize` parameter. When `summarize = TRUE`, labels can be fine-tuned via the new `label_fmt` argument to the same function. * Added `fraction` statistic to the `analyze_var_count` method group. * Improved `summarize_glm_count()` documentation and all its associated functions to better describe the results and the functions' purpose. +* Added `method` argument to `s_odds_ratio()` and `estimate_odds_ratio()` to control whether exact or approximate conditional likelihood calculations are used. ### Bug Fixes * Added defaults for `d_count_cumulative` parameters as described in the documentation. diff --git a/R/odds_ratio.R b/R/odds_ratio.R index eccdccc1b1..16bb58ac06 100644 --- a/R/odds_ratio.R +++ b/R/odds_ratio.R @@ -13,6 +13,8 @@ #' @inheritParams argument_convention #' @param .stats (`character`)\cr statistics to select for the table. Run `get_stats("estimate_odds_ratio")` #' to see available statistics for this function. +#' @param method (`string`)\cr whether to use the correct (`"exact"`) calculation in the conditional likelihood or one +#' of the approximations. See [survival::clogit()] for details. #' #' @note #' * This function uses logistic regression for unstratified analyses, and conditional logistic regression for @@ -64,7 +66,8 @@ s_odds_ratio <- function(df, .df_row, variables = list(arm = NULL, strata = NULL), conf_level = 0.95, - groups_list = NULL) { + groups_list = NULL, + method = "exact") { y <- list(or_ci = "", n_tot = "") if (!.in_ref_col) { @@ -83,6 +86,7 @@ s_odds_ratio <- function(df, y <- or_glm(data, conf_level = conf_level) } else { assert_df_with_variables(.df_row, c(list(rsp = .var), variables)) + checkmate::assert_subset(method, c("exact", "approximate", "efron", "breslow"), empty.ok = FALSE) # The group variable prepared for clogit must be synchronised with combination groups definition. if (is.null(groups_list)) { @@ -118,7 +122,7 @@ s_odds_ratio <- function(df, grp = grp, strata = interaction(.df_row[variables$strata]) ) - y_all <- or_clogit(data, conf_level = conf_level) + y_all <- or_clogit(data, conf_level = conf_level, method = method) checkmate::assert_string(trt_grp) checkmate::assert_subset(trt_grp, names(y_all$or_ci)) y$or_ci <- y_all$or_ci[[trt_grp]] @@ -126,6 +130,13 @@ s_odds_ratio <- function(df, } } + if ("est" %in% names(y$or_ci) && is.na(y$or_ci[["est"]]) && method != "approximate") { + warning( + "Unable to compute the odds ratio estimate. Please try re-running the function with ", + 'parameter `method` set to "approximate".' + ) + } + y$or_ci <- formatters::with_label( x = y$or_ci, label = paste0("Odds Ratio (", 100 * conf_level, "% CI)") @@ -163,8 +174,6 @@ a_odds_ratio <- make_afun( #' @describeIn odds_ratio Layout-creating function which can take statistics function arguments #' and additional format arguments. This function is a wrapper for [rtables::analyze()]. #' -#' @param ... arguments passed to `s_odds_ratio()`. -#' #' @return #' * `estimate_odds_ratio()` returns a layout object suitable for passing to further layouting functions, #' or to [rtables::build_table()]. Adding this function to an `rtable` layout will add formatted rows containing @@ -193,7 +202,7 @@ estimate_odds_ratio <- function(lyt, groups_list = NULL, na_str = default_na_str(), nested = TRUE, - ..., + method = "exact", show_labels = "hidden", table_names = vars, var_labels = vars, @@ -201,7 +210,7 @@ estimate_odds_ratio <- function(lyt, .formats = NULL, .labels = NULL, .indent_mods = NULL) { - extra_args <- list(variables = variables, conf_level = conf_level, groups_list = groups_list, ...) + extra_args <- list(variables = variables, conf_level = conf_level, groups_list = groups_list, method = method) afun <- make_afun( a_odds_ratio, @@ -230,6 +239,7 @@ estimate_odds_ratio <- function(lyt, #' #' Functions to calculate odds ratios in [estimate_odds_ratio()]. #' +#' @inheritParams odds_ratio #' @inheritParams argument_convention #' @param data (`data.frame`)\cr data frame containing at least the variables `rsp` and `grp`, and optionally #' `strata` for [or_clogit()]. @@ -300,19 +310,20 @@ or_glm <- function(data, conf_level) { #' or_clogit(data, conf_level = 0.95) #' #' @export -or_clogit <- function(data, conf_level) { +or_clogit <- function(data, conf_level, method = "exact") { checkmate::assert_logical(data$rsp) assert_proportion_value(conf_level) assert_df_with_variables(data, list(rsp = "rsp", grp = "grp", strata = "strata")) checkmate::assert_multi_class(data$grp, classes = c("factor", "character")) checkmate::assert_multi_class(data$strata, classes = c("factor", "character")) + checkmate::assert_subset(method, c("exact", "approximate", "efron", "breslow"), empty.ok = FALSE) data$grp <- as_factor_keep_attributes(data$grp) data$strata <- as_factor_keep_attributes(data$strata) # Deviation from convention: `survival::strata` must be simply `strata`. formula <- stats::as.formula("rsp ~ grp + strata(strata)") - model_fit <- clogit_with_tryCatch(formula = formula, data = data) + model_fit <- clogit_with_tryCatch(formula = formula, data = data, method = method) # Create a list with one set of OR estimates and CI per coefficient, i.e. # comparison of one group vs. the reference group. diff --git a/man/h_odds_ratio.Rd b/man/h_odds_ratio.Rd index 238c2d7c86..ca24b33a8a 100644 --- a/man/h_odds_ratio.Rd +++ b/man/h_odds_ratio.Rd @@ -8,13 +8,16 @@ \usage{ or_glm(data, conf_level) -or_clogit(data, conf_level) +or_clogit(data, conf_level, method = "exact") } \arguments{ \item{data}{(\code{data.frame})\cr data frame containing at least the variables \code{rsp} and \code{grp}, and optionally \code{strata} for \code{\link[=or_clogit]{or_clogit()}}.} \item{conf_level}{(\code{proportion})\cr confidence level of the interval.} + +\item{method}{(\code{string})\cr whether to use the correct (\code{"exact"}) calculation in the conditional likelihood or one +of the approximations. See \code{\link[survival:clogit]{survival::clogit()}} for details.} } \value{ A named \code{list} of elements \code{or_ci} and \code{n_tot}. diff --git a/man/odds_ratio.Rd b/man/odds_ratio.Rd index ee88612e3b..b0361d3410 100644 --- a/man/odds_ratio.Rd +++ b/man/odds_ratio.Rd @@ -15,7 +15,7 @@ estimate_odds_ratio( groups_list = NULL, na_str = default_na_str(), nested = TRUE, - ..., + method = "exact", show_labels = "hidden", table_names = vars, var_labels = vars, @@ -33,7 +33,8 @@ s_odds_ratio( .df_row, variables = list(arm = NULL, strata = NULL), conf_level = 0.95, - groups_list = NULL + groups_list = NULL, + method = "exact" ) a_odds_ratio( @@ -44,7 +45,8 @@ a_odds_ratio( .df_row, variables = list(arm = NULL, strata = NULL), conf_level = 0.95, - groups_list = NULL + groups_list = NULL, + method = "exact" ) } \arguments{ @@ -65,7 +67,8 @@ levels that belong to it in the character vectors that are elements of the list. possible (\code{TRUE}, the default) or as a new top-level element (\code{FALSE}). Ignored if it would nest a split. underneath analyses, which is not allowed.} -\item{...}{arguments passed to \code{s_odds_ratio()}.} +\item{method}{(\code{string})\cr whether to use the correct (\code{"exact"}) calculation in the conditional likelihood or one +of the approximations. See \code{\link[survival:clogit]{survival::clogit()}} for details.} \item{show_labels}{(\code{string})\cr label visibility: one of "default", "visible" and "hidden".} diff --git a/tests/testthat/_snaps/odds_ratio.md b/tests/testthat/_snaps/odds_ratio.md index 3614b53e64..3bbb44dd2d 100644 --- a/tests/testthat/_snaps/odds_ratio.md +++ b/tests/testthat/_snaps/odds_ratio.md @@ -97,3 +97,12 @@ ——————————————————————————————————————————————————————————— Odds Ratio (95% CI) 1.24 (0.54 - 2.89) +# estimate_odds_ratio method argument works + + Code + res + Output + A B + ———————————————————————————————————————————— + Odds Ratio (95% CI) 0.96 (0.85 - 1.08) + diff --git a/tests/testthat/test-odds_ratio.R b/tests/testthat/test-odds_ratio.R index 1cbd34c79d..0a00b93866 100644 --- a/tests/testthat/test-odds_ratio.R +++ b/tests/testthat/test-odds_ratio.R @@ -30,11 +30,7 @@ testthat::test_that("or_clogit estimates right OR and CI", { stringsAsFactors = TRUE ) - # https://github.com/therneau/survival/issues/240 - withr::with_options( - opts_partial_match_old, - result <- or_clogit(data, conf_level = 0.95) - ) + result <- or_clogit(data, conf_level = 0.95) # from SAS res <- testthat::expect_silent(result) @@ -66,17 +62,13 @@ testthat::test_that("s_odds_ratio estimates right OR and CI (stratified analysis strata = factor(sample(c("C", "D"), 100, TRUE)) ) - # https://github.com/therneau/survival/issues/240 - withr::with_options( - opts_partial_match_old, - result <- s_odds_ratio( - df = subset(dta, grp == "A"), - .var = "rsp", - .ref_group = subset(dta, grp == "B"), - .in_ref_col = FALSE, - .df_row = dta, - variables = list(arm = "grp", strata = "strata") - ) + result <- s_odds_ratio( + df = subset(dta, grp == "A"), + .var = "rsp", + .ref_group = subset(dta, grp == "B"), + .in_ref_col = FALSE, + .df_row = dta, + variables = list(arm = "grp", strata = "strata") ) res <- testthat::expect_silent(result) @@ -94,19 +86,15 @@ testthat::test_that("s_odds_ratio returns error for incorrect groups", { "Arms A+B" = c("A", "B") ) - # https://github.com/therneau/survival/issues/240 - withr::with_options( - opts_partial_match_old, - result <- testthat::expect_error(s_odds_ratio( - df = subset(data, grp == "A"), - .var = "rsp", - .ref_group = subset(data, grp == "B"), - .in_ref_col = FALSE, - .df_row = data, - variables = list(arm = "grp", strata = "strata"), - groups_list = groups - )) - ) + testthat::expect_error(result <- s_odds_ratio( + df = subset(data, grp == "A"), + .var = "rsp", + .ref_group = subset(data, grp == "B"), + .in_ref_col = FALSE, + .df_row = data, + variables = list(arm = "grp", strata = "strata"), + groups_list = groups + )) }) testthat::test_that("estimate_odds_ratio estimates right OR and CI (unstratified analysis)", { @@ -132,14 +120,10 @@ testthat::test_that("estimate_odds_ratio estimates right OR and CI (stratified a strata = factor(sample(c("C", "D"), 100, TRUE)) ) - # https://github.com/therneau/survival/issues/240 - withr::with_options( - opts_partial_match_old, - result <- basic_table() %>% - split_cols_by(var = "grp", ref_group = "A", split_fun = ref_group_position("first")) %>% - estimate_odds_ratio(vars = "rsp", variables = list(arm = "grp", strata = "strata")) %>% - build_table(df = data) - ) + result <- basic_table() %>% + split_cols_by(var = "grp", ref_group = "A", split_fun = ref_group_position("first")) %>% + estimate_odds_ratio(vars = "rsp", variables = list(arm = "grp", strata = "strata")) %>% + build_table(df = data) res <- testthat::expect_silent(result) testthat::expect_snapshot(res) @@ -170,12 +154,67 @@ testthat::test_that("estimate_odds_ratio works with strata and combined groups", groups_list = groups ) - # https://github.com/therneau/survival/issues/240 - withr::with_options( - opts_partial_match_old, - result <- build_table(lyt = lyt, df = anl) + result <- build_table(lyt = lyt, df = anl) + + res <- testthat::expect_silent(result) + testthat::expect_snapshot(res) +}) + +testthat::test_that("s_odds_ratio method argument works", { + set.seed(1) + nex <- 2000 # Number of example rows + dta <- data.frame( + "rsp" = sample(c(TRUE, FALSE), nex, TRUE), + "grp" = sample(c("A", "B"), nex, TRUE), + "f1" = sample(c("a1", "a2"), nex, TRUE), + "f2" = sample(c("x", "y", "z"), nex, TRUE), + strata = factor(sample(c("C", "D"), nex, TRUE)), + stringsAsFactors = TRUE + ) + + res <- s_odds_ratio( + df = subset(dta, grp == "A"), + .var = "rsp", + .ref_group = subset(dta, grp == "B"), + .in_ref_col = FALSE, + .df_row = dta, + variables = list(arm = "grp", strata = "strata"), + method = "approximate" ) + testthat::expect_false(all(is.na(res$or_ci))) + + # warning works + expect_warning( + s_odds_ratio( + df = subset(dta, grp == "A"), + .var = "rsp", + .ref_group = subset(dta, grp == "B"), + .in_ref_col = FALSE, + .df_row = dta, + variables = list(arm = "grp", strata = "strata") + ) + ) +}) + +testthat::test_that("estimate_odds_ratio method argument works", { + nex <- 2000 # Number of example rows + set.seed(12) + dta <- data.frame( + "rsp" = sample(c(TRUE, FALSE), nex, TRUE), + "grp" = sample(c("A", "B"), nex, TRUE), + "f1" = sample(c("a1", "a2"), nex, TRUE), + "f2" = sample(c("x", "y", "z"), nex, TRUE), + strata = factor(sample(c("C", "D"), nex, TRUE)), + stringsAsFactors = TRUE + ) + + lyt <- basic_table() %>% + split_cols_by(var = "grp", ref_group = "B") %>% + estimate_odds_ratio(vars = "rsp", variables = list(arm = "grp", strata = "strata"), method = "approximate") + + result <- build_table(lyt, df = dta) + res <- testthat::expect_silent(result) testthat::expect_snapshot(res) })