Skip to content

Commit edcd6ba

Browse files
danielinteractivemunoztd0Copilot
committed
Add multiple imputation functions for response output (#408)
* add draft imputation functions * add pval category functions * add pooling functions * add imputation/analysis functions * add to NEWS, refresh docs * fix assertion * ignore .Renviron * update snapshot * Update WORDLIST Hilferty * rewrite namespace with roxygen 8.0.0 --------- Co-authored-by: David Muñoz Tord <david.munoz@mailbox.org> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Co-authored-by: munoztd0 <dtord@its.jnj.com>
1 parent a6180d3 commit edcd6ba

17 files changed

Lines changed: 838 additions & 0 deletions

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
.RData
55
.Ruserdata
66
*.Rproj
7+
.Renviron
78

89
# docs
910
inst/doc

NAMESPACE

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ export(bspt_pruner)
3939
export(build_formula)
4040
export(c_proportion_logical)
4141
export(c_summary_subset_label)
42+
export(categorize_pval)
4243
export(check_wrap_nobreak)
4344
export(cmp_cfun)
4445
export(cmp_split_fun)
@@ -99,6 +100,8 @@ export(no_data_to_report_str)
99100
export(or_clogit_j)
100101
export(or_cmh)
101102
export(or_glm_j)
103+
export(pool_rubin_scalar)
104+
export(pool_z_stat)
102105
export(postfun_eq5d)
103106
export(prepend_label_cell)
104107
export(prop_ratio_cmh)
@@ -117,6 +120,7 @@ export(resp01_a_comp_stat_logical)
117120
export(resp01_acfun)
118121
export(resp01_counts_cfun)
119122
export(resp01_split_fun_fct)
123+
export(resp_multiple_imputation)
120124
export(response_by_var)
121125
export(rightside)
122126
export(rm_levels)

NEWS.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@
3434
- update documentation to `roxygen2` 8.0.0
3535

3636
### Added
37+
- Added `categorize_pval()` for assigning p-values to validated, user-defined categories.
38+
- Added `pool_rubin_scalar()` and `pool_z_stat()` for pooling scalar estimates and z statistics across imputations.
39+
- Added `resp_multiple_imputation()` to impute missing binary responses across scenarios and pool CMH risk-difference and p-value results.
3740
- Updated documentation and examples for `label_map` in `a_freq_j` (#235)
3841
- Added `tern` methods for difference in proportions in `a_freq_j` and `a_freq_resp_var_j` : `cmh_sato`, `cmh_mn`, `uncond_exact_diff` (#389)
3942
- Added `a_summary_j_with_exclude()` to allow `tern::a_summary()` analyses to be skipped for selected row split levels.

R/pool_funs.R

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
#' Pool a scalar estimate across imputations
2+
#'
3+
#' Applies Rubin's rules to combine scalar estimates and their
4+
#' within-imputation variance estimates.
5+
#'
6+
#' @details For `m` imputations, the pooled estimate is the mean of `q`. The
7+
#' pooled variance combines the mean within-imputation variance with the
8+
#' between-imputation variance, using the usual `(1 + 1 / m)` multiplier.
9+
#' The returned standard error is the square root of the pooled variance.
10+
#'
11+
#' @param q (`numeric`)
12+
#' Vector of scalar estimates, with one value for each imputed dataset.
13+
#' @param u (`numeric`)
14+
#' Vector of within-imputation variance estimates corresponding to `q`.
15+
#'
16+
#' @return A named `list` with the pooled estimate (`est`), standard error
17+
#' (`se`), variance (`var`), and number of imputations (`m`).
18+
#'
19+
#' @seealso [pool_z_stat()]
20+
#' @export
21+
#'
22+
#' @examples
23+
#' pool_rubin_scalar(
24+
#' q = c(0.20, 0.25, 0.15),
25+
#' u = c(0.01, 0.016, 0.012)
26+
#' )
27+
pool_rubin_scalar <- function(q, u) {
28+
# q: vector of estimates
29+
# u: vector of within-imputation variances
30+
m <- length(q)
31+
qbar <- mean(q, na.rm = TRUE)
32+
ubar <- mean(u, na.rm = TRUE)
33+
b <- stats::var(q, na.rm = TRUE)
34+
tvar <- ubar + (1 + 1 / m) * b
35+
se <- sqrt(tvar)
36+
list(est = qbar, se = se, var = tvar, m = m)
37+
}
38+
39+
#' Pool z statistics across imputations
40+
#'
41+
#' Combines z statistics from multiple imputed datasets and calculates a
42+
#' two-sided p-value for the pooled statistic.
43+
#'
44+
#' @details The within-imputation variance of a z statistic is approximately 1.
45+
#' The between-imputation variance is pooled using the same
46+
#' `(1 + 1 / m)` multiplier as [pool_rubin_scalar()]. The returned p-value is
47+
#' calculated from the standard normal distribution.
48+
#'
49+
#' @param z_stat_vals (`numeric`)
50+
#' Vector of z statistics, with one value for each imputed dataset.
51+
#'
52+
#' @return A named `list` with the pooled z statistic (`z`) and its two-sided
53+
#' p-value (`p`).
54+
#'
55+
#' @seealso [pool_rubin_scalar()]
56+
#' @export
57+
#'
58+
#' @examples
59+
#' pool_z_stat(c(1.1, 1.3, 1.2))
60+
pool_z_stat <- function(z_stat_vals) {
61+
m <- length(z_stat_vals)
62+
qbar <- mean(z_stat_vals, na.rm = TRUE)
63+
b <- stats::var(z_stat_vals, na.rm = TRUE)
64+
ubar <- 1 # approx var(z) ~ 1 because it is a z statistic
65+
tvar <- ubar + (1 + 1 / m) * b
66+
z_pool <- qbar / sqrt(tvar)
67+
p_pool <- 2 * stats::pnorm(abs(z_pool), lower.tail = FALSE)
68+
list(z = z_pool, p = p_pool)
69+
}

R/pval_cat.R

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
#' Helper function to normalize p-value categories
2+
#'
3+
#' @description Converts a named list of p-value category bounds into a matrix
4+
#' and retains the category labels in their supplied order.
5+
#'
6+
#' @param pvalcat (`named list`)\cr A non-empty named list. Each element must be
7+
#' a numeric vector of length two specifying the lower and upper bounds of a
8+
#' p-value category.
9+
#'
10+
#' @return A named `list` with the following elements:
11+
#'
12+
#' * `bounds`: a two-column numeric matrix of lower and upper category bounds.
13+
#' * `cats`: a character vector of category labels.
14+
#'
15+
#' @keywords internal
16+
h_normalize_pvalcat <- function(pvalcat) {
17+
checkmate::assert_list(pvalcat, min.len = 1, names = "unique")
18+
checkmate::assert_character(names(pvalcat), any.missing = FALSE, unique = TRUE)
19+
checkmate::assert_true(all(nzchar(names(pvalcat))))
20+
21+
bounds <- lapply(pvalcat, function(x) {
22+
checkmate::assert_numeric(x, len = 2, any.missing = FALSE, finite = TRUE)
23+
checkmate::assert_true(x[1] <= x[2])
24+
x
25+
})
26+
27+
list(
28+
bounds = matrix(unlist(bounds, use.names = FALSE), ncol = 2, byrow = TRUE),
29+
cats = names(pvalcat)
30+
)
31+
}
32+
33+
#' Categorize p-values
34+
#'
35+
#' @description Assigns each p-value to a category defined by a named list of
36+
#' lower and upper bounds.
37+
#'
38+
#' @details Categories are evaluated in the order supplied by `pvalcat`. Bounds
39+
#' are lower-inclusive and upper-exclusive, except that the upper bound of
40+
#' the final category is inclusive. P-values that do not fall in a category,
41+
#' including missing values, are returned as `NA_character_`.
42+
#'
43+
#' @param p (`numeric`)\cr A vector of p-values to categorize. Missing values are
44+
#' permitted.
45+
#' @param pvalcat (`named list`)\cr A non-empty named list of p-value category
46+
#' bounds. See [h_normalize_pvalcat()] for the required structure.
47+
#'
48+
#' @return A character vector of category labels, with one element per value in
49+
#' `p`.
50+
#'
51+
#' @examples
52+
#' pvalcat <- list(
53+
#' "<0.001" = c(0, 0.001),
54+
#' "0.001 to <0.05" = c(0.001, 0.05),
55+
#' ">=0.05" = c(0.05, 1)
56+
#' )
57+
#'
58+
#' categorize_pval(c(0, 0.001, 0.049, 0.05, 1, NA), pvalcat)
59+
#' @export
60+
categorize_pval <- function(p, pvalcat) {
61+
checkmate::assert_numeric(p, any.missing = TRUE)
62+
checkmate::assert_list(pvalcat, min.len = 1, names = "unique")
63+
64+
info <- h_normalize_pvalcat(pvalcat)
65+
bounds <- info$bounds
66+
cats <- info$cats
67+
last_row <- nrow(bounds)
68+
69+
vapply(
70+
p,
71+
function(x) {
72+
if (is.na(x)) {
73+
return(NA_character_)
74+
}
75+
76+
idx <- which(
77+
bounds[, 1] <= x &
78+
(x < bounds[, 2] | (seq_len(last_row) == last_row & x <= bounds[, 2]))
79+
)
80+
if (length(idx) == 0) {
81+
return(NA_character_)
82+
}
83+
cats[idx[1]]
84+
},
85+
FUN.VALUE = character(1)
86+
)
87+
}

0 commit comments

Comments
 (0)