Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add method argument to s_odds_ratio() and estimate_odds_ratio() #1320

Merged
merged 15 commits into from
Oct 7, 2024
2 changes: 1 addition & 1 deletion DESCRIPTION
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
27 changes: 19 additions & 8 deletions R/odds_ratio.R
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand All @@ -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)) {
Expand Down Expand Up @@ -118,14 +122,21 @@ 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]]
y$n_tot <- y_all$n_tot
}
}

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)")
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -193,15 +202,15 @@ 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,
.stats = "or_ci",
.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,
Expand Down Expand Up @@ -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()].
Expand Down Expand Up @@ -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.
Expand Down
5 changes: 4 additions & 1 deletion man/h_odds_ratio.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 7 additions & 4 deletions man/odds_ratio.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions tests/testthat/_snaps/odds_ratio.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

121 changes: 80 additions & 41 deletions tests/testthat/test-odds_ratio.R
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand All @@ -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)", {
Expand All @@ -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)
Expand Down Expand Up @@ -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)
})
Loading