From 7ee4b0f461ecb0f8f5b337402c0d93fc091751ad Mon Sep 17 00:00:00 2001 From: Daniel Sabanes Bove Date: Fri, 12 Jun 2026 13:57:11 +0000 Subject: [PATCH 01/13] start dev --- R/prop_diff_test.R | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/R/prop_diff_test.R b/R/prop_diff_test.R index 1c5f4be587..b9b9385d9b 100644 --- a/R/prop_diff_test.R +++ b/R/prop_diff_test.R @@ -9,7 +9,7 @@ #' supplied via the `strata` element of the `variables` argument. #' #' @inheritParams argument_convention -#' @param method (`string`)\cr one of `chisq`, `cmh`, `cmh_wh`, `fisher`, or `schouten`; +#' @param method (`string`)\cr one of `chisq`, `cmh`, `cmh_sato`, `cmh_wh`, `fisher`, or `schouten`; #' specifies the test used to calculate the p-value. #' @param .stats (`character`)\cr statistics to select for the table. #' @@ -53,7 +53,7 @@ s_test_proportion_diff <- function(df, .ref_group, .in_ref_col, variables = list(strata = NULL), - method = c("chisq", "schouten", "fisher", "cmh", "cmh_wh"), + method = c("chisq", "schouten", "fisher", "cmh", "cmh_sato", "cmh_wh"), alternative = c("two.sided", "less", "greater"), ...) { method <- match.arg(method) @@ -82,6 +82,7 @@ s_test_proportion_diff <- function(df, tbl <- switch(method, cmh = table(grp, rsp, strata), + cmh_sato = table(grp, rsp, strata), cmh_wh = table(grp, rsp, strata), table(grp, rsp) ) @@ -91,6 +92,7 @@ s_test_proportion_diff <- function(df, cmh = prop_cmh(tbl, alternative = alternative), fisher = prop_fisher(tbl, alternative = alternative), schouten = prop_schouten(tbl, alternative = alternative), + cmh_sato = prop_cmh(tbl, alternative = alternative, diff_se = "sato"), cmh_wh = prop_cmh(tbl, alternative = alternative, transform = "wilson_hilferty") ) } @@ -118,6 +120,7 @@ d_test_proportion_diff <- function(method, alternative = c("two.sided", "less", "schouten" = "Chi-Squared Test with Schouten Correction", "chisq" = "Chi-Squared Test", "cmh" = "Cochran-Mantel-Haenszel Test", + "cmh_sato" = "Cochran-Mantel-Haenszel Test with Sato Variance Estimator", "cmh_wh" = "Cochran-Mantel-Haenszel Test with Wilson-Hilferty Transformation", "fisher" = "Fisher's Exact Test", stop(paste(method, "does not have a description")) @@ -235,7 +238,7 @@ a_test_proportion_diff <- function(df, test_proportion_diff <- function(lyt, vars, variables = list(strata = NULL), - method = c("chisq", "schouten", "fisher", "cmh", "cmh_wh"), + method = c("chisq", "schouten", "fisher", "cmh", "cmh_sato", "cmh_wh"), alternative = c("two.sided", "less", "greater"), var_labels = vars, na_str = default_na_str(), @@ -321,17 +324,20 @@ prop_chisq <- function(tbl, alternative = c("two.sided", "less", "greater")) { #' #' @param ary (`array`, 3 dimensions)\cr array with two groups in rows, the binary response #' (`TRUE`/`FALSE`) in columns, and the strata in the third dimension. +#' @param diff_se (`string`)\cr either `standard` or `sato`; specifies whether to use the Sato variance estimator to calculate the chi-squared statistic. #' @param transform (`string`)\cr either `none` or `wilson_hilferty`; specifies whether to apply #' the Wilson-Hilferty transformation of the chi-squared statistic. #' #' @keywords internal prop_cmh <- function(ary, alternative = c("two.sided", "less", "greater"), + diff_se = c("standard", "sato"), transform = c("none", "wilson_hilferty")) { checkmate::assert_array(ary) checkmate::assert_integer(c(ncol(ary), nrow(ary)), lower = 2, upper = 2) checkmate::assert_integer(length(dim(ary)), lower = 3, upper = 3) alternative <- match.arg(alternative) + diff_se <- match.arg(diff_se) transform <- match.arg(transform) strata_sizes <- apply(ary, MARGIN = 3, sum) From 695615e8f2e65094abe2c232d721d3765c58013a Mon Sep 17 00:00:00 2001 From: Daniel Sabanes Bove Date: Mon, 15 Jun 2026 11:49:19 +0800 Subject: [PATCH 02/13] make sato test work --- R/prop_diff.R | 154 ++++++++++++------ R/prop_diff_test.R | 39 +++-- tests/testthat/_snaps/test_proportion_diff.md | 29 +++- tests/testthat/test-test_proportion_diff.R | 99 ++++++++++- 4 files changed, 253 insertions(+), 68 deletions(-) diff --git a/R/prop_diff.R b/R/prop_diff.R index 1e10395134..ba4aa187f1 100644 --- a/R/prop_diff.R +++ b/R/prop_diff.R @@ -596,6 +596,81 @@ prop_diff_nc <- function(rsp, ) } +#' @describeIn h_prop_diff Helper function to calculate the CMH weighted +#' difference in proportions. +#' +#' @param tbl (`array`)\cr 3-dimensional array with dimensions corresponding to +#' group, response, and strata. The second dimension (response) should have names +#' "TRUE" and "FALSE". +#' +#' @keywords internal +h_diff_cmh <- function(tbl) { + checkmate::assert_array(tbl) + checkmate::assert_integer(c(ncol(tbl), nrow(tbl)), lower = 2, upper = 2) + checkmate::assert_integer(length(dim(tbl)), lower = 3, upper = 3) + checkmate::assert_true(identical(dimnames(tbl)[[2]], c("TRUE", "FALSE"))) + + n1 <- colSums(tbl[1, 1:2, ]) + n2 <- colSums(tbl[2, 1:2, ]) + x1 <- tbl[1, 1, ] + p1 <- x1 / n1 + x2 <- tbl[2, 1, ] + p2 <- x2 / n2 + # CMH weights + use_stratum <- (n1 > 0) & (n2 > 0) + n1 <- n1[use_stratum] + n2 <- n2[use_stratum] + p1 <- p1[use_stratum] + p2 <- p2[use_stratum] + wt <- (n1 * n2 / (n1 + n2)) + wt_normalized <- wt / sum(wt) + est1 <- sum(wt_normalized * p1) + est2 <- sum(wt_normalized * p2) + diff_est <- est2 - est1 + + list( + x1 = x1, + n1 = n1, + p1 = p1, + x2 = x2, + n2 = n2, + p2 = p2, + wt = wt, + wt_normalized = wt_normalized, + est1 = est1, + est2 = est2, + diff_est = diff_est + ) +} + +#' @describeIn h_prop_diff Helper function to calculate the standard error for the +#' CMH weighted difference in proportions. +#' +#' @param cmh_results (`list`)\cr output of [h_diff_cmh()]. +#' +#' @keywords internal +h_diff_cmh_se <- function(cmh_results, diff_se = c("standard", "sato")) { + checkmate::assert_list(cmh_results, types = "numeric", any.missing = FALSE, names = "unique") + diff_se <- match.arg(diff_se) + l <- cmh_results # For easier readability of the formulas below. + if (diff_se == "standard") { + terms1 <- l$p1 * (1 - l$p1) / l$n1 + terms2 <- l$p2 * (1 - l$p2) / l$n2 + sqrt(sum((terms1 + terms2) * l$wt_normalized^2)) + } else { + # Sato variance estimator. + p_terms_num <- l$n2^2 * l$x1 - + l$n1^2 * l$x2 + + l$n1 * l$n2 * (l$n1 - l$n2) / 2 + p_terms <- p_terms_num / (l$n1 + l$n2)^2 + q_terms_num <- l$x1 * (l$n2 - l$x2) + l$x2 * (l$n1 - l$x1) + q_terms <- q_terms_num / (2 * (l$n1 + l$n2)) + num <- l$diff_est * sum(p_terms) + sum(q_terms) + denom <- sum(l$wt)^2 + sqrt(num / denom) + } +} + #' @describeIn h_prop_diff Calculates the weighted difference. This is defined as the difference in #' response rates between the experimental treatment group and the control treatment group, adjusted #' for stratification factors by applying Cochran-Mantel-Haenszel (CMH) weights. For the CMH chi-squared @@ -645,66 +720,43 @@ prop_diff_cmh <- function(rsp, warning("Less than 5 observations in some strata.") } - # first dimension: FALSE, TRUE - # 2nd dimension: CONTROL, TX + # first dimension: CONTROL, TX + # 2nd dimension: TRUE, FALSE # 3rd dimension: levels of strata - # rsp as factor rsp to handle edge case of no FALSE (or TRUE) rsp records + # Note: rsp needs to be a factor to handle edge case of + # no FALSE (or TRUE) rsp records. t_tbl <- table( - factor(rsp, levels = c("FALSE", "TRUE")), grp, + factor(rsp, levels = c("TRUE", "FALSE")), strata ) - n1 <- colSums(t_tbl[1:2, 1, ]) - n2 <- colSums(t_tbl[1:2, 2, ]) - x1 <- t_tbl[2, 1, ] - p1 <- x1 / n1 - x2 <- t_tbl[2, 2, ] - p2 <- x2 / n2 - # CMH weights - use_stratum <- (n1 > 0) & (n2 > 0) - n1 <- n1[use_stratum] - n2 <- n2[use_stratum] - p1 <- p1[use_stratum] - p2 <- p2[use_stratum] - wt <- (n1 * n2 / (n1 + n2)) - wt_normalized <- wt / sum(wt) - est1 <- sum(wt_normalized * p1) - est2 <- sum(wt_normalized * p2) - estimate <- c(est1, est2) + cmh <- h_diff_cmh(t_tbl) + + estimate <- c(cmh$est1, cmh$est2) names(estimate) <- levels(grp) - se1 <- sqrt(sum(wt_normalized^2 * p1 * (1 - p1) / n1)) - se2 <- sqrt(sum(wt_normalized^2 * p2 * (1 - p2) / n2)) + se1 <- sqrt(sum(cmh$wt_normalized^2 * cmh$p1 * (1 - cmh$p1) / cmh$n1)) + se2 <- sqrt(sum(cmh$wt_normalized^2 * cmh$p2 * (1 - cmh$p2) / cmh$n2)) z <- stats::qnorm((1 + conf_level) / 2) err1 <- z * se1 err2 <- z * se2 - ci1 <- c((est1 - err1), (est1 + err1)) - ci2 <- c((est2 - err2), (est2 + err2)) + ci1 <- c((cmh$est1 - err1), (cmh$est1 + err1)) + ci2 <- c((cmh$est2 - err2), (cmh$est2 + err2)) estimate_ci <- list(ci1, ci2) names(estimate_ci) <- levels(grp) - diff_est <- est2 - est1 if (diff_se %in% c("standard", "sato")) { - se_diff <- if (diff_se == "standard") { - sqrt(sum(((p1 * (1 - p1) / n1) + (p2 * (1 - p2) / n2)) * wt_normalized^2)) - } else { - # Sato variance estimator. - p_terms <- (n2^2 * x1 - n1^2 * x2 + n1 * n2 * (n1 - n2) / 2) / (n1 + n2)^2 - q_terms <- (x1 * (n2 - x2) + x2 * (n1 - x1)) / (2 * (n1 + n2)) - num <- diff_est * sum(p_terms) + sum(q_terms) - denom <- sum(wt)^2 - sqrt(num / denom) - } - diff_ci <- c(diff_est - z * se_diff, diff_est + z * se_diff) + se_diff <- h_diff_cmh_se(cmh, diff_se = diff_se) + diff_ci <- c(cmh$diff_est - z * se_diff, cmh$diff_est + z * se_diff) } else { # Miettinen and Nurminen method is used. z_stat_fun <- function(delta) { var_est <- h_miettinen_nurminen_var_est( - n1 = n1, n2 = n2, - x1 = x1, x2 = x2, + n1 = cmh$n1, n2 = cmh$n2, + x1 = cmh$x1, x2 = cmh$x2, diff_par = delta )$var_est - num <- sum(wt * (p2 - p1 - delta)) - denom <- sqrt(sum(wt^2 * var_est)) + num <- sum(cmh$wt * (cmh$p2 - cmh$p1 - delta)) + denom <- sqrt(sum(cmh$wt^2 * var_est)) num / denom } # Find upper and lower confidence limits by root finding such that @@ -712,27 +764,27 @@ prop_diff_cmh <- function(rsp, root_lower <- function(delta) z_stat_fun(delta) - z root_upper <- function(delta) z_stat_fun(delta) + z diff_ci <- c( - stats::uniroot(root_lower, interval = c(-0.99, diff_est))$root, - stats::uniroot(root_upper, interval = c(diff_est, 0.99))$root + stats::uniroot(root_lower, interval = c(-0.99, cmh$diff_est))$root, + stats::uniroot(root_upper, interval = c(cmh$diff_est, 0.99))$root ) # Calculate the standard error separately. var_est <- h_miettinen_nurminen_var_est( - n1 = n1, n2 = n2, - x1 = x1, x2 = x2, - diff_par = diff_est + n1 = cmh$n1, n2 = cmh$n2, + x1 = cmh$x1, x2 = cmh$x2, + diff_par = cmh$diff_est )$var_est - se_diff <- sqrt(sum(wt_normalized^2 * var_est)) + se_diff <- sqrt(sum(cmh$wt_normalized^2 * var_est)) } list( prop = estimate, prop_ci = estimate_ci, - diff = diff_est, + diff = cmh$diff_est, diff_ci = diff_ci, se_diff = se_diff, - weights = wt_normalized, - n1 = n1, - n2 = n2 + weights = cmh$wt_normalized, + n1 = cmh$n1, + n2 = cmh$n2 ) } diff --git a/R/prop_diff_test.R b/R/prop_diff_test.R index b9b9385d9b..ed75fb6705 100644 --- a/R/prop_diff_test.R +++ b/R/prop_diff_test.R @@ -346,22 +346,35 @@ prop_cmh <- function(ary, ary <- ary[, , strata_sizes > 1] } - cmh_res <- stats::mantelhaen.test(ary, correct = FALSE, alternative = alternative) - - if (transform == "none") { - cmh_res$p.value + z_stat <- if (diff_se == "standard") { + mh_res <- stats::mantelhaen.test(ary, correct = FALSE, alternative = alternative) + checkmate::assert_true(mh_res$parameter == 1) + sqrt(unname(mh_res$statistic)) * ifelse(unname(mh_res$estimate) < 1, 1, -1) } else { - chisq_stat <- unname(cmh_res$statistic) - df <- unname(cmh_res$parameter) - num <- (chisq_stat / df)^(1 / 3) - (1 - 2 / (9 * df)) - denom <- sqrt(2 / (9 * df)) - wh_stat <- num / denom + # Use the Sato variance estimator. + cmh <- h_diff_cmh(ary) + cmh_se <- h_diff_cmh_se(cmh, diff_se = "sato") + cmh$diff_est / cmh_se + } - if (alternative == "two.sided") { - 2 * stats::pnorm(-abs(wh_stat)) - } else { - stats::pnorm(wh_stat, lower.tail = (alternative == "greater")) + if (transform == "wilson_hilferty") { + if (diff_se == "sato") { + warning( + "Wilson-Hilferty transformation was not designed ", + "for use with the Sato variance estimator" + ) } + chisq_stat <- z_stat^2 + df <- 1 # Because we only compare two groups. + num <- (1 - 2 / (9 * df)) - (chisq_stat / df)^(1 / 3) + denom <- sqrt(2 / (9 * df)) + z_stat <- num / denom * sign(z_stat) # Preserve the direction of effect. + } + + if (alternative == "two.sided") { + 2 * stats::pnorm(-abs(z_stat)) + } else { + stats::pnorm(z_stat, lower.tail = (alternative == "greater")) } } diff --git a/tests/testthat/_snaps/test_proportion_diff.md b/tests/testthat/_snaps/test_proportion_diff.md index 9d740d69cd..176cd3ebf1 100644 --- a/tests/testthat/_snaps/test_proportion_diff.md +++ b/tests/testthat/_snaps/test_proportion_diff.md @@ -19,7 +19,7 @@ Output [1] 0.02826514 -# prop_cmh returns right result +# prop_cmh returns right result for odds ratio < 1 Code res @@ -30,15 +30,36 @@ Code res_less + Output + [1] 0.3238582 + +--- + + Code + res_greater Output [1] 0.6761418 +# prop_cmh returns right result for odds ratio > 1 + + Code + res + Output + [1] 0.3833285 + +--- + + Code + res_less + Output + [1] 0.8083357 + --- Code res_greater Output - [1] 0.3238582 + [1] 0.1916643 # prop_cmh also works when there are strata with just one observation @@ -124,6 +145,10 @@ Output [1] 0.3477347 +# prop_cmh with Sato variance estimator and Wilson-Hilferty transformation works + + 0.383328522648178 + # s_test_proportion_diff and d_test_proportion_diff return right result Code diff --git a/tests/testthat/test-test_proportion_diff.R b/tests/testthat/test-test_proportion_diff.R index 1da73e89f8..73b4ffa4c9 100644 --- a/tests/testthat/test-test_proportion_diff.R +++ b/tests/testthat/test-test_proportion_diff.R @@ -20,24 +20,58 @@ testthat::test_that("prop_chisq returns right result", { testthat::expect_snapshot(res_greater) }) -testthat::test_that("prop_cmh returns right result", { +testthat::test_that("prop_cmh returns right result for odds ratio < 1", { set.seed(1, kind = "Mersenne-Twister") - rsp <- sample(c(TRUE, FALSE), 100, TRUE) + rsp <- factor(sample(c(TRUE, FALSE), 100, TRUE), levels = c("TRUE", "FALSE")) grp <- factor(rep(c("A", "B"), each = 50)) strata <- factor(rep(c("V", "W", "X", "Y", "Z"), each = 20)) tbl <- table(grp, rsp, strata) + checkmate::assert_true(stats::mantelhaen.test(tbl, correct = FALSE)$estimate < 1) result <- prop_cmh(tbl) res <- testthat::expect_silent(result) testthat::expect_snapshot(res) + expected <- stats::mantelhaen.test(tbl, correct = FALSE)$p.value + testthat::expect_equal(result, expected, tolerance = 1e-3) result_less <- prop_cmh(tbl, alternative = "less") res_less <- testthat::expect_silent(result_less) testthat::expect_snapshot(res_less) + expected_less <- stats::mantelhaen.test(tbl, correct = FALSE, alternative = "less")$p.value + testthat::expect_equal(result_less, expected_less, tolerance = 1e-3) result_greater <- prop_cmh(tbl, alternative = "greater") res_greater <- testthat::expect_silent(result_greater) testthat::expect_snapshot(res_greater) + expected_greater <- stats::mantelhaen.test(tbl, correct = FALSE, alternative = "greater")$p.value + testthat::expect_equal(result_greater, expected_greater, tolerance = 1e-3) +}) + +testthat::test_that("prop_cmh returns right result for odds ratio > 1", { + set.seed(847, kind = "Mersenne-Twister") + rsp <- factor(sample(c(TRUE, FALSE), 100, TRUE), levels = c("TRUE", "FALSE")) + grp <- factor(rep(c("A", "B"), each = 50)) + strata <- factor(rep(c("V", "W", "X", "Y", "Z"), each = 20)) + tbl <- table(grp, rsp, strata) + checkmate::assert_true(stats::mantelhaen.test(tbl, correct = FALSE)$estimate > 1) + + result <- prop_cmh(tbl) + res <- testthat::expect_silent(result) + testthat::expect_snapshot(res) + expected <- stats::mantelhaen.test(tbl, correct = FALSE)$p.value + testthat::expect_equal(result, expected, tolerance = 1e-3) + + result_less <- prop_cmh(tbl, alternative = "less") + res_less <- testthat::expect_silent(result_less) + testthat::expect_snapshot(res_less) + expected_less <- stats::mantelhaen.test(tbl, correct = FALSE, alternative = "less")$p.value + testthat::expect_equal(result_less, expected_less, tolerance = 1e-3) + + result_greater <- prop_cmh(tbl, alternative = "greater") + res_greater <- testthat::expect_silent(result_greater) + testthat::expect_snapshot(res_greater) + expected_greater <- stats::mantelhaen.test(tbl, correct = FALSE, alternative = "greater")$p.value + testthat::expect_equal(result_greater, expected_greater, tolerance = 1e-3) }) testthat::test_that("prop_cmh also works when there are strata with just one observation", { @@ -161,6 +195,67 @@ testthat::test_that("prop_cmh with Wilson-Hilferty transformation works", { expect_equal(result_greater, result_cmh_greater, tolerance = 1e-1) }) +testthat::test_that("prop_cmh with Sato variance estimator works", { + tbl1 <- structure( + c( + 2L, 5L, 14L, 11L, + 3L, 4L, 8L, 6L, + 2L, 7L, 17L, 13L, + 4L, 5L, 13L, 12L + ), + .Dim = c(2L, 2L, 4L), + .Dimnames = list( + grp = c("B", "A"), + rsp = c("TRUE", "FALSE"), + strata = c("S1", "S2", "S3", "S4") + ), + class = "table" + ) + result1 <- prop_cmh(tbl1, diff_se = "sato") + testthat::expect_equal(result1, 0.035, tolerance = 1e-3) + + tbl2 <- structure( + c( + 2L, 7L, 14L, 8L, + 3L, 3L, 8L, 7L, + 2L, 10L, 17L, 10L, + 4L, 5L, 13L, 12L + ), + .Dim = c(2L, 2L, 4L), + .Dimnames = list( + grp = c("B", "A"), + rsp = c("TRUE", "FALSE"), + strata = c("S1", "S2", "S3", "S4") + ), + class = "table" + ) + result2 <- prop_cmh(tbl2, diff_se = "sato") + testthat::expect_equal(result2, 0.004, tolerance = 1e-2) +}) + +testthat::test_that("prop_cmh with Sato variance estimator and Wilson-Hilferty transformation works", { + tbl <- structure( + c( + 2L, 5L, 14L, 11L, + 3L, 4L, 8L, 6L, + 2L, 7L, 17L, 13L, + 4L, 5L, 13L, 12L + ), + .Dim = c(2L, 2L, 4L), + .Dimnames = list( + grp = c("B", "A"), + rsp = c("TRUE", "FALSE"), + strata = c("S1", "S2", "S3", "S4") + ), + class = "table" + ) + testthat::expect_warning( + result <- prop_cmh(tbl, diff_se = "sato", transform = "wilson_hilferty"), + "not designed for use with the Sato variance estimator" + ) + testthat::expect_snapshot_value(res, style = "deparse", tolerance = 1e-3) +}) + testthat::test_that("s_test_proportion_diff and d_test_proportion_diff return right result", { set.seed(1984, kind = "Mersenne-Twister") dta <- data.frame( From 9c87b991cd98aa73a3bc1c4107edeab22322b5ce Mon Sep 17 00:00:00 2001 From: Daniel Sabanes Bove Date: Mon, 15 Jun 2026 11:53:34 +0800 Subject: [PATCH 03/13] add NEWS entry --- NEWS.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/NEWS.md b/NEWS.md index f4fa10b877..cc12a22411 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,19 +1,21 @@ # tern 0.9.10.9012 -### 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 `cmh_sato` as option for `test_proportion_diff()` to enable CMH test with Sato variance estimator. * 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. +* Fixed bug in `prop_cmh()` where previously the Wilson-Hilferty transformation did not always preserve the direction of the effect for one-sided tests. + +### 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. # tern 0.9.10 From c7c1ab12fb24b49875dbf3ef1732e88cb562b43d Mon Sep 17 00:00:00 2001 From: Daniel Sabanes Bove Date: Mon, 15 Jun 2026 11:57:02 +0800 Subject: [PATCH 04/13] add edge case test --- tests/testthat/_snaps/test_proportion_diff.md | 10 +++++++++ tests/testthat/test-test_proportion_diff.R | 22 +++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/tests/testthat/_snaps/test_proportion_diff.md b/tests/testthat/_snaps/test_proportion_diff.md index 176cd3ebf1..c6ff2a9bce 100644 --- a/tests/testthat/_snaps/test_proportion_diff.md +++ b/tests/testthat/_snaps/test_proportion_diff.md @@ -263,3 +263,13 @@ Variable Label p-value (Cochran-Mantel-Haenszel Test with Wilson-Hilferty Transformation) NA +# test_proportion_diff edge case: all responder by CMH with Sato variance estimator + + Code + res + Output + B A + ———————————————————————————————————————————————————————————————————————————————————— + Variable Label + p-value (Cochran-Mantel-Haenszel Test with Sato Variance Estimator) 1.0000 + diff --git a/tests/testthat/test-test_proportion_diff.R b/tests/testthat/test-test_proportion_diff.R index 73b4ffa4c9..d239e16012 100644 --- a/tests/testthat/test-test_proportion_diff.R +++ b/tests/testthat/test-test_proportion_diff.R @@ -448,3 +448,25 @@ testthat::test_that("test_proportion_diff edge case: all responder by CMH with W res <- testthat::expect_silent(result) testthat::expect_snapshot(res) }) + +testthat::test_that("test_proportion_diff edge case: all responder by CMH with Sato variance estimator", { + dta <- data.frame( + rsp = rep(TRUE, each = 100), + grp = factor(rep(c("A", "B"), each = 50)), + strata = factor(rep(c("V", "W", "X", "Y", "Z"), each = 20)) + ) + + result <- basic_table() %>% + split_cols_by(var = "grp", ref_group = "B", split_fun = ref_group_position("first")) %>% + test_proportion_diff( + vars = "rsp", + var_labels = "Variable Label", + show_labels = "visible", + method = "cmh_sato", + variables = list(strata = "strata") + ) %>% + build_table(df = dta) + + res <- testthat::expect_silent(result) + testthat::expect_snapshot(res) +}) From cb20233d3d1bcb978829b41b6ef1dc2a69ef537a Mon Sep 17 00:00:00 2001 From: Daniel Sabanes Bove Date: Mon, 15 Jun 2026 11:58:47 +0800 Subject: [PATCH 05/13] run styler --- R/coxph.R | 3 +-- R/h_step.R | 6 ++---- R/prop_diff.R | 22 +++++++++++----------- R/prop_diff_test.R | 4 ++-- R/utils_default_stats_formats_labels.R | 6 ++++-- tests/testthat/test-survival_time.R | 4 ++-- 6 files changed, 22 insertions(+), 23 deletions(-) diff --git a/R/coxph.R b/R/coxph.R index 5427f7eb8b..7f7cb6b495 100644 --- a/R/coxph.R +++ b/R/coxph.R @@ -195,8 +195,7 @@ try_car_anova <- function(mod, invokeRestart("muffleWarning") } ), - finally = { - } + finally = {} ) return(y) diff --git a/R/h_step.R b/R/h_step.R index e2ea4ae1d8..6ba8a314d7 100644 --- a/R/h_step.R +++ b/R/h_step.R @@ -171,8 +171,7 @@ h_step_survival_est <- function(formula, invokeRestart("muffleWarning") } ), - finally = { - } + finally = {} ) if (!is.null(coxph_warnings)) { warning(paste( @@ -285,8 +284,7 @@ h_step_rsp_est <- function(formula, invokeRestart("muffleWarning") } ), - finally = { - } + finally = {} ) if (!is.null(fit_warnings)) { warning(paste( diff --git a/R/prop_diff.R b/R/prop_diff.R index ba4aa187f1..f403c6bdc1 100644 --- a/R/prop_diff.R +++ b/R/prop_diff.R @@ -596,13 +596,13 @@ prop_diff_nc <- function(rsp, ) } -#' @describeIn h_prop_diff Helper function to calculate the CMH weighted +#' @describeIn h_prop_diff Helper function to calculate the CMH weighted #' difference in proportions. -#' -#' @param tbl (`array`)\cr 3-dimensional array with dimensions corresponding to -#' group, response, and strata. The second dimension (response) should have names +#' +#' @param tbl (`array`)\cr 3-dimensional array with dimensions corresponding to +#' group, response, and strata. The second dimension (response) should have names #' "TRUE" and "FALSE". -#' +#' #' @keywords internal h_diff_cmh <- function(tbl) { checkmate::assert_array(tbl) @@ -643,11 +643,11 @@ h_diff_cmh <- function(tbl) { ) } -#' @describeIn h_prop_diff Helper function to calculate the standard error for the +#' @describeIn h_prop_diff Helper function to calculate the standard error for the #' CMH weighted difference in proportions. -#' +#' #' @param cmh_results (`list`)\cr output of [h_diff_cmh()]. -#' +#' #' @keywords internal h_diff_cmh_se <- function(cmh_results, diff_se = c("standard", "sato")) { checkmate::assert_list(cmh_results, types = "numeric", any.missing = FALSE, names = "unique") @@ -659,8 +659,8 @@ h_diff_cmh_se <- function(cmh_results, diff_se = c("standard", "sato")) { sqrt(sum((terms1 + terms2) * l$wt_normalized^2)) } else { # Sato variance estimator. - p_terms_num <- l$n2^2 * l$x1 - - l$n1^2 * l$x2 + + p_terms_num <- l$n2^2 * l$x1 - + l$n1^2 * l$x2 + l$n1 * l$n2 * (l$n1 - l$n2) / 2 p_terms <- p_terms_num / (l$n1 + l$n2)^2 q_terms_num <- l$x1 * (l$n2 - l$x2) + l$x2 * (l$n1 - l$x1) @@ -723,7 +723,7 @@ prop_diff_cmh <- function(rsp, # first dimension: CONTROL, TX # 2nd dimension: TRUE, FALSE # 3rd dimension: levels of strata - # Note: rsp needs to be a factor to handle edge case of + # Note: rsp needs to be a factor to handle edge case of # no FALSE (or TRUE) rsp records. t_tbl <- table( grp, diff --git a/R/prop_diff_test.R b/R/prop_diff_test.R index ed75fb6705..04096ee02a 100644 --- a/R/prop_diff_test.R +++ b/R/prop_diff_test.R @@ -360,12 +360,12 @@ prop_cmh <- function(ary, if (transform == "wilson_hilferty") { if (diff_se == "sato") { warning( - "Wilson-Hilferty transformation was not designed ", + "Wilson-Hilferty transformation was not designed ", "for use with the Sato variance estimator" ) } chisq_stat <- z_stat^2 - df <- 1 # Because we only compare two groups. + df <- 1 # Because we only compare two groups. num <- (1 - 2 / (9 * df)) - (chisq_stat / df)^(1 / 3) denom <- sqrt(2 / (9 * df)) z_stat <- num / denom * sign(z_stat) # Preserve the direction of effect. diff --git a/R/utils_default_stats_formats_labels.R b/R/utils_default_stats_formats_labels.R index cbd0f885bc..c998ea4176 100644 --- a/R/utils_default_stats_formats_labels.R +++ b/R/utils_default_stats_formats_labels.R @@ -580,8 +580,10 @@ 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_se", "lsmean_ci", - "lsmean_diff", "lsmean_diff_ci", "lsmean_diff_with_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"), diff --git a/tests/testthat/test-survival_time.R b/tests/testthat/test-survival_time.R index ebaf7b5717..8da67ac970 100644 --- a/tests/testthat/test-survival_time.R +++ b/tests/testthat/test-survival_time.R @@ -223,8 +223,8 @@ testthat::test_that("s_surv_time range_with_cens_info flags upper censored (0, 1 ) 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::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)", { From 898552b464cf444d8881be6eba393609fb64ec65 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 15 Jun 2026 04:02:59 +0000 Subject: [PATCH 06/13] [skip roxygen] [skip vbump] Roxygen Man Pages Auto Update --- man/d_test_proportion_diff.Rd | 2 +- man/extreme_format.Rd | 1 + man/format_auto.Rd | 1 + man/format_count_fraction.Rd | 1 + man/format_count_fraction_fixed_dp.Rd | 1 + man/format_count_fraction_lt10.Rd | 1 + man/format_extreme_values.Rd | 1 + man/format_extreme_values_ci.Rd | 1 + man/format_fraction.Rd | 1 + man/format_fraction_fixed_dp.Rd | 1 + man/format_fraction_threshold.Rd | 1 + man/format_range_cens.Rd | 24 ++++++++++++------------ man/format_sigfig.Rd | 1 + man/format_xx.Rd | 1 + man/formatting_functions.Rd | 1 + man/h_prop_diff.Rd | 21 ++++++++++++++++++++- man/h_prop_diff_test.Rd | 3 +++ man/prop_diff_test.Rd | 6 +++--- 18 files changed, 52 insertions(+), 17 deletions(-) diff --git a/man/d_test_proportion_diff.Rd b/man/d_test_proportion_diff.Rd index eacf3efd93..5dd4f3ec59 100644 --- a/man/d_test_proportion_diff.Rd +++ b/man/d_test_proportion_diff.Rd @@ -7,7 +7,7 @@ d_test_proportion_diff(method, alternative = c("two.sided", "less", "greater")) } \arguments{ -\item{method}{(\code{string})\cr one of \code{chisq}, \code{cmh}, \code{cmh_wh}, \code{fisher}, or \code{schouten}; +\item{method}{(\code{string})\cr one of \code{chisq}, \code{cmh}, \code{cmh_sato}, \code{cmh_wh}, \code{fisher}, or \code{schouten}; specifies the test used to calculate the p-value.} \item{alternative}{(\code{string})\cr whether \code{two.sided}, or one-sided \code{less} or \code{greater} p-value diff --git a/man/extreme_format.Rd b/man/extreme_format.Rd index 5b20fc0b1a..0a036cb0f1 100644 --- a/man/extreme_format.Rd +++ b/man/extreme_format.Rd @@ -64,6 +64,7 @@ Other formatting functions: \code{\link[=format_fraction]{format_fraction()}}, \code{\link[=format_fraction_fixed_dp]{format_fraction_fixed_dp()}}, \code{\link[=format_fraction_threshold]{format_fraction_threshold()}}, +\code{\link[=format_range_cens]{format_range_cens()}}, \code{\link[=format_sigfig]{format_sigfig()}}, \code{\link[=format_xx]{format_xx()}}, \code{\link{formatting_functions}} diff --git a/man/format_auto.Rd b/man/format_auto.Rd index 70ed8f3e7a..849369549e 100644 --- a/man/format_auto.Rd +++ b/man/format_auto.Rd @@ -53,6 +53,7 @@ Other formatting functions: \code{\link[=format_fraction]{format_fraction()}}, \code{\link[=format_fraction_fixed_dp]{format_fraction_fixed_dp()}}, \code{\link[=format_fraction_threshold]{format_fraction_threshold()}}, +\code{\link[=format_range_cens]{format_range_cens()}}, \code{\link[=format_sigfig]{format_sigfig()}}, \code{\link[=format_xx]{format_xx()}}, \code{\link{formatting_functions}} diff --git a/man/format_count_fraction.Rd b/man/format_count_fraction.Rd index 57285437e2..bca0febb17 100644 --- a/man/format_count_fraction.Rd +++ b/man/format_count_fraction.Rd @@ -35,6 +35,7 @@ Other formatting functions: \code{\link[=format_fraction]{format_fraction()}}, \code{\link[=format_fraction_fixed_dp]{format_fraction_fixed_dp()}}, \code{\link[=format_fraction_threshold]{format_fraction_threshold()}}, +\code{\link[=format_range_cens]{format_range_cens()}}, \code{\link[=format_sigfig]{format_sigfig()}}, \code{\link[=format_xx]{format_xx()}}, \code{\link{formatting_functions}} diff --git a/man/format_count_fraction_fixed_dp.Rd b/man/format_count_fraction_fixed_dp.Rd index 2cb787bb40..5c2662de8f 100644 --- a/man/format_count_fraction_fixed_dp.Rd +++ b/man/format_count_fraction_fixed_dp.Rd @@ -36,6 +36,7 @@ Other formatting functions: \code{\link[=format_fraction]{format_fraction()}}, \code{\link[=format_fraction_fixed_dp]{format_fraction_fixed_dp()}}, \code{\link[=format_fraction_threshold]{format_fraction_threshold()}}, +\code{\link[=format_range_cens]{format_range_cens()}}, \code{\link[=format_sigfig]{format_sigfig()}}, \code{\link[=format_xx]{format_xx()}}, \code{\link{formatting_functions}} diff --git a/man/format_count_fraction_lt10.Rd b/man/format_count_fraction_lt10.Rd index 2e496677d9..3f481893d2 100644 --- a/man/format_count_fraction_lt10.Rd +++ b/man/format_count_fraction_lt10.Rd @@ -36,6 +36,7 @@ Other formatting functions: \code{\link[=format_fraction]{format_fraction()}}, \code{\link[=format_fraction_fixed_dp]{format_fraction_fixed_dp()}}, \code{\link[=format_fraction_threshold]{format_fraction_threshold()}}, +\code{\link[=format_range_cens]{format_range_cens()}}, \code{\link[=format_sigfig]{format_sigfig()}}, \code{\link[=format_xx]{format_xx()}}, \code{\link{formatting_functions}} diff --git a/man/format_extreme_values.Rd b/man/format_extreme_values.Rd index 8690f386d6..f04829db64 100644 --- a/man/format_extreme_values.Rd +++ b/man/format_extreme_values.Rd @@ -36,6 +36,7 @@ Other formatting functions: \code{\link[=format_fraction]{format_fraction()}}, \code{\link[=format_fraction_fixed_dp]{format_fraction_fixed_dp()}}, \code{\link[=format_fraction_threshold]{format_fraction_threshold()}}, +\code{\link[=format_range_cens]{format_range_cens()}}, \code{\link[=format_sigfig]{format_sigfig()}}, \code{\link[=format_xx]{format_xx()}}, \code{\link{formatting_functions}} diff --git a/man/format_extreme_values_ci.Rd b/man/format_extreme_values_ci.Rd index 6d09b79eee..4a53e33f26 100644 --- a/man/format_extreme_values_ci.Rd +++ b/man/format_extreme_values_ci.Rd @@ -36,6 +36,7 @@ Other formatting functions: \code{\link[=format_fraction]{format_fraction()}}, \code{\link[=format_fraction_fixed_dp]{format_fraction_fixed_dp()}}, \code{\link[=format_fraction_threshold]{format_fraction_threshold()}}, +\code{\link[=format_range_cens]{format_range_cens()}}, \code{\link[=format_sigfig]{format_sigfig()}}, \code{\link[=format_xx]{format_xx()}}, \code{\link{formatting_functions}} diff --git a/man/format_fraction.Rd b/man/format_fraction.Rd index 051461a628..970967bc09 100644 --- a/man/format_fraction.Rd +++ b/man/format_fraction.Rd @@ -35,6 +35,7 @@ Other formatting functions: \code{\link[=format_extreme_values_ci]{format_extreme_values_ci()}}, \code{\link[=format_fraction_fixed_dp]{format_fraction_fixed_dp()}}, \code{\link[=format_fraction_threshold]{format_fraction_threshold()}}, +\code{\link[=format_range_cens]{format_range_cens()}}, \code{\link[=format_sigfig]{format_sigfig()}}, \code{\link[=format_xx]{format_xx()}}, \code{\link{formatting_functions}} diff --git a/man/format_fraction_fixed_dp.Rd b/man/format_fraction_fixed_dp.Rd index 2c5c5eebef..7134efe7e8 100644 --- a/man/format_fraction_fixed_dp.Rd +++ b/man/format_fraction_fixed_dp.Rd @@ -37,6 +37,7 @@ Other formatting functions: \code{\link[=format_extreme_values_ci]{format_extreme_values_ci()}}, \code{\link[=format_fraction]{format_fraction()}}, \code{\link[=format_fraction_threshold]{format_fraction_threshold()}}, +\code{\link[=format_range_cens]{format_range_cens()}}, \code{\link[=format_sigfig]{format_sigfig()}}, \code{\link[=format_xx]{format_xx()}}, \code{\link{formatting_functions}} diff --git a/man/format_fraction_threshold.Rd b/man/format_fraction_threshold.Rd index 6d839f2ffe..f97a23e807 100644 --- a/man/format_fraction_threshold.Rd +++ b/man/format_fraction_threshold.Rd @@ -39,6 +39,7 @@ Other formatting functions: \code{\link[=format_extreme_values_ci]{format_extreme_values_ci()}}, \code{\link[=format_fraction]{format_fraction()}}, \code{\link[=format_fraction_fixed_dp]{format_fraction_fixed_dp()}}, +\code{\link[=format_range_cens]{format_range_cens()}}, \code{\link[=format_sigfig]{format_sigfig()}}, \code{\link[=format_xx]{format_xx()}}, \code{\link{formatting_functions}} diff --git a/man/format_range_cens.Rd b/man/format_range_cens.Rd index d5e4c15d3d..9ac1c224b7 100644 --- a/man/format_range_cens.Rd +++ b/man/format_range_cens.Rd @@ -28,19 +28,19 @@ fmt(c(1.23, 9.87, 0, 0)) } \seealso{ -Other formatting functions: +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[=format_auto]{format_auto()}}, +\code{\link[=format_count_fraction]{format_count_fraction()}}, +\code{\link[=format_count_fraction_fixed_dp]{format_count_fraction_fixed_dp()}}, +\code{\link[=format_count_fraction_lt10]{format_count_fraction_lt10()}}, +\code{\link[=format_extreme_values]{format_extreme_values()}}, +\code{\link[=format_extreme_values_ci]{format_extreme_values_ci()}}, +\code{\link[=format_fraction]{format_fraction()}}, +\code{\link[=format_fraction_fixed_dp]{format_fraction_fixed_dp()}}, +\code{\link[=format_fraction_threshold]{format_fraction_threshold()}}, +\code{\link[=format_sigfig]{format_sigfig()}}, +\code{\link[=format_xx]{format_xx()}}, \code{\link{formatting_functions}} } \concept{formatting functions} diff --git a/man/format_sigfig.Rd b/man/format_sigfig.Rd index 3ea1ef6975..429ac42c29 100644 --- a/man/format_sigfig.Rd +++ b/man/format_sigfig.Rd @@ -46,6 +46,7 @@ Other formatting functions: \code{\link[=format_fraction]{format_fraction()}}, \code{\link[=format_fraction_fixed_dp]{format_fraction_fixed_dp()}}, \code{\link[=format_fraction_threshold]{format_fraction_threshold()}}, +\code{\link[=format_range_cens]{format_range_cens()}}, \code{\link[=format_xx]{format_xx()}}, \code{\link{formatting_functions}} } diff --git a/man/format_xx.Rd b/man/format_xx.Rd index b44eb9faed..08f114d449 100644 --- a/man/format_xx.Rd +++ b/man/format_xx.Rd @@ -41,6 +41,7 @@ Other formatting functions: \code{\link[=format_fraction]{format_fraction()}}, \code{\link[=format_fraction_fixed_dp]{format_fraction_fixed_dp()}}, \code{\link[=format_fraction_threshold]{format_fraction_threshold()}}, +\code{\link[=format_range_cens]{format_range_cens()}}, \code{\link[=format_sigfig]{format_sigfig()}}, \code{\link{formatting_functions}} } diff --git a/man/formatting_functions.Rd b/man/formatting_functions.Rd index afc526007c..99f7bed810 100644 --- a/man/formatting_functions.Rd +++ b/man/formatting_functions.Rd @@ -22,6 +22,7 @@ Other formatting functions: \code{\link[=format_fraction]{format_fraction()}}, \code{\link[=format_fraction_fixed_dp]{format_fraction_fixed_dp()}}, \code{\link[=format_fraction_threshold]{format_fraction_threshold()}}, +\code{\link[=format_range_cens]{format_range_cens()}}, \code{\link[=format_sigfig]{format_sigfig()}}, \code{\link[=format_xx]{format_xx()}} } diff --git a/man/h_prop_diff.Rd b/man/h_prop_diff.Rd index a162de655a..343ee80f90 100644 --- a/man/h_prop_diff.Rd +++ b/man/h_prop_diff.Rd @@ -5,6 +5,8 @@ \alias{prop_diff_wald} \alias{prop_diff_ha} \alias{prop_diff_nc} +\alias{h_diff_cmh} +\alias{h_diff_cmh_se} \alias{prop_diff_cmh} \alias{prop_diff_strat_nc} \alias{prop_diff_uncond_exact} @@ -16,6 +18,10 @@ prop_diff_ha(rsp, grp, conf_level) prop_diff_nc(rsp, grp, conf_level, correct = FALSE) +h_diff_cmh(tbl) + +h_diff_cmh_se(cmh_results, diff_se = c("standard", "sato")) + prop_diff_cmh( rsp, grp, @@ -46,12 +52,18 @@ prop_diff_uncond_exact(rsp, grp, conf_level = 0.95) \item{correct}{(\code{flag})\cr whether to include the continuity correction. For further information, see \code{\link[stats:prop.test]{stats::prop.test()}}.} -\item{strata}{(\code{factor})\cr variable with one level per stratum and same length as \code{rsp}.} +\item{tbl}{(\code{array})\cr 3-dimensional array with dimensions corresponding to +group, response, and strata. The second dimension (response) should have names +"TRUE" and "FALSE".} + +\item{cmh_results}{(\code{list})\cr output of \code{\link[=h_diff_cmh]{h_diff_cmh()}}.} \item{diff_se}{(\code{string})\cr method to estimate the standard error for the difference, either \code{standard}, \code{sato} \insertCite{Sato1989}{tern} or \code{miettinen_nurminen} \insertCite{MiettinenNurminen1985}{tern}.} +\item{strata}{(\code{factor})\cr variable with one level per stratum and same length as \code{rsp}.} + \item{weights_method}{(\code{string})\cr weights method. Can be either \code{"cmh"} or \code{"heuristic"} and directs the way weights are estimated.} } @@ -74,6 +86,12 @@ interval. \item \code{prop_diff_nc()}: Newcombe confidence interval. It is based on the Wilson score confidence interval for a single binomial proportion \insertCite{Newcombe1998}{tern}. +\item \code{h_diff_cmh()}: Helper function to calculate the CMH weighted +difference in proportions. + +\item \code{h_diff_cmh_se()}: Helper function to calculate the standard error for the +CMH weighted difference in proportions. + \item \code{prop_diff_cmh()}: Calculates the weighted difference. This is defined as the difference in response rates between the experimental treatment group and the control treatment group, adjusted for stratification factors by applying Cochran-Mantel-Haenszel (CMH) weights. For the CMH chi-squared @@ -184,3 +202,4 @@ prop_diff_uncond_exact(rsp = rsp, grp = grp, conf_level = 0.95) \seealso{ \code{\link[=prop_diff]{prop_diff()}} for implementation of these helper functions. } +\keyword{internal} diff --git a/man/h_prop_diff_test.Rd b/man/h_prop_diff_test.Rd index 6515ab33f5..66f3fea04a 100644 --- a/man/h_prop_diff_test.Rd +++ b/man/h_prop_diff_test.Rd @@ -13,6 +13,7 @@ prop_chisq(tbl, alternative = c("two.sided", "less", "greater")) prop_cmh( ary, alternative = c("two.sided", "less", "greater"), + diff_se = c("standard", "sato"), transform = c("none", "wilson_hilferty") ) @@ -29,6 +30,8 @@ should be displayed.} \item{ary}{(\code{array}, 3 dimensions)\cr array with two groups in rows, the binary response (\code{TRUE}/\code{FALSE}) in columns, and the strata in the third dimension.} +\item{diff_se}{(\code{string})\cr either \code{standard} or \code{sato}; specifies whether to use the Sato variance estimator to calculate the chi-squared statistic.} + \item{transform}{(\code{string})\cr either \code{none} or \code{wilson_hilferty}; specifies whether to apply the Wilson-Hilferty transformation of the chi-squared statistic.} } diff --git a/man/prop_diff_test.Rd b/man/prop_diff_test.Rd index 20f43b1ada..84d22fef6e 100644 --- a/man/prop_diff_test.Rd +++ b/man/prop_diff_test.Rd @@ -11,7 +11,7 @@ test_proportion_diff( lyt, vars, variables = list(strata = NULL), - method = c("chisq", "schouten", "fisher", "cmh", "cmh_wh"), + method = c("chisq", "schouten", "fisher", "cmh", "cmh_sato", "cmh_wh"), alternative = c("two.sided", "less", "greater"), var_labels = vars, na_str = default_na_str(), @@ -34,7 +34,7 @@ s_test_proportion_diff( .ref_group, .in_ref_col, variables = list(strata = NULL), - method = c("chisq", "schouten", "fisher", "cmh", "cmh_wh"), + method = c("chisq", "schouten", "fisher", "cmh", "cmh_sato", "cmh_wh"), alternative = c("two.sided", "less", "greater"), ... ) @@ -56,7 +56,7 @@ a_test_proportion_diff( \item{variables}{(named \code{list} of \code{string})\cr list of additional analysis variables.} -\item{method}{(\code{string})\cr one of \code{chisq}, \code{cmh}, \code{cmh_wh}, \code{fisher}, or \code{schouten}; +\item{method}{(\code{string})\cr one of \code{chisq}, \code{cmh}, \code{cmh_sato}, \code{cmh_wh}, \code{fisher}, or \code{schouten}; specifies the test used to calculate the p-value.} \item{alternative}{(\code{string})\cr whether \code{two.sided}, or one-sided \code{less} or \code{greater} p-value From 57b55fef9364398b88b25251d54be2240887a572 Mon Sep 17 00:00:00 2001 From: Daniel Sabanes Bove Date: Mon, 15 Jun 2026 12:03:33 +0800 Subject: [PATCH 07/13] trigger checks From 2f29c05109a0092a4fdcf03634506c154c8e2896 Mon Sep 17 00:00:00 2001 From: Daniel Sabanes Bove Date: Mon, 15 Jun 2026 13:18:49 +0800 Subject: [PATCH 08/13] fix typo --- tests/testthat/_snaps/test_proportion_diff.md | 2 +- tests/testthat/test-test_proportion_diff.R | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/testthat/_snaps/test_proportion_diff.md b/tests/testthat/_snaps/test_proportion_diff.md index c6ff2a9bce..9b3cf7690b 100644 --- a/tests/testthat/_snaps/test_proportion_diff.md +++ b/tests/testthat/_snaps/test_proportion_diff.md @@ -147,7 +147,7 @@ # prop_cmh with Sato variance estimator and Wilson-Hilferty transformation works - 0.383328522648178 + 0.0660308573944943 # s_test_proportion_diff and d_test_proportion_diff return right result diff --git a/tests/testthat/test-test_proportion_diff.R b/tests/testthat/test-test_proportion_diff.R index d239e16012..2dba4aa670 100644 --- a/tests/testthat/test-test_proportion_diff.R +++ b/tests/testthat/test-test_proportion_diff.R @@ -253,7 +253,7 @@ testthat::test_that("prop_cmh with Sato variance estimator and Wilson-Hilferty t result <- prop_cmh(tbl, diff_se = "sato", transform = "wilson_hilferty"), "not designed for use with the Sato variance estimator" ) - testthat::expect_snapshot_value(res, style = "deparse", tolerance = 1e-3) + testthat::expect_snapshot_value(result, style = "deparse", tolerance = 1e-3) }) testthat::test_that("s_test_proportion_diff and d_test_proportion_diff return right result", { From 4ebed4cba12b4ec609d22cba3e97e5e5a85f8aa4 Mon Sep 17 00:00:00 2001 From: Daniel Sabanes Bove Date: Mon, 15 Jun 2026 13:44:06 +0800 Subject: [PATCH 09/13] fix linter issues --- .lintr | 1 - R/coxph.R | 17 ++++++++--------- R/prop_diff_test.R | 3 ++- man/h_prop_diff_test.Rd | 3 ++- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.lintr b/.lintr index b0b8c224d3..e6e3e9d487 100644 --- a/.lintr +++ b/.lintr @@ -1,6 +1,5 @@ linters: linters_with_defaults( line_length_linter = line_length_linter(120), - cyclocomp_linter = NULL, object_usage_linter = NULL, object_length_linter = NULL, pipe_consistency_linter = NULL diff --git a/R/coxph.R b/R/coxph.R index 7f7cb6b495..e971353b49 100644 --- a/R/coxph.R +++ b/R/coxph.R @@ -25,8 +25,7 @@ univariate <- function(x) { # Get the right-hand-term of a formula rht <- function(x) { checkmate::assert_formula(x) - y <- as.character(rev(x)[[1]]) - return(y) + as.character(rev(x)[[1]]) } #' Hazard ratio estimation in interactions @@ -109,7 +108,7 @@ estimate_coef <- function(variable, given, design_mat <- apply( X = design_mat, MARGIN = 1, FUN = function(x) { mmat[names(mmat) %in% x[-which(names(x) == "given")]] <- 1 - return(mmat) + mmat } ) colnames(design_mat) <- interaction_names @@ -124,7 +123,7 @@ estimate_coef <- function(variable, given, y <- vcov[vcov_el, vcov_el] y <- sum(y) y <- sqrt(y) - return(y) + y }) q_norm <- stats::qnorm((1 + conf_level) / 2) @@ -135,7 +134,7 @@ estimate_coef <- function(variable, given, x["lcl"] <- exp(x["coef"] - q_norm * x["se(coef)"]) x["ucl"] <- exp(x["coef"] + q_norm * x["se(coef)"]) - return(x) + x }) y <- t(y) @@ -147,7 +146,7 @@ estimate_coef <- function(variable, given, " hazard ratio given the level of ", given, " compared to ", variable, " level ", lvl_var[1], "." ) - return(y) + y } #' `tryCatch` around `car::Anova` @@ -198,7 +197,7 @@ try_car_anova <- function(mod, finally = {} ) - return(y) + y } #' Fit a Cox regression model and ANOVA @@ -240,7 +239,7 @@ fit_n_aov <- function(formula, y <- list(mod = mod, msum = msum, aov = aov) attr(y, "message") <- warn_attr - return(y) + y } # argument_checks @@ -265,7 +264,7 @@ name_covariate_names <- function(covariates) { no_names <- is.null(names(covariates)) if (any(miss_names)) names(covariates)[miss_names] <- vapply(covariates[miss_names], FUN = rht, FUN.VALUE = "name") if (no_names) names(covariates) <- vapply(covariates, FUN = rht, FUN.VALUE = "name") - return(covariates) + covariates } check_increments <- function(increments, covariates) { diff --git a/R/prop_diff_test.R b/R/prop_diff_test.R index 04096ee02a..5ab95e4533 100644 --- a/R/prop_diff_test.R +++ b/R/prop_diff_test.R @@ -324,7 +324,8 @@ prop_chisq <- function(tbl, alternative = c("two.sided", "less", "greater")) { #' #' @param ary (`array`, 3 dimensions)\cr array with two groups in rows, the binary response #' (`TRUE`/`FALSE`) in columns, and the strata in the third dimension. -#' @param diff_se (`string`)\cr either `standard` or `sato`; specifies whether to use the Sato variance estimator to calculate the chi-squared statistic. +#' @param diff_se (`string`)\cr either `standard` or `sato`; specifies whether to use the Sato +#' variance estimator to calculate the chi-squared statistic. #' @param transform (`string`)\cr either `none` or `wilson_hilferty`; specifies whether to apply #' the Wilson-Hilferty transformation of the chi-squared statistic. #' diff --git a/man/h_prop_diff_test.Rd b/man/h_prop_diff_test.Rd index 66f3fea04a..eed793efa6 100644 --- a/man/h_prop_diff_test.Rd +++ b/man/h_prop_diff_test.Rd @@ -30,7 +30,8 @@ should be displayed.} \item{ary}{(\code{array}, 3 dimensions)\cr array with two groups in rows, the binary response (\code{TRUE}/\code{FALSE}) in columns, and the strata in the third dimension.} -\item{diff_se}{(\code{string})\cr either \code{standard} or \code{sato}; specifies whether to use the Sato variance estimator to calculate the chi-squared statistic.} +\item{diff_se}{(\code{string})\cr either \code{standard} or \code{sato}; specifies whether to use the Sato +variance estimator to calculate the chi-squared statistic.} \item{transform}{(\code{string})\cr either \code{none} or \code{wilson_hilferty}; specifies whether to apply the Wilson-Hilferty transformation of the chi-squared statistic.} From 1b0aa895dbb50d9e4c0ee3c6f7c50da551d99c15 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 15 Jun 2026 05:46:59 +0000 Subject: [PATCH 10/13] [skip style] [skip vbump] Restyle files --- R/prop_diff_test.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/R/prop_diff_test.R b/R/prop_diff_test.R index 5ab95e4533..c145b404f3 100644 --- a/R/prop_diff_test.R +++ b/R/prop_diff_test.R @@ -324,7 +324,7 @@ prop_chisq <- function(tbl, alternative = c("two.sided", "less", "greater")) { #' #' @param ary (`array`, 3 dimensions)\cr array with two groups in rows, the binary response #' (`TRUE`/`FALSE`) in columns, and the strata in the third dimension. -#' @param diff_se (`string`)\cr either `standard` or `sato`; specifies whether to use the Sato +#' @param diff_se (`string`)\cr either `standard` or `sato`; specifies whether to use the Sato #' variance estimator to calculate the chi-squared statistic. #' @param transform (`string`)\cr either `none` or `wilson_hilferty`; specifies whether to apply #' the Wilson-Hilferty transformation of the chi-squared statistic. From a6b91eece45a624d02201628bb0b7eaf37a1909a Mon Sep 17 00:00:00 2001 From: Daniel Sabanes Bove Date: Mon, 15 Jun 2026 13:47:31 +0800 Subject: [PATCH 11/13] trigger checks From efe34aa1a4ee45b0fbd0cfbdb763333c9facc345 Mon Sep 17 00:00:00 2001 From: Daniel Sabanes Bove Date: Tue, 16 Jun 2026 10:26:11 +0800 Subject: [PATCH 12/13] polish news as suggested and add comment for sign derivation --- NEWS.md | 6 +++--- R/prop_diff_test.R | 5 ++++- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/NEWS.md b/NEWS.md index cc12a22411..ed5ebe6fcf 100644 --- a/NEWS.md +++ b/NEWS.md @@ -4,18 +4,18 @@ * 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 `cmh_sato` as option for `test_proportion_diff()` to enable CMH test with Sato variance estimator. +* Added `cmh_sato` method to `test_proportion_diff()` for CMH testing with the Sato variance estimator. (#1482) * 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. -* Fixed bug in `prop_cmh()` where previously the Wilson-Hilferty transformation did not always preserve the direction of the effect for one-sided tests. +* Fixed one-sided p-values in `prop_cmh()` with Wilson-Hilferty transformation — the sign of the effect was lost, producing incorrect p-values for `alternative = "less"` and `alternative = "greater"`. ### 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. +* Converted `s_surv_time()` to exported functions. # tern 0.9.10 diff --git a/R/prop_diff_test.R b/R/prop_diff_test.R index c145b404f3..0f9589cd75 100644 --- a/R/prop_diff_test.R +++ b/R/prop_diff_test.R @@ -350,7 +350,10 @@ prop_cmh <- function(ary, z_stat <- if (diff_se == "standard") { mh_res <- stats::mantelhaen.test(ary, correct = FALSE, alternative = alternative) checkmate::assert_true(mh_res$parameter == 1) - sqrt(unname(mh_res$statistic)) * ifelse(unname(mh_res$estimate) < 1, 1, -1) + # Note: The odds ratio (OR) estimate from `mantelhaen.test` and the proportion difference + # always agree in direction, therefore we can use the OR estimate to determine the sign here. + stat_sign <- ifelse(mh_res$estimate < 1, 1, -1) + sqrt(unname(mh_res$statistic)) * stat_sign } else { # Use the Sato variance estimator. cmh <- h_diff_cmh(ary) From 66371ece8056422b6e2fa27122958bbc78043271 Mon Sep 17 00:00:00 2001 From: Daniel Sabanes Bove Date: Tue, 16 Jun 2026 11:21:06 +0800 Subject: [PATCH 13/13] fix with unname --- R/prop_diff_test.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/R/prop_diff_test.R b/R/prop_diff_test.R index 0f9589cd75..968176c74e 100644 --- a/R/prop_diff_test.R +++ b/R/prop_diff_test.R @@ -352,7 +352,7 @@ prop_cmh <- function(ary, checkmate::assert_true(mh_res$parameter == 1) # Note: The odds ratio (OR) estimate from `mantelhaen.test` and the proportion difference # always agree in direction, therefore we can use the OR estimate to determine the sign here. - stat_sign <- ifelse(mh_res$estimate < 1, 1, -1) + stat_sign <- ifelse(unname(mh_res$estimate) < 1, 1, -1) sqrt(unname(mh_res$statistic)) * stat_sign } else { # Use the Sato variance estimator.