1 |
#' Missing Data |
|
2 |
#' |
|
3 |
#' @description `r lifecycle::badge("stable")` |
|
4 |
#' |
|
5 |
#' Substitute missing data with a string or factor level. |
|
6 |
#' |
|
7 |
#' @param x (`factor` or `character` vector)\cr values for which any missing values should be substituted. |
|
8 |
#' @param label (`character`)\cr string that missing data should be replaced with. |
|
9 |
#' |
|
10 |
#' @return `x` with any `NA` values substituted by `label`. |
|
11 |
#' |
|
12 |
#' @examples |
|
13 |
#' explicit_na(c(NA, "a", "b")) |
|
14 |
#' is.na(explicit_na(c(NA, "a", "b"))) |
|
15 |
#' |
|
16 |
#' explicit_na(factor(c(NA, "a", "b"))) |
|
17 |
#' is.na(explicit_na(factor(c(NA, "a", "b")))) |
|
18 |
#' |
|
19 |
#' explicit_na(sas_na(c("a", ""))) |
|
20 |
#' |
|
21 |
#' @export |
|
22 |
explicit_na <- function(x, label = "<Missing>") { |
|
23 | 406x |
checkmate::assert_string(label) |
24 | ||
25 | 406x |
if (is.factor(x)) { |
26 | 304x |
x <- forcats::fct_na_value_to_level(x, label) |
27 | 304x |
forcats::fct_drop(x, only = label) |
28 | 102x |
} else if (is.character(x)) { |
29 | 102x |
x[is.na(x)] <- label |
30 | 102x |
x |
31 |
} else { |
|
32 | ! |
stop("only factors and character vectors allowed") |
33 |
} |
|
34 |
} |
|
35 | ||
36 |
#' Convert Strings to `NA` |
|
37 |
#' |
|
38 |
#' @description `r lifecycle::badge("stable")` |
|
39 |
#' |
|
40 |
#' SAS imports missing data as empty strings or strings with whitespaces only. This helper function can be used to |
|
41 |
#' convert these values to `NA`s. |
|
42 |
#' |
|
43 |
#' @inheritParams explicit_na |
|
44 |
#' @param empty (`logical`)\cr if `TRUE` empty strings get replaced by `NA`. |
|
45 |
#' @param whitespaces (`logical`)\cr if `TRUE` then strings made from whitespaces only get replaced with `NA`. |
|
46 |
#' |
|
47 |
#' @return `x` with `""` and/or whitespace-only values substituted by `NA`, depending on the values of |
|
48 |
#' `empty` and `whitespaces`. |
|
49 |
#' |
|
50 |
#' @examples |
|
51 |
#' sas_na(c("1", "", " ", " ", "b")) |
|
52 |
#' sas_na(factor(c("", " ", "b"))) |
|
53 |
#' |
|
54 |
#' is.na(sas_na(c("1", "", " ", " ", "b"))) |
|
55 |
#' |
|
56 |
#' @export |
|
57 |
sas_na <- function(x, empty = TRUE, whitespaces = TRUE) { |
|
58 | 407x |
checkmate::assert_flag(empty) |
59 | 407x |
checkmate::assert_flag(whitespaces) |
60 | ||
61 | 407x |
if (is.factor(x)) { |
62 | 301x |
empty_levels <- levels(x) == "" |
63 | 11x |
if (empty && any(empty_levels)) levels(x)[empty_levels] <- NA |
64 | ||
65 | 301x |
ws_levels <- grepl("^\\s+$", levels(x)) |
66 | ! |
if (whitespaces && any(ws_levels)) levels(x)[ws_levels] <- NA |
67 | ||
68 | 301x |
x |
69 | 106x |
} else if (is.character(x)) { |
70 | 106x |
if (empty) x[x == ""] <- NA_character_ |
71 | ||
72 | 106x |
if (whitespaces) x[grepl("^\\s+$", x)] <- NA_character_ |
73 | ||
74 | 106x |
x |
75 |
} else { |
|
76 | ! |
stop("only factors and character vectors allowed") |
77 |
} |
|
78 |
} |
1 |
#' Cox Proportional Hazards Regression |
|
2 |
#' |
|
3 |
#' @description `r lifecycle::badge("stable")` |
|
4 |
#' |
|
5 |
#' Fits a Cox regression model and estimates hazard ratio to describe the effect size in a survival analysis. |
|
6 |
#' |
|
7 |
#' @inheritParams argument_convention |
|
8 |
#' |
|
9 |
#' @details Cox models are the most commonly used methods to estimate the magnitude of |
|
10 |
#' the effect in survival analysis. It assumes proportional hazards: the ratio |
|
11 |
#' of the hazards between groups (e.g., two arms) is constant over time. |
|
12 |
#' This ratio is referred to as the "hazard ratio" (HR) and is one of the |
|
13 |
#' most commonly reported metrics to describe the effect size in survival |
|
14 |
#' analysis (NEST Team, 2020). |
|
15 |
#' |
|
16 |
#' @seealso [fit_coxreg] for relevant fitting functions, [h_cox_regression] for relevant |
|
17 |
#' helper functions, and [tidy_coxreg] for custom tidy methods. |
|
18 |
#' |
|
19 |
#' @examples |
|
20 |
#' library(survival) |
|
21 |
#' |
|
22 |
#' # Testing dataset [survival::bladder]. |
|
23 |
#' set.seed(1, kind = "Mersenne-Twister") |
|
24 |
#' dta_bladder <- with( |
|
25 |
#' data = bladder[bladder$enum < 5, ], |
|
26 |
#' tibble::tibble( |
|
27 |
#' TIME = stop, |
|
28 |
#' STATUS = event, |
|
29 |
#' ARM = as.factor(rx), |
|
30 |
#' COVAR1 = as.factor(enum) %>% formatters::with_label("A Covariate Label"), |
|
31 |
#' COVAR2 = factor( |
|
32 |
#' sample(as.factor(enum)), |
|
33 |
#' levels = 1:4, labels = c("F", "F", "M", "M") |
|
34 |
#' ) %>% formatters::with_label("Sex (F/M)") |
|
35 |
#' ) |
|
36 |
#' ) |
|
37 |
#' dta_bladder$AGE <- sample(20:60, size = nrow(dta_bladder), replace = TRUE) |
|
38 |
#' dta_bladder$STUDYID <- factor("X") |
|
39 |
#' |
|
40 |
#' plot( |
|
41 |
#' survfit(Surv(TIME, STATUS) ~ ARM + COVAR1, data = dta_bladder), |
|
42 |
#' lty = 2:4, |
|
43 |
#' xlab = "Months", |
|
44 |
#' col = c("blue1", "blue2", "blue3", "blue4", "red1", "red2", "red3", "red4") |
|
45 |
#' ) |
|
46 |
#' |
|
47 |
#' @name cox_regression |
|
48 |
NULL |
|
49 | ||
50 |
#' @describeIn cox_regression Statistics function that transforms results tabulated |
|
51 |
#' from [fit_coxreg_univar()] or [fit_coxreg_multivar()] into a list. |
|
52 |
#' |
|
53 |
#' @param model_df (`data.frame`)\cr contains the resulting model fit from a [fit_coxreg] |
|
54 |
#' function with tidying applied via [broom::tidy()]. |
|
55 |
#' @param .stats (`character`)\cr the name of statistics to be reported among: |
|
56 |
#' * `n`: number of observations (univariate only) |
|
57 |
#' * `hr`: hazard ratio |
|
58 |
#' * `ci`: confidence interval |
|
59 |
#' * `pval`: p-value of the treatment effect |
|
60 |
#' * `pval_inter`: p-value of the interaction effect between the treatment and the covariate (univariate only) |
|
61 |
#' @param .which_vars (`character`)\cr which rows should statistics be returned for from the given model. |
|
62 |
#' Defaults to "all". Other options include "var_main" for main effects, "inter" for interaction effects, |
|
63 |
#' and "multi_lvl" for multivariate model covariate level rows. When `.which_vars` is "all" specific |
|
64 |
#' variables can be selected by specifying `.var_nms`. |
|
65 |
#' @param .var_nms (`character`)\cr the `term` value of rows in `df` for which `.stats` should be returned. Typically |
|
66 |
#' this is the name of a variable. If using variable labels, `var` should be a vector of both the desired |
|
67 |
#' variable name and the variable label in that order to see all `.stats` related to that variable. When `.which_vars` |
|
68 |
#' is "var_main" `.var_nms` should be only the variable name. |
|
69 |
#' |
|
70 |
#' @return |
|
71 |
#' * `s_coxreg()` returns the selected statistic for from the Cox regression model for the selected variable(s). |
|
72 |
#' |
|
73 |
#' @examples |
|
74 |
#' # s_coxreg |
|
75 |
#' |
|
76 |
#' # Univariate |
|
77 |
#' u1_variables <- list( |
|
78 |
#' time = "TIME", event = "STATUS", arm = "ARM", covariates = c("COVAR1", "COVAR2") |
|
79 |
#' ) |
|
80 |
#' univar_model <- fit_coxreg_univar(variables = u1_variables, data = dta_bladder) |
|
81 |
#' df1 <- broom::tidy(univar_model) |
|
82 |
#' s_coxreg(model_df = df1, .stats = "hr") |
|
83 |
#' |
|
84 |
#' # Univariate with interactions |
|
85 |
#' univar_model_inter <- fit_coxreg_univar( |
|
86 |
#' variables = u1_variables, control = control_coxreg(interaction = TRUE), data = dta_bladder |
|
87 |
#' ) |
|
88 |
#' df1_inter <- broom::tidy(univar_model_inter) |
|
89 |
#' s_coxreg(model_df = df1_inter, .stats = "hr", .which_vars = "inter", .var_nms = "COVAR1") |
|
90 |
#' |
|
91 |
#' # Univariate without treatment arm - only "COVAR2" covariate effects |
|
92 |
#' u2_variables <- list(time = "TIME", event = "STATUS", covariates = c("COVAR1", "COVAR2")) |
|
93 |
#' univar_covs_model <- fit_coxreg_univar(variables = u2_variables, data = dta_bladder) |
|
94 |
#' df1_covs <- broom::tidy(univar_covs_model) |
|
95 |
#' s_coxreg(model_df = df1_covs, .stats = "hr", .var_nms = c("COVAR2", "Sex (F/M)")) |
|
96 |
#' |
|
97 |
#' # Multivariate. |
|
98 |
#' m1_variables <- list( |
|
99 |
#' time = "TIME", event = "STATUS", arm = "ARM", covariates = c("COVAR1", "COVAR2") |
|
100 |
#' ) |
|
101 |
#' multivar_model <- fit_coxreg_multivar(variables = m1_variables, data = dta_bladder) |
|
102 |
#' df2 <- broom::tidy(multivar_model) |
|
103 |
#' s_coxreg(model_df = df2, .stats = "pval", .which_vars = "var_main", .var_nms = "COVAR1") |
|
104 |
#' s_coxreg( |
|
105 |
#' model_df = df2, .stats = "pval", .which_vars = "multi_lvl", |
|
106 |
#' .var_nms = c("COVAR1", "A Covariate Label") |
|
107 |
#' ) |
|
108 |
#' |
|
109 |
#' # Multivariate without treatment arm - only "COVAR1" main effect |
|
110 |
#' m2_variables <- list(time = "TIME", event = "STATUS", covariates = c("COVAR1", "COVAR2")) |
|
111 |
#' multivar_covs_model <- fit_coxreg_multivar(variables = m2_variables, data = dta_bladder) |
|
112 |
#' df2_covs <- broom::tidy(multivar_covs_model) |
|
113 |
#' s_coxreg(model_df = df2_covs, .stats = "hr") |
|
114 |
#' |
|
115 |
#' @export |
|
116 |
s_coxreg <- function(model_df, .stats, .which_vars = "all", .var_nms = NULL) { |
|
117 | 178x |
assert_df_with_variables(model_df, list(term = "term", stat = .stats)) |
118 | 178x |
checkmate::assert_multi_class(model_df$term, classes = c("factor", "character")) |
119 | 178x |
model_df$term <- as.character(model_df$term) |
120 | 178x |
.var_nms <- .var_nms[!is.na(.var_nms)] |
121 | ||
122 | 177x |
if (length(.var_nms) > 0) model_df <- model_df[model_df$term %in% .var_nms, ] |
123 | 39x |
if (.which_vars == "multi_lvl") model_df$term <- tail(.var_nms, 1) |
124 | ||
125 |
# We need a list with names corresponding to the stats to display of equal length to the list of stats. |
|
126 | 178x |
y <- split(model_df, f = model_df$term, drop = FALSE) |
127 | 178x |
y <- stats::setNames(y, nm = rep(.stats, length(y))) |
128 | ||
129 | 178x |
if (.which_vars == "var_main") { |
130 | 79x |
y <- lapply(y, function(x) x[1, ]) # only main effect |
131 | 99x |
} else if (.which_vars %in% c("inter", "multi_lvl")) { |
132 | 75x |
y <- lapply(y, function(x) if (nrow(y[[1]]) > 1) x[-1, ] else x) # exclude main effect |
133 |
} |
|
134 | ||
135 | 178x |
lapply( |
136 | 178x |
X = y, |
137 | 178x |
FUN = function(x) { |
138 | 180x |
z <- as.list(x[[.stats]]) |
139 | 180x |
stats::setNames(z, nm = x$term_label) |
140 |
} |
|
141 |
) |
|
142 |
} |
|
143 | ||
144 |
#' @describeIn cox_regression Analysis function which is used as `afun` in [rtables::analyze()] |
|
145 |
#' and `cfun` in [rtables::summarize_row_groups()] within `summarize_coxreg()`. |
|
146 |
#' |
|
147 |
#' @param eff (`flag`)\cr whether treatment effect should be calculated. Defaults to `FALSE`. |
|
148 |
#' @param var_main (`flag`)\cr whether main effects should be calculated. Defaults to `FALSE`. |
|
149 |
#' @param na_level (`string`)\cr custom string to replace all `NA` values with. Defaults to `""`. |
|
150 |
#' @param cache_env (`environment`)\cr an environment object used to cache the regression model in order to |
|
151 |
#' avoid repeatedly fitting the same model for every row in the table. Defaults to `NULL` (no caching). |
|
152 |
#' |
|
153 |
#' @return |
|
154 |
#' * `a_coxreg()` returns formatted [rtables::CellValue()]. |
|
155 |
#' |
|
156 |
#' @examples |
|
157 |
#' tern:::a_coxreg( |
|
158 |
#' df = dta_bladder, |
|
159 |
#' labelstr = "Label 1", |
|
160 |
#' variables = u1_variables, |
|
161 |
#' .spl_context = list(value = "COVAR1"), |
|
162 |
#' .stats = "n", |
|
163 |
#' .formats = "xx" |
|
164 |
#' ) |
|
165 |
#' |
|
166 |
#' tern:::a_coxreg( |
|
167 |
#' df = dta_bladder, |
|
168 |
#' labelstr = "", |
|
169 |
#' variables = u1_variables, |
|
170 |
#' .spl_context = list(value = "COVAR2"), |
|
171 |
#' .stats = "pval", |
|
172 |
#' .formats = "xx.xxxx" |
|
173 |
#' ) |
|
174 |
#' |
|
175 |
#' @keywords internal |
|
176 |
a_coxreg <- function(df, |
|
177 |
labelstr, |
|
178 |
eff = FALSE, |
|
179 |
var_main = FALSE, |
|
180 |
multivar = FALSE, |
|
181 |
variables, |
|
182 |
at = list(), |
|
183 |
control = control_coxreg(), |
|
184 |
.spl_context, |
|
185 |
.stats, |
|
186 |
.formats, |
|
187 |
na_level = "", |
|
188 |
cache_env = NULL) { |
|
189 | 176x |
cov_no_arm <- !multivar && !"arm" %in% names(variables) && control$interaction # special case: univar no arm |
190 | 176x |
cov <- tail(.spl_context$value, 1) # current variable/covariate |
191 | 176x |
var_lbl <- formatters::var_labels(df)[cov] # check for df labels |
192 | 78x |
if (!is.na(var_lbl) && labelstr == cov && cov %in% variables$covariates) labelstr <- var_lbl # use df labels if none |
193 | 176x |
if (eff || multivar || cov_no_arm) { |
194 | 77x |
control$interaction <- FALSE |
195 |
} else { |
|
196 | 99x |
variables$covariates <- cov |
197 | 35x |
if (var_main) control$interaction <- TRUE |
198 |
} |
|
199 | ||
200 | 176x |
if (is.null(cache_env[[cov]])) { |
201 | 28x |
if (!multivar) { |
202 | 21x |
model <- fit_coxreg_univar(variables = variables, data = df, at = at, control = control) %>% broom::tidy() |
203 |
} else { |
|
204 | 7x |
model <- fit_coxreg_multivar(variables = variables, data = df, control = control) %>% broom::tidy() |
205 |
} |
|
206 | 28x |
cache_env[[cov]] <- model |
207 |
} else { |
|
208 | 148x |
model <- cache_env[[cov]] |
209 |
} |
|
210 | 99x |
if (!multivar && !var_main) model[, "pval_inter"] <- NA_real_ |
211 | ||
212 | 176x |
if (cov_no_arm || (!cov_no_arm && !"arm" %in% names(variables) && is.numeric(df[[cov]]))) { |
213 | 15x |
multivar <- TRUE |
214 | 3x |
if (!cov_no_arm) var_main <- TRUE |
215 |
} |
|
216 | ||
217 | 176x |
vars_coxreg <- list(which_vars = "all", var_nms = NULL) |
218 | 176x |
if (eff) { |
219 | 35x |
if (multivar && !var_main) { # multivar treatment level |
220 | 6x |
var_lbl_arm <- formatters::var_labels(df)[[variables$arm]] |
221 | 6x |
vars_coxreg[c("var_nms", "which_vars")] <- list(c(variables$arm, var_lbl_arm), "multi_lvl") |
222 |
} else { # treatment effect |
|
223 | 29x |
vars_coxreg["var_nms"] <- variables$arm |
224 | 6x |
if (var_main) vars_coxreg["which_vars"] <- "var_main" |
225 |
} |
|
226 |
} else { |
|
227 | 141x |
if (!multivar || (multivar && var_main && !is.numeric(df[[cov]]))) { # covariate effect/level |
228 | 108x |
vars_coxreg[c("var_nms", "which_vars")] <- list(cov, "var_main") |
229 | 33x |
} else if (multivar) { # multivar covariate level |
230 | 33x |
vars_coxreg[c("var_nms", "which_vars")] <- list(c(cov, var_lbl), "multi_lvl") |
231 | 6x |
if (var_main) model[cov, .stats] <- NA_real_ |
232 |
} |
|
233 | 35x |
if (!multivar && !var_main && control$interaction) vars_coxreg["which_vars"] <- "inter" # interaction effect |
234 |
} |
|
235 | 176x |
var_vals <- s_coxreg(model, .stats, .which_vars = vars_coxreg$which_vars, .var_nms = vars_coxreg$var_nms)[[1]] |
236 | 176x |
var_names <- if (all(grepl("\\(reference = ", names(var_vals))) && labelstr != tail(.spl_context$value, 1)) { |
237 | 21x |
paste(c(labelstr, tail(strsplit(names(var_vals), " ")[[1]], 3)), collapse = " ") # "reference" main effect labels |
238 | 176x |
} else if ((!multivar && !eff && !(!var_main && control$interaction) && nchar(labelstr) > 0) || |
239 | 176x |
(multivar && var_main && is.numeric(df[[cov]]))) { |
240 | 58x |
labelstr # other main effect labels |
241 | 176x |
} else if (multivar && !eff && !var_main && is.numeric(df[[cov]])) { |
242 | 6x |
"All" # multivar numeric covariate |
243 |
} else { |
|
244 | 91x |
names(var_vals) |
245 |
} |
|
246 | 176x |
in_rows( |
247 | 176x |
.list = var_vals, .names = var_names, .labels = var_names, |
248 | 176x |
.formats = stats::setNames(rep(.formats, length(var_names)), var_names), |
249 | 176x |
.format_na_strs = stats::setNames(rep(na_level, length(var_names)), var_names) |
250 |
) |
|
251 |
} |
|
252 | ||
253 |
#' @describeIn cox_regression Layout-creating function which creates a Cox regression summary table |
|
254 |
#' layout. This function is a wrapper for several `rtables` layouting functions. This function |
|
255 |
#' is a wrapper for [rtables::analyze_colvars()] and [rtables::summarize_row_groups()]. |
|
256 |
#' |
|
257 |
#' @inheritParams fit_coxreg_univar |
|
258 |
#' @param multivar (`flag`)\cr Defaults to `FALSE`. If `TRUE` multivariate Cox regression will run, otherwise |
|
259 |
#' univariate Cox regression will run. |
|
260 |
#' @param common_var (`character`)\cr the name of a factor variable in the dataset which takes the same value |
|
261 |
#' for all rows. This should be created during pre-processing if no such variable currently exists. |
|
262 |
#' @param .section_div (`character`)\cr string which should be repeated as a section divider between sections. |
|
263 |
#' Defaults to `NA` for no section divider. If a vector of two strings are given, the first will be used between |
|
264 |
#' treatment and covariate sections and the second between different covariates. |
|
265 |
#' |
|
266 |
#' @return |
|
267 |
#' * `summarize_coxreg()` returns a layout object suitable for passing to further layouting functions, |
|
268 |
#' or to [rtables::build_table()]. Adding this function to an `rtable` layout will add a Cox regression table |
|
269 |
#' containing the chosen statistics to the table layout. |
|
270 |
#' |
|
271 |
#' @seealso [fit_coxreg_univar()] and [fit_coxreg_multivar()] which also take the `variables`, `data`, |
|
272 |
#' `at` (univariate only), and `control` arguments but return unformatted univariate and multivariate |
|
273 |
#' Cox regression models, respectively. |
|
274 |
#' |
|
275 |
#' @examples |
|
276 |
#' # summarize_coxreg |
|
277 |
#' |
|
278 |
#' result_univar <- basic_table() %>% |
|
279 |
#' summarize_coxreg(variables = u1_variables) %>% |
|
280 |
#' build_table(dta_bladder) |
|
281 |
#' result_univar |
|
282 |
#' |
|
283 |
#' result_multivar <- basic_table() %>% |
|
284 |
#' summarize_coxreg( |
|
285 |
#' variables = m1_variables, |
|
286 |
#' multivar = TRUE, |
|
287 |
#' ) %>% |
|
288 |
#' build_table(dta_bladder) |
|
289 |
#' result_multivar |
|
290 |
#' |
|
291 |
#' result_univar_covs <- basic_table() %>% |
|
292 |
#' summarize_coxreg( |
|
293 |
#' variables = u2_variables, |
|
294 |
#' ) %>% |
|
295 |
#' build_table(dta_bladder) |
|
296 |
#' result_univar_covs |
|
297 |
#' |
|
298 |
#' result_multivar_covs <- basic_table() %>% |
|
299 |
#' summarize_coxreg( |
|
300 |
#' variables = m2_variables, |
|
301 |
#' multivar = TRUE, |
|
302 |
#' varlabels = c("Covariate 1", "Covariate 2") # custom labels |
|
303 |
#' ) %>% |
|
304 |
#' build_table(dta_bladder) |
|
305 |
#' result_multivar_covs |
|
306 |
#' |
|
307 |
#' @export |
|
308 |
summarize_coxreg <- function(lyt, |
|
309 |
variables, |
|
310 |
control = control_coxreg(), |
|
311 |
at = list(), |
|
312 |
multivar = FALSE, |
|
313 |
common_var = "STUDYID", |
|
314 |
.stats = c("n", "hr", "ci", "pval", "pval_inter"), |
|
315 |
.formats = c( |
|
316 |
n = "xx", hr = "xx.xx", ci = "(xx.xx, xx.xx)", |
|
317 |
pval = "x.xxxx | (<0.0001)", pval_inter = "x.xxxx | (<0.0001)" |
|
318 |
), |
|
319 |
varlabels = NULL, |
|
320 |
.indent_mods = NULL, |
|
321 |
na_level = "", |
|
322 |
.section_div = NA_character_) { |
|
323 | 10x |
if (multivar && control$interaction) { |
324 | 1x |
warning(paste( |
325 | 1x |
"Interactions are not available for multivariate cox regression using summarize_coxreg.", |
326 | 1x |
"The model will be calculated without interaction effects." |
327 |
)) |
|
328 |
} |
|
329 | 10x |
if (control$interaction && !"arm" %in% names(variables)) { |
330 | 1x |
stop("To include interactions please specify 'arm' in variables.") |
331 |
} |
|
332 | ||
333 | 9x |
.stats <- if (!"arm" %in% names(variables) || multivar) { # only valid statistics |
334 | 4x |
intersect(c("hr", "ci", "pval"), .stats) |
335 | 9x |
} else if (control$interaction) { |
336 | 3x |
intersect(c("n", "hr", "ci", "pval", "pval_inter"), .stats) |
337 |
} else { |
|
338 | 2x |
intersect(c("n", "hr", "ci", "pval"), .stats) |
339 |
} |
|
340 | 9x |
stat_labels <- c( |
341 | 9x |
n = "n", hr = "Hazard Ratio", ci = paste0(control$conf_level * 100, "% CI"), |
342 | 9x |
pval = "p-value", pval_inter = "Interaction p-value" |
343 |
) |
|
344 | 9x |
stat_labels <- stat_labels[names(stat_labels) %in% .stats] |
345 | 9x |
.formats <- .formats[names(.formats) %in% .stats] |
346 | 9x |
env <- new.env() # create caching environment |
347 | ||
348 | 9x |
lyt <- lyt %>% |
349 | 9x |
split_cols_by_multivar( |
350 | 9x |
vars = rep(common_var, length(.stats)), |
351 | 9x |
varlabels = stat_labels, |
352 | 9x |
extra_args = list( |
353 | 9x |
.stats = .stats, .formats = .formats, na_level = rep(na_level, length(.stats)), |
354 | 9x |
cache_env = replicate(length(.stats), list(env)) |
355 |
) |
|
356 |
) |
|
357 | ||
358 | 9x |
if ("arm" %in% names(variables)) { # treatment effect |
359 | 7x |
lyt <- lyt %>% |
360 | 7x |
split_rows_by( |
361 | 7x |
common_var, |
362 | 7x |
split_label = "Treatment:", |
363 | 7x |
label_pos = "visible", |
364 | 7x |
section_div = head(.section_div, 1) |
365 |
) %>% |
|
366 | 7x |
summarize_row_groups( |
367 | 7x |
cfun = a_coxreg, |
368 | 7x |
extra_args = list( |
369 | 7x |
variables = variables, control = control, multivar = multivar, eff = TRUE, var_main = multivar |
370 |
) |
|
371 |
) |
|
372 | 7x |
if (multivar) { # treatment level effects |
373 | 2x |
lyt <- lyt %>% |
374 | 2x |
analyze_colvars( |
375 | 2x |
afun = a_coxreg, |
376 | 2x |
extra_args = list(eff = TRUE, control = control, variables = variables, multivar = multivar) |
377 |
) |
|
378 |
} |
|
379 |
} |
|
380 | ||
381 | 9x |
if ("covariates" %in% names(variables)) { # covariate main effects |
382 | 9x |
lyt <- lyt %>% |
383 | 9x |
split_rows_by_multivar( |
384 | 9x |
vars = variables$covariates, |
385 | 9x |
varlabels = varlabels, |
386 | 9x |
split_label = "Covariate:", |
387 | 9x |
nested = FALSE, |
388 | 9x |
section_div = tail(.section_div, 1) |
389 |
) %>% |
|
390 | 9x |
summarize_row_groups( |
391 | 9x |
cfun = a_coxreg, |
392 | 9x |
extra_args = list( |
393 | 9x |
variables = variables, at = at, control = control, multivar = multivar, |
394 | 9x |
var_main = if (multivar) multivar else control$interaction |
395 |
) |
|
396 |
) |
|
397 | 2x |
if (!"arm" %in% names(variables)) control$interaction <- TRUE # special case: univar no arm |
398 | 9x |
if (multivar || control$interaction) { # covariate level effects |
399 | 7x |
lyt <- lyt %>% |
400 | 7x |
analyze_colvars( |
401 | 7x |
afun = a_coxreg, |
402 | 7x |
extra_args = list(variables = variables, at = at, control = control, multivar = multivar, labelstr = "") |
403 |
) |
|
404 |
} |
|
405 |
} |
|
406 | ||
407 | 9x |
lyt |
408 |
} |
1 |
#' Pairwise Formula Special Term |
|
2 |
#' |
|
3 |
#' @description `r lifecycle::badge("deprecated")` |
|
4 |
#' |
|
5 |
#' The special term `pairwise` indicate that the model should be fitted individually for |
|
6 |
#' every tested level in comparison to the reference level. |
|
7 |
#' |
|
8 |
#' @param x the variable for which pairwise result is expected. |
|
9 |
#' |
|
10 |
#' @return Variable "paired". |
|
11 |
#' |
|
12 |
#' @details Let's `ARM` being a factor with level A, B, C; let's be B the reference level, |
|
13 |
#' a model calling the formula including `pairwise(ARM)` will result in two models: |
|
14 |
#' * A model including only levels A and B, and effect of A estimated in reference to B. |
|
15 |
#' * A model including only levels C and B, the effect of C estimated in reference to B. |
|
16 |
#' |
|
17 |
#' @export |
|
18 |
pairwise <- function(x) { |
|
19 | ! |
lifecycle::deprecate_warn("0.8.1.9013", "pairwise()", "univariate()") |
20 | ! |
structure(x, varname = deparse(substitute(x))) |
21 |
} |
|
22 | ||
23 |
#' Univariate Formula Special Term |
|
24 |
#' |
|
25 |
#' @description `r lifecycle::badge("stable")` |
|
26 |
#' |
|
27 |
#' The special term `univariate` indicate that the model should be fitted individually for |
|
28 |
#' every variable included in univariate. |
|
29 |
#' |
|
30 |
#' @param x A vector of variable name separated by commas. |
|
31 |
#' |
|
32 |
#' @return When used within a model formula, produces univariate models for each variable provided. |
|
33 |
#' |
|
34 |
#' @details |
|
35 |
#' If provided alongside with pairwise specification, the model |
|
36 |
#' `y ~ ARM + univariate(SEX, AGE, RACE)` lead to the study and comparison of the models |
|
37 |
#' + `y ~ ARM` |
|
38 |
#' + `y ~ ARM + SEX` |
|
39 |
#' + `y ~ ARM + AGE` |
|
40 |
#' + `y ~ ARM + RACE` |
|
41 |
#' |
|
42 |
#' @export |
|
43 |
univariate <- function(x) { |
|
44 | 1x |
structure(x, varname = deparse(substitute(x))) |
45 |
} |
|
46 | ||
47 |
# Get the right-hand-term of a formula |
|
48 |
rht <- function(x) { |
|
49 | 4x |
checkmate::assert_formula(x) |
50 | 4x |
y <- as.character(rev(x)[[1]]) |
51 | 4x |
return(y) |
52 |
} |
|
53 | ||
54 |
#' Hazard Ratio Estimation in Interactions |
|
55 |
#' |
|
56 |
#' This function estimates the hazard ratios between arms when an interaction variable is given with |
|
57 |
#' specific values. |
|
58 |
#' |
|
59 |
#' @param variable,given Names of two variable in interaction. We seek the estimation of the levels of `variable` |
|
60 |
#' given the levels of `given`. |
|
61 |
#' @param lvl_var,lvl_given corresponding levels has given by `levels`. |
|
62 |
#' @param mmat A name numeric filled with 0 used as template to obtain the design matrix. |
|
63 |
#' @param coef Numeric of estimated coefficients. |
|
64 |
#' @param vcov Variance-covariance matrix of underlying model. |
|
65 |
#' @param conf_level Single numeric for the confidence level of estimate intervals. |
|
66 |
#' |
|
67 |
#' @details Given the cox regression investigating the effect of Arm (A, B, C; reference A) |
|
68 |
#' and Sex (F, M; reference Female). The model is abbreviated: y ~ Arm + Sex + Arm x Sex. |
|
69 |
#' The cox regression estimates the coefficients along with a variance-covariance matrix for: |
|
70 |
#' |
|
71 |
#' - b1 (arm b), b2 (arm c) |
|
72 |
#' - b3 (sex m) |
|
73 |
#' - b4 (arm b: sex m), b5 (arm c: sex m) |
|
74 |
#' |
|
75 |
#' Given that I want an estimation of the Hazard Ratio for arm C/sex M, the estimation |
|
76 |
#' will be given in reference to arm A/Sex M by exp(b2 + b3 + b5)/ exp(b3) = exp(b2 + b5), |
|
77 |
#' therefore the interaction coefficient is given by b2 + b5 while the standard error is obtained |
|
78 |
#' as $1.96 * sqrt(Var b2 + Var b5 + 2 * covariance (b2,b5))$ for a confidence level of 0.95. |
|
79 |
#' |
|
80 |
#' @return A list of matrix (one per level of variable) with rows corresponding to the combinations of |
|
81 |
#' `variable` and `given`, with columns: |
|
82 |
#' * `coef_hat`: Estimation of the coefficient. |
|
83 |
#' * `coef_se`: Standard error of the estimation. |
|
84 |
#' * `hr`: Hazard ratio. |
|
85 |
#' * `lcl, ucl`: Lower/upper confidence limit of the hazard ratio. |
|
86 |
#' |
|
87 |
#' @seealso [s_cox_multivariate()]. |
|
88 |
#' |
|
89 |
#' @examples |
|
90 |
#' library(dplyr) |
|
91 |
#' library(survival) |
|
92 |
#' |
|
93 |
#' ADSL <- tern_ex_adsl %>% |
|
94 |
#' filter(SEX %in% c("F", "M")) |
|
95 |
#' |
|
96 |
#' adtte <- tern_ex_adtte %>% filter(PARAMCD == "PFS") |
|
97 |
#' adtte$ARMCD <- droplevels(adtte$ARMCD) |
|
98 |
#' adtte$SEX <- droplevels(adtte$SEX) |
|
99 |
#' |
|
100 |
#' mod <- coxph( |
|
101 |
#' formula = Surv(time = AVAL, event = 1 - CNSR) ~ (SEX + ARMCD)^2, |
|
102 |
#' data = adtte |
|
103 |
#' ) |
|
104 |
#' |
|
105 |
#' mmat <- stats::model.matrix(mod)[1, ] |
|
106 |
#' mmat[!mmat == 0] <- 0 |
|
107 |
#' |
|
108 |
#' # Internal function - estimate_coef |
|
109 |
#' \dontrun{ |
|
110 |
#' estimate_coef( |
|
111 |
#' variable = "ARMCD", given = "SEX", lvl_var = "ARM A", lvl_given = "M", |
|
112 |
#' coef = stats::coef(mod), mmat = mmat, vcov = stats::vcov(mod), conf_level = .95 |
|
113 |
#' ) |
|
114 |
#' } |
|
115 |
#' |
|
116 |
#' @keywords internal |
|
117 |
estimate_coef <- function(variable, given, |
|
118 |
lvl_var, lvl_given, |
|
119 |
coef, |
|
120 |
mmat, |
|
121 |
vcov, |
|
122 |
conf_level = 0.95) { |
|
123 | 8x |
var_lvl <- paste0(variable, lvl_var[-1]) # [-1]: reference level |
124 | 8x |
giv_lvl <- paste0(given, lvl_given) |
125 | ||
126 | 8x |
design_mat <- expand.grid(variable = var_lvl, given = giv_lvl) |
127 | 8x |
design_mat <- design_mat[order(design_mat$variable, design_mat$given), ] |
128 | 8x |
design_mat <- within( |
129 | 8x |
data = design_mat, |
130 | 8x |
expr = { |
131 | 8x |
inter <- paste0(variable, ":", given) |
132 | 8x |
rev_inter <- paste0(given, ":", variable) |
133 |
} |
|
134 |
) |
|
135 | ||
136 | 8x |
split_by_variable <- design_mat$variable |
137 | 8x |
interaction_names <- paste(design_mat$variable, design_mat$given, sep = "/") |
138 | ||
139 | 8x |
design_mat <- apply( |
140 | 8x |
X = design_mat, MARGIN = 1, FUN = function(x) { |
141 | 27x |
mmat[names(mmat) %in% x[-which(names(x) == "given")]] <- 1 |
142 | 27x |
return(mmat) |
143 |
} |
|
144 |
) |
|
145 | 8x |
colnames(design_mat) <- interaction_names |
146 | ||
147 | 8x |
betas <- as.matrix(coef) |
148 | ||
149 | 8x |
coef_hat <- t(design_mat) %*% betas |
150 | 8x |
dimnames(coef_hat)[2] <- "coef" |
151 | ||
152 | 8x |
coef_se <- apply(design_mat, 2, function(x) { |
153 | 27x |
vcov_el <- as.logical(x) |
154 | 27x |
y <- vcov[vcov_el, vcov_el] |
155 | 27x |
y <- sum(y) |
156 | 27x |
y <- sqrt(y) |
157 | 27x |
return(y) |
158 |
}) |
|
159 | ||
160 | 8x |
q_norm <- stats::qnorm((1 + conf_level) / 2) |
161 | 8x |
y <- cbind(coef_hat, `se(coef)` = coef_se) |
162 | ||
163 | 8x |
y <- apply(y, 1, function(x) { |
164 | 27x |
x["hr"] <- exp(x["coef"]) |
165 | 27x |
x["lcl"] <- exp(x["coef"] - q_norm * x["se(coef)"]) |
166 | 27x |
x["ucl"] <- exp(x["coef"] + q_norm * x["se(coef)"]) |
167 | ||
168 | 27x |
return(x) |
169 |
}) |
|
170 | ||
171 | 8x |
y <- t(y) |
172 | 8x |
y <- by(y, split_by_variable, identity) |
173 | 8x |
y <- lapply(y, as.matrix) |
174 | ||
175 | 8x |
attr(y, "details") <- paste0( |
176 | 8x |
"Estimations of ", variable, |
177 | 8x |
" hazard ratio given the level of ", given, " compared to ", |
178 | 8x |
variable, " level ", lvl_var[1], "." |
179 |
) |
|
180 | 8x |
return(y) |
181 |
} |
|
182 | ||
183 |
#' `tryCatch` around `car::Anova` |
|
184 |
#' |
|
185 |
#' Captures warnings when executing [car::Anova]. |
|
186 |
#' |
|
187 |
#' @inheritParams car::Anova |
|
188 |
#' |
|
189 |
#' @return A list with item `aov` for the result of the model and `error_text` for the captured warnings. |
|
190 |
#' |
|
191 |
#' @examples |
|
192 |
#' # `car::Anova` on cox regression model including strata and expected |
|
193 |
#' # a likelihood ratio test triggers a warning as only Wald method is |
|
194 |
#' # accepted. |
|
195 |
#' |
|
196 |
#' library(survival) |
|
197 |
#' |
|
198 |
#' mod <- coxph( |
|
199 |
#' formula = Surv(time = futime, event = fustat) ~ factor(rx) + strata(ecog.ps), |
|
200 |
#' data = ovarian |
|
201 |
#' ) |
|
202 |
#' |
|
203 |
#' # Internal function - try_car_anova |
|
204 |
#' \dontrun{ |
|
205 |
#' with_wald <- try_car_anova(mod = mod, test.statistic = "Wald") |
|
206 |
#' with_lr <- try_car_anova(mod = mod, test.statistic = "LR") |
|
207 |
#' } |
|
208 |
#' |
|
209 |
#' @keywords internal |
|
210 |
try_car_anova <- function(mod, |
|
211 |
test.statistic) { # nolint |
|
212 | 2x |
y <- tryCatch( |
213 | 2x |
withCallingHandlers( |
214 | 2x |
expr = { |
215 | 2x |
warn_text <- c() |
216 | 2x |
list( |
217 | 2x |
aov = car::Anova( |
218 | 2x |
mod, |
219 | 2x |
test.statistic = test.statistic, |
220 | 2x |
type = "III" |
221 |
), |
|
222 | 2x |
warn_text = warn_text |
223 |
) |
|
224 |
}, |
|
225 | 2x |
warning = function(w) { |
226 |
# If a warning is detected it is handled as "w". |
|
227 | ! |
warn_text <<- trimws(paste0("Warning in `try_car_anova`: ", w)) |
228 | ||
229 |
# A warning is sometimes expected, then, we want to restart |
|
230 |
# the execution while ignoring the warning. |
|
231 | ! |
invokeRestart("muffleWarning") |
232 |
} |
|
233 |
), |
|
234 | 2x |
finally = { |
235 |
} |
|
236 |
) |
|
237 | ||
238 | 2x |
return(y) |
239 |
} |
|
240 | ||
241 |
#' Fit the Cox Regression Model and Anova |
|
242 |
#' |
|
243 |
#' The functions allows to derive from the [survival::coxph()] results the effect p.values using [car::Anova()]. |
|
244 |
#' This last package introduces more flexibility to get the effect p.values. |
|
245 |
#' |
|
246 |
#' @inheritParams t_coxreg |
|
247 |
#' |
|
248 |
#' @return A list with items `mod` (results of [survival::coxph()]), `msum` (result of `summary`) and |
|
249 |
#' `aov` (result of [car::Anova()]). |
|
250 |
#' |
|
251 |
#' @noRd |
|
252 |
fit_n_aov <- function(formula, |
|
253 |
data = data, |
|
254 |
conf_level = conf_level, |
|
255 |
pval_method = c("wald", "likelihood"), |
|
256 |
...) { |
|
257 | 1x |
pval_method <- match.arg(pval_method) |
258 | ||
259 | 1x |
environment(formula) <- environment() |
260 | 1x |
suppressWarnings({ |
261 |
# We expect some warnings due to coxph which fails strict programming. |
|
262 | 1x |
mod <- survival::coxph(formula, data = data, ...) |
263 | 1x |
msum <- summary(mod, conf.int = conf_level) |
264 |
}) |
|
265 | ||
266 | 1x |
aov <- try_car_anova( |
267 | 1x |
mod, |
268 | 1x |
test.statistic = switch(pval_method, |
269 | 1x |
"wald" = "Wald", |
270 | 1x |
"likelihood" = "LR" |
271 |
) |
|
272 |
) |
|
273 | ||
274 | 1x |
warn_attr <- aov$warn_text |
275 | ! |
if (!is.null(aov$warn_text)) message(warn_attr) |
276 | ||
277 | 1x |
aov <- aov$aov |
278 | 1x |
y <- list(mod = mod, msum = msum, aov = aov) |
279 | 1x |
attr(y, "message") <- warn_attr |
280 | ||
281 | 1x |
return(y) |
282 |
} |
|
283 | ||
284 |
# argument_checks |
|
285 |
check_formula <- function(formula) { |
|
286 | 1x |
if (!(inherits(formula, "formula"))) { |
287 | 1x |
stop("Check `formula`. A formula should resemble `Surv(time = AVAL, event = 1 - CNSR) ~ study_arm(ARMCD)`.") |
288 |
} |
|
289 | ||
290 | ! |
invisible() |
291 |
} |
|
292 | ||
293 |
check_covariate_formulas <- function(covariates) { |
|
294 | 1x |
if (!all(vapply(X = covariates, FUN = inherits, what = "formula", FUN.VALUE = TRUE)) || is.null(covariates)) { |
295 | 1x |
stop("Check `covariates`, it should be a list of right-hand-term formulas, e.g. list(Age = ~AGE).") |
296 |
} |
|
297 | ||
298 | ! |
invisible() |
299 |
} |
|
300 | ||
301 |
name_covariate_names <- function(covariates) { |
|
302 | 1x |
miss_names <- names(covariates) == "" |
303 | 1x |
no_names <- is.null(names(covariates)) |
304 | ! |
if (any(miss_names)) names(covariates)[miss_names] <- vapply(covariates[miss_names], FUN = rht, FUN.VALUE = "name") |
305 | ! |
if (no_names) names(covariates) <- vapply(covariates, FUN = rht, FUN.VALUE = "name") |
306 | 1x |
return(covariates) |
307 |
} |
|
308 | ||
309 |
check_increments <- function(increments, covariates) { |
|
310 | 1x |
if (!is.null(increments)) { |
311 | 1x |
covariates <- vapply(covariates, FUN = rht, FUN.VALUE = "name") |
312 | 1x |
lapply( |
313 | 1x |
X = names(increments), FUN = function(x) { |
314 | 3x |
if (!x %in% covariates) { |
315 | 1x |
warning( |
316 | 1x |
paste( |
317 | 1x |
"Check `increments`, the `increment` for ", x, |
318 | 1x |
"doesn't match any names in investigated covariate(s)." |
319 |
) |
|
320 |
) |
|
321 |
} |
|
322 |
} |
|
323 |
) |
|
324 |
} |
|
325 | ||
326 | 1x |
invisible() |
327 |
} |
|
328 | ||
329 |
#' Multivariate Cox Model - Summarized Results |
|
330 |
#' |
|
331 |
#' Analyses based on multivariate Cox model are usually not performed for the Controlled Substance Reporting or |
|
332 |
#' regulatory documents but serve exploratory purposes only (e.g., for publication). In practice, the model usually |
|
333 |
#' includes only the main effects (without interaction terms). It produces the hazard ratio estimates for each of the |
|
334 |
#' covariates included in the model. |
|
335 |
#' The analysis follows the same principles (e.g., stratified vs. unstratified analysis and tie handling) as the |
|
336 |
#' usual Cox model analysis. Since there is usually no pre-specified hypothesis testing for such analysis, |
|
337 |
#' the p.values need to be interpreted with caution. (**Statistical Analysis of Clinical Trials Data with R**, |
|
338 |
#' `NEST's bookdown`) |
|
339 |
#' |
|
340 |
#' @param formula (`formula`)\cr A formula corresponding to the investigated [survival::Surv()] survival model |
|
341 |
#' including covariates. |
|
342 |
#' @param data (`data.frame`)\cr A data frame which includes the variable in formula and covariates. |
|
343 |
#' @param conf_level (`proportion`)\cr The confidence level for the hazard ratio interval estimations. Default is 0.95. |
|
344 |
#' @param pval_method (`character`)\cr The method used for the estimation of p-values, should be one of |
|
345 |
#' "wald" (default) or "likelihood". |
|
346 |
#' @param ... Optional parameters passed to [survival::coxph()]. Can include `ties`, a character string specifying the |
|
347 |
#' method for tie handling, one of `exact` (default), `efron`, `breslow`. |
|
348 |
#' |
|
349 |
#' @return A `list` with elements `mod`, `msum`, `aov`, and `coef_inter`. |
|
350 |
#' |
|
351 |
#' @details The output is limited to single effect terms. Work in ongoing for estimation of interaction terms |
|
352 |
#' but is out of scope as defined by the Global Data Standards Repository |
|
353 |
#' (**`GDS_Standard_TLG_Specs_Tables_2.doc`**). |
|
354 |
#' |
|
355 |
#' @seealso [estimate_coef()]. |
|
356 |
#' |
|
357 |
#' @examples |
|
358 |
#' library(dplyr) |
|
359 |
#' |
|
360 |
#' adtte <- tern_ex_adtte |
|
361 |
#' adtte_f <- subset(adtte, PARAMCD == "OS") # _f: filtered |
|
362 |
#' adtte_f <- filter( |
|
363 |
#' adtte_f, |
|
364 |
#' PARAMCD == "OS" & |
|
365 |
#' SEX %in% c("F", "M") & |
|
366 |
#' RACE %in% c("ASIAN", "BLACK OR AFRICAN AMERICAN", "WHITE") |
|
367 |
#' ) |
|
368 |
#' adtte_f$SEX <- droplevels(adtte_f$SEX) |
|
369 |
#' adtte_f$RACE <- droplevels(adtte_f$RACE) |
|
370 |
#' |
|
371 |
#' # Internal function - s_cox_multivariate |
|
372 |
#' \dontrun{ |
|
373 |
#' s_cox_multivariate( |
|
374 |
#' formula = Surv(time = AVAL, event = 1 - CNSR) ~ (ARMCD + RACE + AGE)^2, data = adtte_f |
|
375 |
#' ) |
|
376 |
#' } |
|
377 |
#' |
|
378 |
#' @keywords internal |
|
379 |
s_cox_multivariate <- function(formula, data, |
|
380 |
conf_level = 0.95, |
|
381 |
pval_method = c("wald", "likelihood"), |
|
382 |
...) { |
|
383 | 1x |
tf <- stats::terms(formula, specials = c("strata")) |
384 | 1x |
covariates <- rownames(attr(tf, "factors"))[-c(1, unlist(attr(tf, "specials")))] |
385 | 1x |
lapply( |
386 | 1x |
X = covariates, |
387 | 1x |
FUN = function(x) { |
388 | 3x |
if (is.character(data[[x]])) { |
389 | 1x |
data[[x]] <<- as.factor(data[[x]]) |
390 |
} |
|
391 | 3x |
invisible() |
392 |
} |
|
393 |
) |
|
394 | 1x |
pval_method <- match.arg(pval_method) |
395 | ||
396 |
# Results directly exported from environment(fit_n_aov) to environment(s_function_draft) |
|
397 | 1x |
y <- fit_n_aov( |
398 | 1x |
formula = formula, |
399 | 1x |
data = data, |
400 | 1x |
conf_level = conf_level, |
401 | 1x |
pval_method = pval_method, |
402 |
... |
|
403 |
) |
|
404 | 1x |
mod <- y$mod |
405 | 1x |
aov <- y$aov |
406 | 1x |
msum <- y$msum |
407 | 1x |
list2env(as.list(y), environment()) |
408 | ||
409 | 1x |
all_term_labs <- attr(mod$terms, "term.labels") |
410 | 1x |
term_labs <- all_term_labs[which(attr(mod$terms, "order") == 1)] |
411 | 1x |
names(term_labs) <- term_labs |
412 | ||
413 | 1x |
coef_inter <- NULL |
414 | 1x |
if (any(attr(mod$terms, "order") > 1)) { |
415 | 1x |
for_inter <- all_term_labs[attr(mod$terms, "order") > 1] |
416 | 1x |
names(for_inter) <- for_inter |
417 | 1x |
mmat <- stats::model.matrix(mod)[1, ] |
418 | 1x |
mmat[!mmat == 0] <- 0 |
419 | 1x |
mcoef <- stats::coef(mod) |
420 | 1x |
mvcov <- stats::vcov(mod) |
421 | ||
422 | 1x |
estimate_coef_local <- function(variable, given) { |
423 | 6x |
estimate_coef( |
424 | 6x |
variable, given, |
425 | 6x |
coef = mcoef, mmat = mmat, vcov = mvcov, conf_level = conf_level, |
426 | 6x |
lvl_var = levels(data[[variable]]), lvl_given = levels(data[[given]]) |
427 |
) |
|
428 |
} |
|
429 | ||
430 | 1x |
coef_inter <- lapply( |
431 | 1x |
for_inter, function(x) { |
432 | 3x |
y <- attr(mod$terms, "factor")[, x] |
433 | 3x |
y <- names(y[y > 0]) |
434 | 3x |
Map(estimate_coef_local, variable = y, given = rev(y)) |
435 |
} |
|
436 |
) |
|
437 |
} |
|
438 | ||
439 | 1x |
list(mod = mod, msum = msum, aov = aov, coef_inter = coef_inter) |
440 |
} |
1 |
#' Confidence Intervals for a Difference of Binomials |
|
2 |
#' |
|
3 |
#' @description `r lifecycle::badge("experimental")` |
|
4 |
#' |
|
5 |
#' Several confidence intervals for the difference between proportions. |
|
6 |
#' |
|
7 |
#' @param grp (`factor`)\cr vector assigning observations to one out of two groups |
|
8 |
#' (e.g. reference and treatment group). |
|
9 |
#' |
|
10 |
#' @name desctools_binom |
|
11 |
NULL |
|
12 | ||
13 |
#' Recycle List of Parameters |
|
14 |
#' |
|
15 |
#' This function recycles all supplied elements to the maximal dimension. |
|
16 |
#' |
|
17 |
#' @param ... (`any`)\cr Elements to recycle. |
|
18 |
#' |
|
19 |
#' @return A `list`. |
|
20 |
#' |
|
21 |
#' @keywords internal |
|
22 |
#' @noRd |
|
23 |
h_recycle <- function(...) { |
|
24 | 60x |
lst <- list(...) |
25 | 60x |
maxdim <- max(lengths(lst)) |
26 | 60x |
res <- lapply(lst, rep, length.out = maxdim) |
27 | 60x |
attr(res, "maxdim") <- maxdim |
28 | 60x |
return(res) |
29 |
} |
|
30 | ||
31 |
#' @describeIn desctools_binom Several confidence intervals for the difference between proportions. |
|
32 |
#' |
|
33 |
#' @return A `matrix` of 3 values: |
|
34 |
#' * `est`: estimate of proportion difference. |
|
35 |
#' * `lwr.ci`: estimate of lower end of the confidence interval. |
|
36 |
#' * `upr.ci`: estimate of upper end of the confidence interval. |
|
37 |
#' |
|
38 |
#' @examples |
|
39 |
#' # Internal function - desctools_binom |
|
40 |
#' \dontrun{ |
|
41 |
#' set.seed(2) |
|
42 |
#' rsp <- sample(c(TRUE, FALSE), replace = TRUE, size = 20) |
|
43 |
#' grp <- factor(c(rep("A", 10), rep("B", 10))) |
|
44 |
#' tbl <- table(grp, factor(rsp, levels = c(TRUE, FALSE))) |
|
45 |
#' desctools_binom( |
|
46 |
#' tbl[1], sum(tbl[1], tbl[3]), tbl[2], sum(tbl[2], tbl[4]), |
|
47 |
#' conf.level = 0.90, method = "waldcc" |
|
48 |
#' ) |
|
49 |
#' } |
|
50 |
#' |
|
51 |
#' @keywords internal |
|
52 |
desctools_binom <- function(x1, n1, x2, n2, conf.level = 0.95, sides = c( # nolint |
|
53 |
"two.sided", |
|
54 |
"left", "right" |
|
55 |
), method = c( |
|
56 |
"ac", "wald", "waldcc", "score", |
|
57 |
"scorecc", "mn", "mee", "blj", "ha", "hal", "jp" |
|
58 |
)) { |
|
59 | 18x |
if (missing(sides)) { |
60 | 18x |
sides <- match.arg(sides) |
61 |
} |
|
62 | 18x |
if (missing(method)) { |
63 | 1x |
method <- match.arg(method) |
64 |
} |
|
65 | 18x |
iBinomDiffCI <- function(x1, n1, x2, n2, conf.level, sides, # nolint |
66 | 18x |
method) { |
67 | 18x |
if (sides != "two.sided") { |
68 | ! |
conf.level <- 1 - 2 * (1 - conf.level) # nolint |
69 |
} |
|
70 | 18x |
alpha <- 1 - conf.level |
71 | 18x |
kappa <- stats::qnorm(1 - alpha / 2) |
72 | 18x |
p1_hat <- x1 / n1 |
73 | 18x |
p2_hat <- x2 / n2 |
74 | 18x |
est <- p1_hat - p2_hat |
75 | 18x |
switch(method, |
76 | 18x |
wald = { |
77 | 2x |
vd <- p1_hat * (1 - p1_hat) / n1 + p2_hat * (1 - p2_hat) / n2 |
78 | 2x |
term2 <- kappa * sqrt(vd) |
79 | 2x |
ci_lwr <- max(-1, est - term2) |
80 | 2x |
ci_upr <- min(1, est + term2) |
81 |
}, |
|
82 | 18x |
waldcc = { |
83 | 2x |
vd <- p1_hat * (1 - p1_hat) / n1 + p2_hat * (1 - p2_hat) / n2 |
84 | 2x |
term2 <- kappa * sqrt(vd) |
85 | 2x |
term2 <- term2 + 0.5 * (1 / n1 + 1 / n2) |
86 | 2x |
ci_lwr <- max(-1, est - term2) |
87 | 2x |
ci_upr <- min(1, est + term2) |
88 |
}, |
|
89 | 18x |
ac = { |
90 | 2x |
n1 <- n1 + 2 |
91 | 2x |
n2 <- n2 + 2 |
92 | 2x |
x1 <- x1 + 1 |
93 | 2x |
x2 <- x2 + 1 |
94 | 2x |
p1_hat <- x1 / n1 |
95 | 2x |
p2_hat <- x2 / n2 |
96 | 2x |
est1 <- p1_hat - p2_hat |
97 | 2x |
vd <- p1_hat * (1 - p1_hat) / n1 + p2_hat * (1 - p2_hat) / n2 |
98 | 2x |
term2 <- kappa * sqrt(vd) |
99 | 2x |
ci_lwr <- max(-1, est1 - term2) |
100 | 2x |
ci_upr <- min(1, est1 + term2) |
101 |
}, |
|
102 | 18x |
exact = { |
103 | ! |
ci_lwr <- NA |
104 | ! |
ci_upr <- NA |
105 |
}, |
|
106 | 18x |
score = { |
107 | 2x |
w1 <- desctools_binomci( |
108 | 2x |
x = x1, n = n1, conf.level = conf.level, |
109 | 2x |
method = "wilson" |
110 |
) |
|
111 | 2x |
w2 <- desctools_binomci( |
112 | 2x |
x = x2, n = n2, conf.level = conf.level, |
113 | 2x |
method = "wilson" |
114 |
) |
|
115 | 2x |
l1 <- w1[2] |
116 | 2x |
u1 <- w1[3] |
117 | 2x |
l2 <- w2[2] |
118 | 2x |
u2 <- w2[3] |
119 | 2x |
ci_lwr <- est - kappa * sqrt(l1 * (1 - l1) / n1 + |
120 | 2x |
u2 * (1 - u2) / n2) |
121 | 2x |
ci_upr <- est + kappa * sqrt(u1 * (1 - u1) / n1 + |
122 | 2x |
l2 * (1 - l2) / n2) |
123 |
}, |
|
124 | 18x |
scorecc = { |
125 | 1x |
w1 <- desctools_binomci( |
126 | 1x |
x = x1, n = n1, conf.level = conf.level, |
127 | 1x |
method = "wilsoncc" |
128 |
) |
|
129 | 1x |
w2 <- desctools_binomci( |
130 | 1x |
x = x2, n = n2, conf.level = conf.level, |
131 | 1x |
method = "wilsoncc" |
132 |
) |
|
133 | 1x |
l1 <- w1[2] |
134 | 1x |
u1 <- w1[3] |
135 | 1x |
l2 <- w2[2] |
136 | 1x |
u2 <- w2[3] |
137 | 1x |
ci_lwr <- max(-1, est - sqrt((p1_hat - l1)^2 + |
138 | 1x |
(u2 - p2_hat)^2)) |
139 | 1x |
ci_upr <- min(1, est + sqrt((u1 - p1_hat)^2 + (p2_hat - |
140 | 1x |
l2)^2)) |
141 |
}, |
|
142 | 18x |
mee = { |
143 | 1x |
.score <- function(p1, n1, p2, n2, dif) { |
144 | ! |
if (dif > 1) dif <- 1 |
145 | ! |
if (dif < -1) dif <- -1 |
146 | 24x |
diff <- p1 - p2 - dif |
147 | 24x |
if (abs(diff) == 0) { |
148 | ! |
res <- 0 |
149 |
} else { |
|
150 | 24x |
t <- n2 / n1 |
151 | 24x |
a <- 1 + t |
152 | 24x |
b <- -(1 + t + p1 + t * p2 + dif * (t + 2)) |
153 | 24x |
c <- dif * dif + dif * (2 * p1 + t + 1) + p1 + |
154 | 24x |
t * p2 |
155 | 24x |
d <- -p1 * dif * (1 + dif) |
156 | 24x |
v <- (b / a / 3)^3 - b * c / (6 * a * a) + d / a / 2 |
157 | 24x |
if (abs(v) < .Machine$double.eps) v <- 0 |
158 | 24x |
s <- sqrt((b / a / 3)^2 - c / a / 3) |
159 | 24x |
u <- ifelse(v > 0, 1, -1) * s |
160 | 24x |
w <- (3.141592654 + acos(v / u^3)) / 3 |
161 | 24x |
p1d <- 2 * u * cos(w) - b / a / 3 |
162 | 24x |
p2d <- p1d - dif |
163 | 24x |
n <- n1 + n2 |
164 | 24x |
res <- (p1d * (1 - p1d) / n1 + p2d * (1 - p2d) / n2) |
165 |
} |
|
166 | 24x |
return(sqrt(res)) |
167 |
} |
|
168 | 1x |
pval <- function(delta) { |
169 | 24x |
z <- (est - delta) / .score( |
170 | 24x |
p1_hat, n1, p2_hat, |
171 | 24x |
n2, delta |
172 |
) |
|
173 | 24x |
2 * min(stats::pnorm(z), 1 - stats::pnorm(z)) |
174 |
} |
|
175 | 1x |
ci_lwr <- max(-1, stats::uniroot(function(delta) { |
176 | 12x |
pval(delta) - |
177 | 12x |
alpha |
178 | 1x |
}, interval = c(-1 + 1e-06, est - 1e-06))$root) |
179 | 1x |
ci_upr <- min(1, stats::uniroot(function(delta) { |
180 | 12x |
pval(delta) - |
181 | 12x |
alpha |
182 | 1x |
}, interval = c(est + 1e-06, 1 - 1e-06))$root) |
183 |
}, |
|
184 | 18x |
blj = { |
185 | 1x |
p1_dash <- (x1 + 0.5) / (n1 + 1) |
186 | 1x |
p2_dash <- (x2 + 0.5) / (n2 + 1) |
187 | 1x |
vd <- p1_dash * (1 - p1_dash) / n1 + p2_dash * (1 - |
188 | 1x |
p2_dash) / n2 |
189 | 1x |
term2 <- kappa * sqrt(vd) |
190 | 1x |
est_dash <- p1_dash - p2_dash |
191 | 1x |
ci_lwr <- max(-1, est_dash - term2) |
192 | 1x |
ci_upr <- min(1, est_dash + term2) |
193 |
}, |
|
194 | 18x |
ha = { |
195 | 4x |
term2 <- 1 / (2 * min(n1, n2)) + kappa * sqrt(p1_hat * |
196 | 4x |
(1 - p1_hat) / (n1 - 1) + p2_hat * (1 - p2_hat) / (n2 - |
197 | 4x |
1)) |
198 | 4x |
ci_lwr <- max(-1, est - term2) |
199 | 4x |
ci_upr <- min(1, est + term2) |
200 |
}, |
|
201 | 18x |
mn = { |
202 | 1x |
.conf <- function(x1, n1, x2, n2, z, lower = FALSE) { |
203 | 2x |
p1 <- x1 / n1 |
204 | 2x |
p2 <- x2 / n2 |
205 | 2x |
p_hat <- p1 - p2 |
206 | 2x |
dp <- 1 + ifelse(lower, 1, -1) * p_hat |
207 | 2x |
i <- 1 |
208 | 2x |
while (i <= 50) { |
209 | 46x |
dp <- 0.5 * dp |
210 | 46x |
y <- p_hat + ifelse(lower, -1, 1) * dp |
211 | 46x |
score <- .score(p1, n1, p2, n2, y) |
212 | 46x |
if (score < z) { |
213 | 20x |
p_hat <- y |
214 |
} |
|
215 | 46x |
if ((dp < 1e-07) || (abs(z - score) < 1e-06)) { |
216 | 2x |
(break)() |
217 |
} else { |
|
218 | 44x |
i <- i + |
219 | 44x |
1 |
220 |
} |
|
221 |
} |
|
222 | 2x |
return(y) |
223 |
} |
|
224 | 1x |
.score <- function(p1, n1, p2, n2, dif) { |
225 | 46x |
diff <- p1 - p2 - dif |
226 | 46x |
if (abs(diff) == 0) { |
227 | ! |
res <- 0 |
228 |
} else { |
|
229 | 46x |
t <- n2 / n1 |
230 | 46x |
a <- 1 + t |
231 | 46x |
b <- -(1 + t + p1 + t * p2 + dif * (t + 2)) |
232 | 46x |
c <- dif * dif + dif * (2 * p1 + t + 1) + p1 + |
233 | 46x |
t * p2 |
234 | 46x |
d <- -p1 * dif * (1 + dif) |
235 | 46x |
v <- (b / a / 3)^3 - b * c / (6 * a * a) + d / a / 2 |
236 | 46x |
s <- sqrt((b / a / 3)^2 - c / a / 3) |
237 | 46x |
u <- ifelse(v > 0, 1, -1) * s |
238 | 46x |
w <- (3.141592654 + acos(v / u^3)) / 3 |
239 | 46x |
p1d <- 2 * u * cos(w) - b / a / 3 |
240 | 46x |
p2d <- p1d - dif |
241 | 46x |
n <- n1 + n2 |
242 | 46x |
var <- (p1d * (1 - p1d) / n1 + p2d * (1 - p2d) / n2) * |
243 | 46x |
n / (n - 1) |
244 | 46x |
res <- diff^2 / var |
245 |
} |
|
246 | 46x |
return(res) |
247 |
} |
|
248 | 1x |
z <- stats::qchisq(conf.level, 1) |
249 | 1x |
ci_lwr <- max(-1, .conf(x1, n1, x2, n2, z, TRUE)) |
250 | 1x |
ci_upr <- min(1, .conf(x1, n1, x2, n2, z, FALSE)) |
251 |
}, |
|
252 | 18x |
beal = { |
253 | ! |
a <- p1_hat + p2_hat |
254 | ! |
b <- p1_hat - p2_hat |
255 | ! |
u <- ((1 / n1) + (1 / n2)) / 4 |
256 | ! |
v <- ((1 / n1) - (1 / n2)) / 4 |
257 | ! |
V <- u * ((2 - a) * a - b^2) + 2 * v * (1 - a) * b # nolint |
258 | ! |
z <- stats::qchisq(p = 1 - alpha / 2, df = 1) |
259 | ! |
A <- sqrt(z * (V + z * u^2 * (2 - a) * a + z * v^2 * (1 - a)^2)) # nolint |
260 | ! |
B <- (b + z * v * (1 - a)) / (1 + z * u) # nolint |
261 | ! |
ci_lwr <- max(-1, B - A / (1 + z * u)) |
262 | ! |
ci_upr <- min(1, B + A / (1 + z * u)) |
263 |
}, |
|
264 | 18x |
hal = { |
265 | 1x |
psi <- (p1_hat + p2_hat) / 2 |
266 | 1x |
u <- (1 / n1 + 1 / n2) / 4 |
267 | 1x |
v <- (1 / n1 - 1 / n2) / 4 |
268 | 1x |
z <- kappa |
269 | 1x |
theta <- ((p1_hat - p2_hat) + z^2 * v * (1 - 2 * |
270 | 1x |
psi)) / (1 + z^2 * u) |
271 | 1x |
w <- z / (1 + z^2 * u) * sqrt(u * (4 * psi * (1 - psi) - |
272 | 1x |
(p1_hat - p2_hat)^2) + 2 * v * (1 - 2 * psi) * |
273 | 1x |
(p1_hat - p2_hat) + 4 * z^2 * u^2 * (1 - psi) * |
274 | 1x |
psi + z^2 * v^2 * (1 - 2 * psi)^2) |
275 | 1x |
c(theta + w, theta - w) |
276 | 1x |
ci_lwr <- max(-1, theta - w) |
277 | 1x |
ci_upr <- min(1, theta + w) |
278 |
}, |
|
279 | 18x |
jp = { |
280 | 1x |
psi <- 0.5 * ((x1 + 0.5) / (n1 + 1) + (x2 + 0.5) / (n2 + |
281 | 1x |
1)) |
282 | 1x |
u <- (1 / n1 + 1 / n2) / 4 |
283 | 1x |
v <- (1 / n1 - 1 / n2) / 4 |
284 | 1x |
z <- kappa |
285 | 1x |
theta <- ((p1_hat - p2_hat) + z^2 * v * (1 - 2 * |
286 | 1x |
psi)) / (1 + z^2 * u) |
287 | 1x |
w <- z / (1 + z^2 * u) * sqrt(u * (4 * psi * (1 - psi) - |
288 | 1x |
(p1_hat - p2_hat)^2) + 2 * v * (1 - 2 * psi) * |
289 | 1x |
(p1_hat - p2_hat) + 4 * z^2 * u^2 * (1 - psi) * |
290 | 1x |
psi + z^2 * v^2 * (1 - 2 * psi)^2) |
291 | 1x |
c(theta + w, theta - w) |
292 | 1x |
ci_lwr <- max(-1, theta - w) |
293 | 1x |
ci_upr <- min(1, theta + w) |
294 |
}, |
|
295 |
) |
|
296 | 18x |
ci <- c( |
297 | 18x |
est = est, lwr.ci = min(ci_lwr, ci_upr), |
298 | 18x |
upr.ci = max(ci_lwr, ci_upr) |
299 |
) |
|
300 | 18x |
if (sides == "left") { |
301 | ! |
ci[3] <- 1 |
302 | 18x |
} else if (sides == "right") { |
303 | ! |
ci[2] <- -1 |
304 |
} |
|
305 | 18x |
return(ci) |
306 |
} |
|
307 | 18x |
method <- match.arg(arg = method, several.ok = TRUE) |
308 | 18x |
sides <- match.arg(arg = sides, several.ok = TRUE) |
309 | 18x |
lst <- h_recycle( |
310 | 18x |
x1 = x1, n1 = n1, x2 = x2, n2 = n2, conf.level = conf.level, |
311 | 18x |
sides = sides, method = method |
312 |
) |
|
313 | 18x |
res <- t(sapply(1:attr(lst, "maxdim"), function(i) { |
314 | 18x |
iBinomDiffCI( |
315 | 18x |
x1 = lst$x1[i], |
316 | 18x |
n1 = lst$n1[i], x2 = lst$x2[i], n2 = lst$n2[i], conf.level = lst$conf.level[i], |
317 | 18x |
sides = lst$sides[i], method = lst$method[i] |
318 |
) |
|
319 |
})) |
|
320 | 18x |
lgn <- h_recycle(x1 = if (is.null(names(x1))) { |
321 | 18x |
paste("x1", seq_along(x1), sep = ".") |
322 |
} else { |
|
323 | ! |
names(x1) |
324 | 18x |
}, n1 = if (is.null(names(n1))) { |
325 | 18x |
paste("n1", seq_along(n1), sep = ".") |
326 |
} else { |
|
327 | ! |
names(n1) |
328 | 18x |
}, x2 = if (is.null(names(x2))) { |
329 | 18x |
paste("x2", seq_along(x2), sep = ".") |
330 |
} else { |
|
331 | ! |
names(x2) |
332 | 18x |
}, n2 = if (is.null(names(n2))) { |
333 | 18x |
paste("n2", seq_along(n2), sep = ".") |
334 |
} else { |
|
335 | ! |
names(n2) |
336 | 18x |
}, conf.level = conf.level, sides = sides, method = method) |
337 | 18x |
xn <- apply(as.data.frame(lgn[sapply(lgn, function(x) { |
338 | 126x |
length(unique(x)) != |
339 | 126x |
1 |
340 | 18x |
})]), 1, paste, collapse = ":") |
341 | 18x |
rownames(res) <- xn |
342 | 18x |
return(res) |
343 |
} |
|
344 | ||
345 |
#' @describeIn desctools_binom Compute confidence intervals for binomial proportions. |
|
346 |
#' |
|
347 |
#' @param x (`count`)\cr number of successes |
|
348 |
#' @param n (`count`)\cr number of trials |
|
349 |
#' @param conf.level (`proportion`)\cr confidence level, defaults to 0.95. |
|
350 |
#' @param sides (`character`)\cr side of the confidence interval to compute. Must be one of "two-sided" (default), |
|
351 |
#' "left", or "right". |
|
352 |
#' @param method (`character`)\cr method to use. Can be one out of: "wald", "wilson", "wilsoncc", "agresti-coull", |
|
353 |
#' "jeffreys", "modified wilson", "modified jeffreys", "clopper-pearson", "arcsine", "logit", "witting", "pratt", |
|
354 |
#' "midp", "lik", and "blaker". |
|
355 |
#' |
|
356 |
#' @return A `matrix` with 3 columns containing: |
|
357 |
#' * `est`: estimate of proportion difference. |
|
358 |
#' * `lwr.ci`: lower end of the confidence interval. |
|
359 |
#' * `upr.ci`: upper end of the confidence interval. |
|
360 |
#' |
|
361 |
#' @keywords internal |
|
362 |
desctools_binomci <- function(x, |
|
363 |
n, |
|
364 |
conf.level = 0.95, # nolint |
|
365 |
sides = c("two.sided", "left", "right"), |
|
366 |
method = c( |
|
367 |
"wilson", "wald", "waldcc", "agresti-coull", |
|
368 |
"jeffreys", "modified wilson", "wilsoncc", "modified jeffreys", |
|
369 |
"clopper-pearson", "arcsine", "logit", "witting", "pratt", |
|
370 |
"midp", "lik", "blaker" |
|
371 |
), |
|
372 |
rand = 123, |
|
373 |
tol = 1e-05) { |
|
374 | 24x |
if (missing(method)) { |
375 | 1x |
method <- "wilson" |
376 |
} |
|
377 | 24x |
if (missing(sides)) { |
378 | 23x |
sides <- "two.sided" |
379 |
} |
|
380 | 24x |
iBinomCI <- function(x, n, conf.level = 0.95, sides = c( # nolint |
381 | 24x |
"two.sided", |
382 | 24x |
"left", "right" |
383 | 24x |
), method = c( |
384 | 24x |
"wilson", "wilsoncc", "wald", |
385 | 24x |
"waldcc", "agresti-coull", "jeffreys", "modified wilson", |
386 | 24x |
"modified jeffreys", "clopper-pearson", "arcsine", "logit", |
387 | 24x |
"witting", "pratt", "midp", "lik", "blaker" |
388 | 24x |
), rand = 123, |
389 | 24x |
tol = 1e-05) { |
390 | 24x |
if (length(x) != 1) { |
391 | ! |
stop("'x' has to be of length 1 (number of successes)") |
392 |
} |
|
393 | 24x |
if (length(n) != 1) { |
394 | ! |
stop("'n' has to be of length 1 (number of trials)") |
395 |
} |
|
396 | 24x |
if (length(conf.level) != 1) { |
397 | ! |
stop("'conf.level' has to be of length 1 (confidence level)") |
398 |
} |
|
399 | 24x |
if (conf.level < 0.5 || conf.level > 1) { |
400 | ! |
stop("'conf.level' has to be in [0.5, 1]") |
401 |
} |
|
402 | 24x |
sides <- match.arg(sides, choices = c( |
403 | 24x |
"two.sided", "left", |
404 | 24x |
"right" |
405 | 24x |
), several.ok = FALSE) |
406 | 24x |
if (sides != "two.sided") { |
407 | 1x |
conf.level <- 1 - 2 * (1 - conf.level) # nolint |
408 |
} |
|
409 | 24x |
alpha <- 1 - conf.level |
410 | 24x |
kappa <- stats::qnorm(1 - alpha / 2) |
411 | 24x |
p_hat <- x / n |
412 | 24x |
q_hat <- 1 - p_hat |
413 | 24x |
est <- p_hat |
414 | 24x |
switch(match.arg(arg = method, choices = c( |
415 | 24x |
"wilson", |
416 | 24x |
"wald", "waldcc", "wilsoncc", "agresti-coull", "jeffreys", |
417 | 24x |
"modified wilson", "modified jeffreys", "clopper-pearson", |
418 | 24x |
"arcsine", "logit", "witting", "pratt", "midp", "lik", |
419 | 24x |
"blaker" |
420 |
)), |
|
421 | 24x |
wald = { |
422 | 1x |
term2 <- kappa * sqrt(p_hat * q_hat) / sqrt(n) |
423 | 1x |
ci_lwr <- max(0, p_hat - term2) |
424 | 1x |
ci_upr <- min(1, p_hat + term2) |
425 |
}, |
|
426 | 24x |
waldcc = { |
427 | 1x |
term2 <- kappa * sqrt(p_hat * q_hat) / sqrt(n) |
428 | 1x |
term2 <- term2 + 1 / (2 * n) |
429 | 1x |
ci_lwr <- max(0, p_hat - term2) |
430 | 1x |
ci_upr <- min(1, p_hat + term2) |
431 |
}, |
|
432 | 24x |
wilson = { |
433 | 6x |
term1 <- (x + kappa^2 / 2) / (n + kappa^2) |
434 | 6x |
term2 <- kappa * sqrt(n) / (n + kappa^2) * sqrt(p_hat * |
435 | 6x |
q_hat + kappa^2 / (4 * n)) |
436 | 6x |
ci_lwr <- max(0, term1 - term2) |
437 | 6x |
ci_upr <- min(1, term1 + term2) |
438 |
}, |
|
439 | 24x |
wilsoncc = { |
440 | 3x |
lci <- (2 * x + kappa^2 - 1 - kappa * sqrt(kappa^2 - |
441 | 3x |
2 - 1 / n + 4 * p_hat * (n * q_hat + 1))) / (2 * |
442 | 3x |
(n + kappa^2)) |
443 | 3x |
uci <- (2 * x + kappa^2 + 1 + kappa * sqrt(kappa^2 + |
444 | 3x |
2 - 1 / n + 4 * p_hat * (n * q_hat - 1))) / (2 * |
445 | 3x |
(n + kappa^2)) |
446 | 3x |
ci_lwr <- max(0, ifelse(p_hat == 0, 0, lci)) |
447 | 3x |
ci_upr <- min(1, ifelse(p_hat == 1, 1, uci)) |
448 |
}, |
|
449 | 24x |
`agresti-coull` = { |
450 | 1x |
x_tilde <- x + kappa^2 / 2 |
451 | 1x |
n_tilde <- n + kappa^2 |
452 | 1x |
p_tilde <- x_tilde / n_tilde |
453 | 1x |
q_tilde <- 1 - p_tilde |
454 | 1x |
est <- p_tilde |
455 | 1x |
term2 <- kappa * sqrt(p_tilde * q_tilde) / sqrt(n_tilde) |
456 | 1x |
ci_lwr <- max(0, p_tilde - term2) |
457 | 1x |
ci_upr <- min(1, p_tilde + term2) |
458 |
}, |
|
459 | 24x |
jeffreys = { |
460 | 1x |
if (x == 0) { |
461 | ! |
ci_lwr <- 0 |
462 |
} else { |
|
463 | 1x |
ci_lwr <- stats::qbeta( |
464 | 1x |
alpha / 2, |
465 | 1x |
x + 0.5, n - x + 0.5 |
466 |
) |
|
467 |
} |
|
468 | 1x |
if (x == n) { |
469 | ! |
ci_upr <- 1 |
470 |
} else { |
|
471 | 1x |
ci_upr <- stats::qbeta(1 - |
472 | 1x |
alpha / 2, x + 0.5, n - x + 0.5) |
473 |
} |
|
474 |
}, |
|
475 | 24x |
`modified wilson` = { |
476 | 1x |
term1 <- (x + kappa^2 / 2) / (n + kappa^2) |
477 | 1x |
term2 <- kappa * sqrt(n) / (n + kappa^2) * sqrt(p_hat * |
478 | 1x |
q_hat + kappa^2 / (4 * n)) |
479 | 1x |
if ((n <= 50 & x %in% c(1, 2)) | (n >= 51 & x %in% |
480 | 1x |
c(1:3))) { |
481 | ! |
ci_lwr <- 0.5 * stats::qchisq(alpha, 2 * |
482 | ! |
x) / n |
483 |
} else { |
|
484 | 1x |
ci_lwr <- max(0, term1 - term2) |
485 |
} |
|
486 | 1x |
if ((n <= 50 & x %in% c(n - 1, n - 2)) | (n >= 51 & |
487 | 1x |
x %in% c(n - (1:3)))) { |
488 | ! |
ci_upr <- 1 - 0.5 * stats::qchisq( |
489 | ! |
alpha, |
490 | ! |
2 * (n - x) |
491 | ! |
) / n |
492 |
} else { |
|
493 | 1x |
ci_upr <- min(1, term1 + |
494 | 1x |
term2) |
495 |
} |
|
496 |
}, |
|
497 | 24x |
`modified jeffreys` = { |
498 | 1x |
if (x == n) { |
499 | ! |
ci_lwr <- (alpha / 2)^(1 / n) |
500 |
} else { |
|
501 | 1x |
if (x <= 1) { |
502 | ! |
ci_lwr <- 0 |
503 |
} else { |
|
504 | 1x |
ci_lwr <- stats::qbeta( |
505 | 1x |
alpha / 2, |
506 | 1x |
x + 0.5, n - x + 0.5 |
507 |
) |
|
508 |
} |
|
509 |
} |
|
510 | 1x |
if (x == 0) { |
511 | ! |
ci_upr <- 1 - (alpha / 2)^(1 / n) |
512 |
} else { |
|
513 | 1x |
if (x >= n - 1) { |
514 | ! |
ci_upr <- 1 |
515 |
} else { |
|
516 | 1x |
ci_upr <- stats::qbeta(1 - |
517 | 1x |
alpha / 2, x + 0.5, n - x + 0.5) |
518 |
} |
|
519 |
} |
|
520 |
}, |
|
521 | 24x |
`clopper-pearson` = { |
522 | 1x |
ci_lwr <- stats::qbeta(alpha / 2, x, n - x + 1) |
523 | 1x |
ci_upr <- stats::qbeta(1 - alpha / 2, x + 1, n - x) |
524 |
}, |
|
525 | 24x |
arcsine = { |
526 | 1x |
p_tilde <- (x + 0.375) / (n + 0.75) |
527 | 1x |
est <- p_tilde |
528 | 1x |
ci_lwr <- sin(asin(sqrt(p_tilde)) - 0.5 * kappa / sqrt(n))^2 |
529 | 1x |
ci_upr <- sin(asin(sqrt(p_tilde)) + 0.5 * kappa / sqrt(n))^2 |
530 |
}, |
|
531 | 24x |
logit = { |
532 | 1x |
lambda_hat <- log(x / (n - x)) |
533 | 1x |
V_hat <- n / (x * (n - x)) # nolint |
534 | 1x |
lambda_lower <- lambda_hat - kappa * sqrt(V_hat) |
535 | 1x |
lambda_upper <- lambda_hat + kappa * sqrt(V_hat) |
536 | 1x |
ci_lwr <- exp(lambda_lower) / (1 + exp(lambda_lower)) |
537 | 1x |
ci_upr <- exp(lambda_upper) / (1 + exp(lambda_upper)) |
538 |
}, |
|
539 | 24x |
witting = { |
540 | 1x |
set.seed(rand) |
541 | 1x |
x_tilde <- x + stats::runif(1, min = 0, max = 1) |
542 | 1x |
pbinom_abscont <- function(q, size, prob) { |
543 | 22x |
v <- trunc(q) |
544 | 22x |
term1 <- stats::pbinom(v - 1, size = size, prob = prob) |
545 | 22x |
term2 <- (q - v) * stats::dbinom(v, size = size, prob = prob) |
546 | 22x |
return(term1 + term2) |
547 |
} |
|
548 | 1x |
qbinom_abscont <- function(p, size, x) { |
549 | 2x |
fun <- function(prob, size, x, p) { |
550 | 22x |
pbinom_abscont(x, size, prob) - p |
551 |
} |
|
552 | 2x |
stats::uniroot(fun, |
553 | 2x |
interval = c(0, 1), size = size, |
554 | 2x |
x = x, p = p |
555 | 2x |
)$root |
556 |
} |
|
557 | 1x |
ci_lwr <- qbinom_abscont(1 - alpha, size = n, x = x_tilde) |
558 | 1x |
ci_upr <- qbinom_abscont(alpha, size = n, x = x_tilde) |
559 |
}, |
|
560 | 24x |
pratt = { |
561 | 1x |
if (x == 0) { |
562 | ! |
ci_lwr <- 0 |
563 | ! |
ci_upr <- 1 - alpha^(1 / n) |
564 | 1x |
} else if (x == 1) { |
565 | ! |
ci_lwr <- 1 - (1 - alpha / 2)^(1 / n) |
566 | ! |
ci_upr <- 1 - (alpha / 2)^(1 / n) |
567 | 1x |
} else if (x == (n - 1)) { |
568 | ! |
ci_lwr <- (alpha / 2)^(1 / n) |
569 | ! |
ci_upr <- (1 - alpha / 2)^(1 / n) |
570 | 1x |
} else if (x == n) { |
571 | ! |
ci_lwr <- alpha^(1 / n) |
572 | ! |
ci_upr <- 1 |
573 |
} else { |
|
574 | 1x |
z <- stats::qnorm(1 - alpha / 2) |
575 | 1x |
A <- ((x + 1) / (n - x))^2 # nolint |
576 | 1x |
B <- 81 * (x + 1) * (n - x) - 9 * n - 8 # nolint |
577 | 1x |
C <- (0 - 3) * z * sqrt(9 * (x + 1) * (n - x) * (9 * n + 5 - z^2) + n + 1) # nolint |
578 | 1x |
D <- 81 * (x + 1)^2 - 9 * (x + 1) * (2 + z^2) + 1 # nolint |
579 | 1x |
E <- 1 + A * ((B + C) / D)^3 # nolint |
580 | 1x |
ci_upr <- 1 / E |
581 | 1x |
A <- (x / (n - x - 1))^2 # nolint |
582 | 1x |
B <- 81 * x * (n - x - 1) - 9 * n - 8 # nolint |
583 | 1x |
C <- 3 * z * sqrt(9 * x * (n - x - 1) * (9 * n + 5 - z^2) + n + 1) # nolint |
584 | 1x |
D <- 81 * x^2 - 9 * x * (2 + z^2) + 1 # nolint |
585 | 1x |
E <- 1 + A * ((B + C) / D)^3 # nolint |
586 | 1x |
ci_lwr <- 1 / E |
587 |
} |
|
588 |
}, |
|
589 | 24x |
midp = { |
590 | 1x |
f_low <- function(pi, x, n) { |
591 | 12x |
1 / 2 * stats::dbinom(x, size = n, prob = pi) + stats::pbinom(x, |
592 | 12x |
size = n, prob = pi, lower.tail = FALSE |
593 |
) - |
|
594 | 12x |
(1 - conf.level) / 2 |
595 |
} |
|
596 | 1x |
f_up <- function(pi, x, n) { |
597 | 12x |
1 / 2 * stats::dbinom(x, size = n, prob = pi) + stats::pbinom(x - |
598 | 12x |
1, size = n, prob = pi) - (1 - conf.level) / 2 |
599 |
} |
|
600 | 1x |
ci_lwr <- 0 |
601 | 1x |
ci_upr <- 1 |
602 | 1x |
if (x != 0) { |
603 | 1x |
ci_lwr <- stats::uniroot(f_low, |
604 | 1x |
interval = c(0, p_hat), |
605 | 1x |
x = x, n = n |
606 | 1x |
)$root |
607 |
} |
|
608 | 1x |
if (x != n) { |
609 | 1x |
ci_upr <- stats::uniroot(f_up, interval = c( |
610 | 1x |
p_hat, |
611 | 1x |
1 |
612 | 1x |
), x = x, n = n)$root |
613 |
} |
|
614 |
}, |
|
615 | 24x |
lik = { |
616 | 2x |
ci_lwr <- 0 |
617 | 2x |
ci_upr <- 1 |
618 | 2x |
z <- stats::qnorm(1 - alpha * 0.5) |
619 | 2x |
tol <- .Machine$double.eps^0.5 |
620 | 2x |
BinDev <- function(y, x, mu, wt, bound = 0, tol = .Machine$double.eps^0.5, # nolint |
621 |
...) { |
|
622 | 40x |
ll_y <- ifelse(y %in% c(0, 1), 0, stats::dbinom(x, wt, |
623 | 40x |
y, |
624 | 40x |
log = TRUE |
625 |
)) |
|
626 | 40x |
ll_mu <- ifelse(mu %in% c(0, 1), 0, stats::dbinom(x, |
627 | 40x |
wt, mu, |
628 | 40x |
log = TRUE |
629 |
)) |
|
630 | 40x |
res <- ifelse(abs(y - mu) < tol, 0, sign(y - |
631 | 40x |
mu) * sqrt(-2 * (ll_y - ll_mu))) |
632 | 40x |
return(res - bound) |
633 |
} |
|
634 | 2x |
if (x != 0 && tol < p_hat) { |
635 | 2x |
ci_lwr <- if (BinDev( |
636 | 2x |
tol, x, p_hat, n, -z, |
637 | 2x |
tol |
638 | 2x |
) <= 0) { |
639 | 2x |
stats::uniroot( |
640 | 2x |
f = BinDev, interval = c(tol, if (p_hat < |
641 | 2x |
tol || p_hat == 1) { |
642 | ! |
1 - tol |
643 |
} else { |
|
644 | 2x |
p_hat |
645 | 2x |
}), bound = -z, |
646 | 2x |
x = x, mu = p_hat, wt = n |
647 | 2x |
)$root |
648 |
} |
|
649 |
} |
|
650 | 2x |
if (x != n && p_hat < (1 - tol)) { |
651 | 2x |
ci_upr <- if (BinDev(y = 1 - tol, x = x, mu = ifelse(p_hat > |
652 | 2x |
1 - tol, tol, p_hat), wt = n, bound = z, tol = tol) < |
653 | 2x |
0) { |
654 | ! |
ci_lwr <- if (BinDev( |
655 | ! |
tol, x, if (p_hat < |
656 | ! |
tol || p_hat == 1) { |
657 | ! |
1 - tol |
658 |
} else { |
|
659 | ! |
p_hat |
660 | ! |
}, n, |
661 | ! |
-z, tol |
662 | ! |
) <= 0) { |
663 | ! |
stats::uniroot( |
664 | ! |
f = BinDev, interval = c(tol, p_hat), |
665 | ! |
bound = -z, x = x, mu = p_hat, wt = n |
666 | ! |
)$root |
667 |
} |
|
668 |
} else { |
|
669 | 2x |
stats::uniroot( |
670 | 2x |
f = BinDev, interval = c(if (p_hat > |
671 | 2x |
1 - tol) { |
672 | ! |
tol |
673 |
} else { |
|
674 | 2x |
p_hat |
675 | 2x |
}, 1 - tol), bound = z, |
676 | 2x |
x = x, mu = p_hat, wt = n |
677 | 2x |
)$root |
678 |
} |
|
679 |
} |
|
680 |
}, |
|
681 | 24x |
blaker = { |
682 | 1x |
acceptbin <- function(x, n, p) { |
683 | 3954x |
p1 <- 1 - stats::pbinom(x - 1, n, p) |
684 | 3954x |
p2 <- stats::pbinom(x, n, p) |
685 | 3954x |
a1 <- p1 + stats::pbinom(stats::qbinom(p1, n, p) - 1, n, p) |
686 | 3954x |
a2 <- p2 + 1 - stats::pbinom( |
687 | 3954x |
stats::qbinom(1 - p2, n, p), n, |
688 | 3954x |
p |
689 |
) |
|
690 | 3954x |
return(min(a1, a2)) |
691 |
} |
|
692 | 1x |
ci_lwr <- 0 |
693 | 1x |
ci_upr <- 1 |
694 | 1x |
if (x != 0) { |
695 | 1x |
ci_lwr <- stats::qbeta((1 - conf.level) / 2, x, n - |
696 | 1x |
x + 1) |
697 | 1x |
while (acceptbin(x, n, ci_lwr + tol) < (1 - |
698 | 1x |
conf.level)) { |
699 | 1976x |
ci_lwr <- ci_lwr + tol |
700 |
} |
|
701 |
} |
|
702 | 1x |
if (x != n) { |
703 | 1x |
ci_upr <- stats::qbeta(1 - (1 - conf.level) / 2, x + |
704 | 1x |
1, n - x) |
705 | 1x |
while (acceptbin(x, n, ci_upr - tol) < (1 - |
706 | 1x |
conf.level)) { |
707 | 1976x |
ci_upr <- ci_upr - tol |
708 |
} |
|
709 |
} |
|
710 |
} |
|
711 |
) |
|
712 | 24x |
ci <- c(est = est, lwr.ci = max(0, ci_lwr), upr.ci = min( |
713 | 24x |
1, |
714 | 24x |
ci_upr |
715 |
)) |
|
716 | 24x |
if (sides == "left") { |
717 | 1x |
ci[3] <- 1 |
718 | 23x |
} else if (sides == "right") { |
719 | ! |
ci[2] <- 0 |
720 |
} |
|
721 | 24x |
return(ci) |
722 |
} |
|
723 | 24x |
lst <- list( |
724 | 24x |
x = x, n = n, conf.level = conf.level, sides = sides, |
725 | 24x |
method = method, rand = rand |
726 |
) |
|
727 | 24x |
maxdim <- max(unlist(lapply(lst, length))) |
728 | 24x |
lgp <- lapply(lst, rep, length.out = maxdim) |
729 | 24x |
lgn <- h_recycle(x = if (is.null(names(x))) { |
730 | 24x |
paste("x", seq_along(x), sep = ".") |
731 |
} else { |
|
732 | ! |
names(x) |
733 | 24x |
}, n = if (is.null(names(n))) { |
734 | 24x |
paste("n", seq_along(n), sep = ".") |
735 |
} else { |
|
736 | ! |
names(n) |
737 | 24x |
}, conf.level = conf.level, sides = sides, method = method) |
738 | 24x |
xn <- apply(as.data.frame(lgn[sapply(lgn, function(x) { |
739 | 120x |
length(unique(x)) != |
740 | 120x |
1 |
741 | 24x |
})]), 1, paste, collapse = ":") |
742 | 24x |
res <- t(sapply(1:maxdim, function(i) { |
743 | 24x |
iBinomCI( |
744 | 24x |
x = lgp$x[i], |
745 | 24x |
n = lgp$n[i], conf.level = lgp$conf.level[i], sides = lgp$sides[i], |
746 | 24x |
method = lgp$method[i], rand = lgp$rand[i] |
747 |
) |
|
748 |
})) |
|
749 | 24x |
colnames(res)[1] <- c("est") |
750 | 24x |
rownames(res) <- xn |
751 | 24x |
return(res) |
752 |
} |
1 |
#' Create a Forest Plot based on a Table |
|
2 |
#' |
|
3 |
#' Create a forest plot from any [rtables::rtable()] object that has a |
|
4 |
#' column with a single value and a column with 2 values. |
|
5 |
#' |
|
6 |
#' @description `r lifecycle::badge("stable")` |
|
7 |
#' |
|
8 |
#' @inheritParams argument_convention |
|
9 |
#' @param tbl (`rtable`) |
|
10 |
#' @param col_x (`integer`)\cr column index with estimator. By default tries to get this from |
|
11 |
#' `tbl` attribute `col_x`, otherwise needs to be manually specified. |
|
12 |
#' @param col_ci (`integer`)\cr column index with confidence intervals. By default tries |
|
13 |
#' to get this from `tbl` attribute `col_ci`, otherwise needs to be manually specified. |
|
14 |
#' @param vline (`number`)\cr x coordinate for vertical line, if `NULL` then the line is omitted. |
|
15 |
#' @param forest_header (`character`, length 2)\cr text displayed to the left and right of `vline`, respectively. |
|
16 |
#' If `vline = NULL` then `forest_header` needs to be `NULL` too. |
|
17 |
#' By default tries to get this from `tbl` attribute `forest_header`. |
|
18 |
#' @param xlim (`numeric`)\cr limits for x axis. |
|
19 |
#' @param logx (`flag`)\cr show the x-values on logarithm scale. |
|
20 |
#' @param x_at (`numeric`)\cr x-tick locations, if `NULL` they get automatically chosen. |
|
21 |
#' @param width_row_names (`unit`)\cr width for row names. |
|
22 |
#' If `NULL` the widths get automatically calculated. See [grid::unit()]. |
|
23 |
#' @param width_columns (`unit`)\cr widths for the table columns. |
|
24 |
#' If `NULL` the widths get automatically calculated. See [grid::unit()]. |
|
25 |
#' @param width_forest (`unit`)\cr width for the forest column. |
|
26 |
#' If `NULL` the widths get automatically calculated. See [grid::unit()]. |
|
27 |
#' @param col_symbol_size (`integer`)\cr column index from `tbl` containing data to be used |
|
28 |
#' to determine relative size for estimator plot symbol. Typically, the symbol size is proportional |
|
29 |
#' to the sample size used to calculate the estimator. If `NULL`, the same symbol size is used for all subgroups. |
|
30 |
#' By default tries to get this from `tbl` attribute `col_symbol_size`, otherwise needs to be manually specified. |
|
31 |
#' @param col (`character`)\cr color(s). |
|
32 |
#' |
|
33 |
#' @return `gTree` object containing the forest plot and table. |
|
34 |
#' |
|
35 |
#' @examples |
|
36 |
#' \dontrun{ |
|
37 |
#' library(dplyr) |
|
38 |
#' library(forcats) |
|
39 |
#' library(nestcolor) |
|
40 |
#' |
|
41 |
#' adrs <- tern_ex_adrs |
|
42 |
#' n_records <- 20 |
|
43 |
#' adrs_labels <- formatters::var_labels(adrs, fill = TRUE) |
|
44 |
#' adrs <- adrs %>% |
|
45 |
#' filter(PARAMCD == "BESRSPI") %>% |
|
46 |
#' filter(ARM %in% c("A: Drug X", "B: Placebo")) %>% |
|
47 |
#' slice(seq_len(n_records)) %>% |
|
48 |
#' droplevels() %>% |
|
49 |
#' mutate( |
|
50 |
#' # Reorder levels of factor to make the placebo group the reference arm. |
|
51 |
#' ARM = fct_relevel(ARM, "B: Placebo"), |
|
52 |
#' rsp = AVALC == "CR" |
|
53 |
#' ) |
|
54 |
#' formatters::var_labels(adrs) <- c(adrs_labels, "Response") |
|
55 |
#' df <- extract_rsp_subgroups( |
|
56 |
#' variables = list(rsp = "rsp", arm = "ARM", subgroups = c("SEX", "STRATA2")), |
|
57 |
#' data = adrs |
|
58 |
#' ) |
|
59 |
#' # Full commonly used response table. |
|
60 |
#' |
|
61 |
#' tbl <- basic_table() %>% |
|
62 |
#' tabulate_rsp_subgroups(df) |
|
63 |
#' p <- g_forest(tbl) |
|
64 |
#' |
|
65 |
#' draw_grob(p) |
|
66 |
#' |
|
67 |
#' # Odds ratio only table. |
|
68 |
#' |
|
69 |
#' tbl_or <- basic_table() %>% |
|
70 |
#' tabulate_rsp_subgroups(df, vars = c("n_tot", "or", "ci")) |
|
71 |
#' tbl_or |
|
72 |
#' p <- g_forest( |
|
73 |
#' tbl_or, |
|
74 |
#' forest_header = c("Comparison\nBetter", "Treatment\nBetter") |
|
75 |
#' ) |
|
76 |
#' |
|
77 |
#' draw_grob(p) |
|
78 |
#' |
|
79 |
#' # Survival forest plot example. |
|
80 |
#' adtte <- tern_ex_adtte |
|
81 |
#' # Save variable labels before data processing steps. |
|
82 |
#' adtte_labels <- formatters::var_labels(adtte, fill = TRUE) |
|
83 |
#' adtte_f <- adtte %>% |
|
84 |
#' filter( |
|
85 |
#' PARAMCD == "OS", |
|
86 |
#' ARM %in% c("B: Placebo", "A: Drug X"), |
|
87 |
#' SEX %in% c("M", "F") |
|
88 |
#' ) %>% |
|
89 |
#' mutate( |
|
90 |
#' # Reorder levels of ARM to display reference arm before treatment arm. |
|
91 |
#' ARM = droplevels(fct_relevel(ARM, "B: Placebo")), |
|
92 |
#' SEX = droplevels(SEX), |
|
93 |
#' AVALU = as.character(AVALU), |
|
94 |
#' is_event = CNSR == 0 |
|
95 |
#' ) |
|
96 |
#' labels <- list( |
|
97 |
#' "ARM" = adtte_labels["ARM"], |
|
98 |
#' "SEX" = adtte_labels["SEX"], |
|
99 |
#' "AVALU" = adtte_labels["AVALU"], |
|
100 |
#' "is_event" = "Event Flag" |
|
101 |
#' ) |
|
102 |
#' formatters::var_labels(adtte_f)[names(labels)] <- as.character(labels) |
|
103 |
#' df <- extract_survival_subgroups( |
|
104 |
#' variables = list( |
|
105 |
#' tte = "AVAL", |
|
106 |
#' is_event = "is_event", |
|
107 |
#' arm = "ARM", subgroups = c("SEX", "BMRKR2") |
|
108 |
#' ), |
|
109 |
#' data = adtte_f |
|
110 |
#' ) |
|
111 |
#' table_hr <- basic_table() %>% |
|
112 |
#' tabulate_survival_subgroups(df, time_unit = adtte_f$AVALU[1]) |
|
113 |
#' g_forest(table_hr) |
|
114 |
#' # Works with any `rtable`. |
|
115 |
#' tbl <- rtable( |
|
116 |
#' header = c("E", "CI", "N"), |
|
117 |
#' rrow("", 1, c(.8, 1.2), 200), |
|
118 |
#' rrow("", 1.2, c(1.1, 1.4), 50) |
|
119 |
#' ) |
|
120 |
#' g_forest( |
|
121 |
#' tbl = tbl, |
|
122 |
#' col_x = 1, |
|
123 |
#' col_ci = 2, |
|
124 |
#' xlim = c(0.5, 2), |
|
125 |
#' x_at = c(0.5, 1, 2), |
|
126 |
#' col_symbol_size = 3 |
|
127 |
#' ) |
|
128 |
#' tbl <- rtable( |
|
129 |
#' header = rheader( |
|
130 |
#' rrow("", rcell("A", colspan = 2)), |
|
131 |
#' rrow("", "c1", "c2") |
|
132 |
#' ), |
|
133 |
#' rrow("row 1", 1, c(.8, 1.2)), |
|
134 |
#' rrow("row 2", 1.2, c(1.1, 1.4)) |
|
135 |
#' ) |
|
136 |
#' g_forest( |
|
137 |
#' tbl = tbl, |
|
138 |
#' col_x = 1, |
|
139 |
#' col_ci = 2, |
|
140 |
#' xlim = c(0.5, 2), |
|
141 |
#' x_at = c(0.5, 1, 2), |
|
142 |
#' vline = 1, |
|
143 |
#' forest_header = c("Hello", "World") |
|
144 |
#' ) |
|
145 |
#' } |
|
146 |
#' |
|
147 |
#' @export |
|
148 |
g_forest <- function(tbl, |
|
149 |
col_x = attr(tbl, "col_x"), |
|
150 |
col_ci = attr(tbl, "col_ci"), |
|
151 |
vline = 1, |
|
152 |
forest_header = attr(tbl, "forest_header"), |
|
153 |
xlim = c(0.1, 10), |
|
154 |
logx = TRUE, |
|
155 |
x_at = c(0.1, 1, 10), |
|
156 |
width_row_names = NULL, |
|
157 |
width_columns = NULL, |
|
158 |
width_forest = grid::unit(1, "null"), |
|
159 |
col_symbol_size = attr(tbl, "col_symbol_size"), |
|
160 |
col = getOption("ggplot2.discrete.colour")[1], |
|
161 |
draw = TRUE, |
|
162 |
newpage = TRUE) { |
|
163 | 2x |
checkmate::assert_class(tbl, "VTableTree") |
164 | ||
165 | 2x |
nr <- nrow(tbl) |
166 | 2x |
nc <- ncol(tbl) |
167 | 2x |
if (is.null(col)) { |
168 | 2x |
col <- "blue" |
169 |
} |
|
170 | ||
171 | 2x |
checkmate::assert_number(col_x, lower = 0, upper = nc, null.ok = FALSE) |
172 | 2x |
checkmate::assert_number(col_ci, lower = 0, upper = nc, null.ok = FALSE) |
173 | 2x |
checkmate::assert_number(col_symbol_size, lower = 0, upper = nc, null.ok = TRUE) |
174 | 2x |
checkmate::assert_true(col_x > 0) |
175 | 2x |
checkmate::assert_true(col_ci > 0) |
176 | 2x |
checkmate::assert_character(col) |
177 | 2x |
if (!is.null(col_symbol_size)) { |
178 | 1x |
checkmate::assert_true(col_symbol_size > 0) |
179 |
} |
|
180 | ||
181 | 2x |
x_e <- vapply(seq_len(nr), function(i) { |
182 |
# If a label row is selected NULL is returned with a warning (suppressed) |
|
183 | 9x |
xi <- suppressWarnings(as.vector(tbl[i, col_x, drop = TRUE])) |
184 | ||
185 | 9x |
if (!is.null(xi) && !(length(xi) <= 0) && is.numeric(xi)) { |
186 | 7x |
xi |
187 |
} else { |
|
188 | 2x |
NA_real_ |
189 |
} |
|
190 | 2x |
}, numeric(1)) |
191 | ||
192 | 2x |
x_ci <- lapply(seq_len(nr), function(i) { |
193 | 9x |
xi <- suppressWarnings(as.vector(tbl[i, col_ci, drop = TRUE])) # as above |
194 | ||
195 | 9x |
if (!is.null(xi) && !(length(xi) <= 0) && is.numeric(xi)) { |
196 | 7x |
if (length(xi) != 2) { |
197 | ! |
stop("ci column needs two elements") |
198 |
} |
|
199 | 7x |
xi |
200 |
} else { |
|
201 | 2x |
c(NA_real_, NA_real_) |
202 |
} |
|
203 |
}) |
|
204 | ||
205 | 2x |
lower <- vapply(x_ci, `[`, numeric(1), 1) |
206 | 2x |
upper <- vapply(x_ci, `[`, numeric(1), 2) |
207 | ||
208 | 2x |
symbol_size <- if (!is.null(col_symbol_size)) { |
209 | 1x |
tmp_symbol_size <- vapply(seq_len(nr), function(i) { |
210 | 7x |
suppressWarnings(xi <- as.vector(tbl[i, col_symbol_size, drop = TRUE])) |
211 | ||
212 | 7x |
if (!is.null(xi) && !(length(xi) <= 0) && is.numeric(xi)) { |
213 | 5x |
xi |
214 |
} else { |
|
215 | 1x |
NA_real_ |
216 |
} |
|
217 | 1x |
}, numeric(1)) |
218 | ||
219 |
# Scale symbol size. |
|
220 | 1x |
tmp_symbol_size <- sqrt(tmp_symbol_size) |
221 | 1x |
max_size <- max(tmp_symbol_size, na.rm = TRUE) |
222 |
# Biggest points have radius is 2 * (1/3.5) lines not to overlap. |
|
223 |
# See forest_dot_line. |
|
224 | 1x |
2 * tmp_symbol_size / max_size |
225 |
} else { |
|
226 | 1x |
NULL |
227 |
} |
|
228 | ||
229 | 2x |
grob_forest <- forest_grob( |
230 | 2x |
tbl, |
231 | 2x |
x_e, |
232 | 2x |
lower, |
233 | 2x |
upper, |
234 | 2x |
vline, |
235 | 2x |
forest_header, |
236 | 2x |
xlim, |
237 | 2x |
logx, |
238 | 2x |
x_at, |
239 | 2x |
width_row_names, |
240 | 2x |
width_columns, |
241 | 2x |
width_forest, |
242 | 2x |
symbol_size = symbol_size, |
243 | 2x |
col = col, |
244 | 2x |
vp = grid::plotViewport(margins = rep(1, 4)) |
245 |
) |
|
246 | ||
247 | 2x |
if (draw) { |
248 | ! |
if (newpage) grid::grid.newpage() |
249 | ! |
grid::grid.draw(grob_forest) |
250 |
} |
|
251 | ||
252 | 2x |
invisible(grob_forest) |
253 |
} |
|
254 | ||
255 |
#' Forest Plot Grob |
|
256 |
#' |
|
257 |
#' @inheritParams g_forest |
|
258 |
#' @param tbl ([rtables::rtable()]) |
|
259 |
#' @param x (`numeric`)\cr coordinate of point. |
|
260 |
#' @param lower,upper (`numeric`)\cr lower/upper bound of the confidence interval. |
|
261 |
#' @param symbol_size (`numeric`)\cr vector with relative size for plot symbol. |
|
262 |
#' If `NULL`, the same symbol size is used. |
|
263 |
#' |
|
264 |
#' @details |
|
265 |
#' The heights get automatically determined. |
|
266 |
#' |
|
267 |
#' @noRd |
|
268 |
#' |
|
269 |
#' @examples |
|
270 |
#' tbl <- rtable( |
|
271 |
#' header = rheader( |
|
272 |
#' rrow("", "E", rcell("CI", colspan = 2), "N"), |
|
273 |
#' rrow("", "A", "B", "C", "D") |
|
274 |
#' ), |
|
275 |
#' rrow("row 1", 1, 0.8, 1.1, 16), |
|
276 |
#' rrow("row 2", 1.4, 0.8, 1.6, 25), |
|
277 |
#' rrow("row 3", 1.2, 0.8, 1.6, 36) |
|
278 |
#' ) |
|
279 |
#' |
|
280 |
#' x <- c(1, 1.4, 1.2) |
|
281 |
#' lower <- c(0.8, 0.8, 0.8) |
|
282 |
#' upper <- c(1.1, 1.6, 1.6) |
|
283 |
#' # numeric vector with multiplication factor to scale each circle radius |
|
284 |
#' # default radius is 1/3.5 lines |
|
285 |
#' symbol_scale <- c(1, 1.25, 1.5) |
|
286 |
#' |
|
287 |
#' # Internal function - forest_grob |
|
288 |
#' \dontrun{ |
|
289 |
#' p <- forest_grob(tbl, x, lower, upper, |
|
290 |
#' vline = 1, forest_header = c("A", "B"), |
|
291 |
#' x_at = c(.1, 1, 10), xlim = c(0.1, 10), logx = TRUE, symbol_size = symbol_scale, |
|
292 |
#' vp = grid::plotViewport(margins = c(1, 1, 1, 1)) |
|
293 |
#' ) |
|
294 |
#' |
|
295 |
#' draw_grob(p) |
|
296 |
#' } |
|
297 |
forest_grob <- function(tbl, |
|
298 |
x, |
|
299 |
lower, |
|
300 |
upper, |
|
301 |
vline, |
|
302 |
forest_header, |
|
303 |
xlim = NULL, |
|
304 |
logx = FALSE, |
|
305 |
x_at = NULL, |
|
306 |
width_row_names = NULL, |
|
307 |
width_columns = NULL, |
|
308 |
width_forest = grid::unit(1, "null"), |
|
309 |
symbol_size = NULL, |
|
310 |
col = "blue", |
|
311 |
name = NULL, |
|
312 |
gp = NULL, |
|
313 |
vp = NULL) { |
|
314 | 2x |
nr <- nrow(tbl) |
315 | 2x |
if (is.null(vline)) { |
316 | ! |
checkmate::assert_true(is.null(forest_header)) |
317 |
} else { |
|
318 | 2x |
checkmate::assert_number(vline) |
319 | 2x |
checkmate::assert_character(forest_header, len = 2, null.ok = TRUE) |
320 |
} |
|
321 | ||
322 | 2x |
checkmate::assert_numeric(x, len = nr) |
323 | 2x |
checkmate::assert_numeric(lower, len = nr) |
324 | 2x |
checkmate::assert_numeric(upper, len = nr) |
325 | 2x |
checkmate::assert_numeric(symbol_size, len = nr, null.ok = TRUE) |
326 | 2x |
checkmate::assert_character(col) |
327 | ||
328 | 2x |
if (is.null(symbol_size)) { |
329 | 1x |
symbol_size <- rep(1, nr) |
330 |
} |
|
331 | ||
332 | 2x |
if (is.null(xlim)) { |
333 | ! |
r <- range(c(x, lower, upper), na.rm = TRUE) |
334 | ! |
xlim <- r + c(-0.05, 0.05) * diff(r) |
335 |
} |
|
336 | ||
337 | 2x |
if (logx) { |
338 | 2x |
if (is.null(x_at)) { |
339 | ! |
x_at <- pretty(log(stats::na.omit(c(x, lower, upper)))) |
340 | ! |
x_labels <- exp(x_at) |
341 |
} else { |
|
342 | 2x |
x_labels <- x_at |
343 | 2x |
x_at <- log(x_at) |
344 |
} |
|
345 | 2x |
xlim <- log(xlim) |
346 | 2x |
x <- log(x) |
347 | 2x |
lower <- log(lower) |
348 | 2x |
upper <- log(upper) |
349 | 2x |
if (!is.null(vline)) { |
350 | 2x |
vline <- log(vline) |
351 |
} |
|
352 |
} else { |
|
353 | ! |
x_labels <- TRUE |
354 |
} |
|
355 | ||
356 | 2x |
data_forest_vp <- grid::dataViewport(xlim, c(0, 1)) |
357 | ||
358 |
# Get table content as matrix form. |
|
359 | 2x |
mf <- matrix_form(tbl) |
360 | ||
361 |
# Use `rtables` indent_string eventually. |
|
362 | 2x |
mf$strings[, 1] <- paste0( |
363 | 2x |
strrep(" ", c(rep(0, attr(mf, "nrow_header")), mf$row_info$indent)), |
364 | 2x |
mf$strings[, 1] |
365 |
) |
|
366 | ||
367 | 2x |
n_header <- attr(mf, "nrow_header") |
368 | ||
369 | ! |
if (any(mf$display[, 1] == FALSE)) stop("row names need to be always displayed") |
370 | ||
371 |
# Pre-process the data to be used in lapply and cell_in_rows. |
|
372 | 2x |
to_args_for_cell_in_rows_fun <- function(part = c("body", "header"), |
373 | 2x |
underline_colspan = FALSE) { |
374 | 4x |
part <- match.arg(part) |
375 | 4x |
if (part == "body") { |
376 | 2x |
mat_row_indices <- seq_len(nrow(tbl)) + n_header |
377 | 2x |
row_ind_offset <- -n_header |
378 |
} else { |
|
379 | 2x |
mat_row_indices <- seq_len(n_header) |
380 | 2x |
row_ind_offset <- 0 |
381 |
} |
|
382 | ||
383 | 4x |
lapply(mat_row_indices, function(i) { |
384 | 13x |
disp <- mf$display[i, -1] |
385 | 13x |
list( |
386 | 13x |
row_name = mf$strings[i, 1], |
387 | 13x |
cells = mf$strings[i, -1][disp], |
388 | 13x |
cell_spans = mf$spans[i, -1][disp], |
389 | 13x |
row_index = i + row_ind_offset, |
390 | 13x |
underline_colspan = underline_colspan |
391 |
) |
|
392 |
}) |
|
393 |
} |
|
394 | ||
395 | 2x |
args_header <- to_args_for_cell_in_rows_fun("header", underline_colspan = TRUE) |
396 | 2x |
args_body <- to_args_for_cell_in_rows_fun("body", underline_colspan = FALSE) |
397 | ||
398 | 2x |
grid::gTree( |
399 | 2x |
name = name, |
400 | 2x |
children = grid::gList( |
401 | 2x |
grid::gTree( |
402 | 2x |
children = do.call(grid::gList, lapply(args_header, do.call, what = cell_in_rows)), |
403 | 2x |
vp = grid::vpPath("vp_table_layout", "vp_header") |
404 |
), |
|
405 | 2x |
grid::gTree( |
406 | 2x |
children = do.call(grid::gList, lapply(args_body, do.call, what = cell_in_rows)), |
407 | 2x |
vp = grid::vpPath("vp_table_layout", "vp_body") |
408 |
), |
|
409 | 2x |
grid::linesGrob( |
410 | 2x |
grid::unit(c(0, 1), "npc"), |
411 | 2x |
y = grid::unit(c(.5, .5), "npc"), |
412 | 2x |
vp = grid::vpPath("vp_table_layout", "vp_spacer") |
413 |
), |
|
414 |
# forest part |
|
415 | 2x |
if (is.null(vline)) { |
416 | ! |
NULL |
417 |
} else { |
|
418 | 2x |
grid::gTree( |
419 | 2x |
children = grid::gList( |
420 | 2x |
grid::gTree( |
421 | 2x |
children = grid::gList( |
422 |
# this may overflow, to fix, look here |
|
423 |
# https://stackoverflow.com/questions/33623169/add-multi-line-footnote-to-tablegrob-while-using-gridextra-in-r #nolintr |
|
424 | 2x |
grid::textGrob( |
425 | 2x |
forest_header[1], |
426 | 2x |
x = grid::unit(vline, "native") - grid::unit(1, "lines"), |
427 | 2x |
just = c("right", "center") |
428 |
), |
|
429 | 2x |
grid::textGrob( |
430 | 2x |
forest_header[2], |
431 | 2x |
x = grid::unit(vline, "native") + grid::unit(1, "lines"), |
432 | 2x |
just = c("left", "center") |
433 |
) |
|
434 |
), |
|
435 | 2x |
vp = grid::vpStack(grid::viewport(layout.pos.col = ncol(tbl) + 2), data_forest_vp) |
436 |
) |
|
437 |
), |
|
438 | 2x |
vp = grid::vpPath("vp_table_layout", "vp_header") |
439 |
) |
|
440 |
}, |
|
441 | 2x |
grid::gTree( |
442 | 2x |
children = grid::gList( |
443 | 2x |
grid::gTree( |
444 | 2x |
children = grid::gList( |
445 | 2x |
grid::rectGrob(gp = grid::gpar(col = "gray90", fill = "gray90")), |
446 | 2x |
if (is.null(vline)) { |
447 | ! |
NULL |
448 |
} else { |
|
449 | 2x |
grid::linesGrob( |
450 | 2x |
x = grid::unit(rep(vline, 2), "native"), |
451 | 2x |
y = grid::unit(c(0, 1), "npc"), |
452 | 2x |
gp = grid::gpar(lwd = 2), |
453 | 2x |
vp = data_forest_vp |
454 |
) |
|
455 |
}, |
|
456 | 2x |
grid::xaxisGrob(at = x_at, label = x_labels, vp = data_forest_vp) |
457 |
), |
|
458 | 2x |
vp = grid::viewport(layout.pos.col = ncol(tbl) + 2) |
459 |
) |
|
460 |
), |
|
461 | 2x |
vp = grid::vpPath("vp_table_layout", "vp_body") |
462 |
), |
|
463 | 2x |
grid::gTree( |
464 | 2x |
children = do.call( |
465 | 2x |
grid::gList, |
466 | 2x |
Map( |
467 | 2x |
function(xi, li, ui, row_index, size_i, col) { |
468 | 9x |
forest_dot_line( |
469 | 9x |
xi, |
470 | 9x |
li, |
471 | 9x |
ui, |
472 | 9x |
row_index, |
473 | 9x |
xlim, |
474 | 9x |
symbol_size = size_i, |
475 | 9x |
col = col, |
476 | 9x |
datavp = data_forest_vp |
477 |
) |
|
478 |
}, |
|
479 | 2x |
x, |
480 | 2x |
lower, |
481 | 2x |
upper, |
482 | 2x |
seq_along(x), |
483 | 2x |
symbol_size, |
484 | 2x |
col, |
485 | 2x |
USE.NAMES = FALSE |
486 |
) |
|
487 |
), |
|
488 | 2x |
vp = grid::vpPath("vp_table_layout", "vp_body") |
489 |
) |
|
490 |
), |
|
491 | 2x |
childrenvp = forest_viewport(tbl, width_row_names, width_columns, width_forest), |
492 | 2x |
vp = vp, |
493 | 2x |
gp = gp |
494 |
) |
|
495 |
} |
|
496 | ||
497 | ||
498 |
cell_in_rows <- function(row_name, |
|
499 |
cells, |
|
500 |
cell_spans, |
|
501 |
row_index, |
|
502 |
underline_colspan = FALSE) { |
|
503 | 13x |
checkmate::assert_string(row_name) |
504 | 13x |
checkmate::assert_character(cells, min.len = 1, any.missing = FALSE) |
505 | 13x |
checkmate::assert_numeric(cell_spans, len = length(cells), any.missing = FALSE) |
506 | 13x |
checkmate::assert_number(row_index) |
507 | 13x |
checkmate::assert_flag(underline_colspan) |
508 | ||
509 | 13x |
vp_name_rn <- paste0("rowname-", row_index) |
510 | 13x |
g_rowname <- if (!is.null(row_name) && row_name != "") { |
511 | 10x |
grid::textGrob( |
512 | 10x |
name = vp_name_rn, |
513 | 10x |
label = row_name, |
514 | 10x |
x = grid::unit(0, "npc"), |
515 | 10x |
just = c("left", "center"), |
516 | 10x |
vp = grid::vpPath(paste0("rowname-", row_index)) |
517 |
) |
|
518 |
} else { |
|
519 | 3x |
NULL |
520 |
} |
|
521 | ||
522 | 13x |
gl_cols <- if (!(length(cells) > 0)) { |
523 | ! |
list(NULL) |
524 |
} else { |
|
525 | 13x |
j <- 1 # column index of cell |
526 | ||
527 | 13x |
lapply(seq_along(cells), function(k) { |
528 | 67x |
cell_ascii <- cells[[k]] |
529 | 67x |
cs <- cell_spans[[k]] |
530 | ||
531 | 67x |
if (is.na(cell_ascii) || is.null(cell_ascii)) { |
532 | ! |
cell_ascii <- "NA" |
533 |
} |
|
534 | ||
535 | 67x |
cell_name <- paste0("g-cell-", row_index, "-", j) |
536 | ||
537 | 67x |
cell_grobs <- if (identical(cell_ascii, "")) { |
538 | 14x |
NULL |
539 |
} else { |
|
540 | 53x |
if (cs == 1) { |
541 | 49x |
grid::textGrob( |
542 | 49x |
label = cell_ascii, |
543 | 49x |
name = cell_name, |
544 | 49x |
vp = grid::vpPath(paste0("cell-", row_index, "-", j)) |
545 |
) |
|
546 |
} else { |
|
547 |
# +1 because of rowname |
|
548 | 4x |
vp_joined_cols <- grid::viewport(layout.pos.row = row_index, layout.pos.col = seq(j + 1, j + cs)) |
549 | ||
550 | 4x |
lab <- grid::textGrob( |
551 | 4x |
label = cell_ascii, |
552 | 4x |
name = cell_name, |
553 | 4x |
vp = vp_joined_cols |
554 |
) |
|
555 | ||
556 | 4x |
if (!underline_colspan || grepl("^[[:space:]]*$", cell_ascii)) { |
557 | 1x |
lab |
558 |
} else { |
|
559 | 3x |
grid::gList( |
560 | 3x |
lab, |
561 | 3x |
grid::linesGrob( |
562 | 3x |
x = grid::unit.c(grid::unit(.2, "lines"), grid::unit(1, "npc") - grid::unit(.2, "lines")), |
563 | 3x |
y = grid::unit(c(0, 0), "npc"), |
564 | 3x |
vp = vp_joined_cols |
565 |
) |
|
566 |
) |
|
567 |
} |
|
568 |
} |
|
569 |
} |
|
570 | 67x |
j <<- j + cs |
571 | ||
572 | 67x |
cell_grobs |
573 |
}) |
|
574 |
} |
|
575 | ||
576 | 13x |
grid::gList( |
577 | 13x |
g_rowname, |
578 | 13x |
do.call(grid::gList, gl_cols) |
579 |
) |
|
580 |
} |
|
581 | ||
582 |
#' Graphic Object: Forest Dot Line |
|
583 |
#' |
|
584 |
#' Calculate the `grob` corresponding to the dot line within the forest plot. |
|
585 |
#' |
|
586 |
#' @noRd |
|
587 |
forest_dot_line <- function(x, |
|
588 |
lower, |
|
589 |
upper, |
|
590 |
row_index, |
|
591 |
xlim, |
|
592 |
symbol_size = 1, |
|
593 |
col = "blue", |
|
594 |
datavp) { |
|
595 | 9x |
ci <- c(lower, upper) |
596 | 9x |
if (any(!is.na(c(x, ci)))) { |
597 |
# line |
|
598 | 7x |
y <- grid::unit(c(0.5, 0.5), "npc") |
599 | ||
600 | 7x |
g_line <- if (all(!is.na(ci)) && ci[2] > xlim[1] && ci[1] < xlim[2]) { |
601 |
# - |
|
602 | 7x |
if (ci[1] >= xlim[1] && ci[2] <= xlim[2]) { |
603 | 2x |
grid::linesGrob(x = grid::unit(c(ci[1], ci[2]), "native"), y = y) |
604 | 5x |
} else if (ci[1] < xlim[1] && ci[2] > xlim[2]) { |
605 |
# <-> |
|
606 | 3x |
grid::linesGrob( |
607 | 3x |
x = grid::unit(xlim, "native"), |
608 | 3x |
y = y, |
609 | 3x |
arrow = grid::arrow(angle = 30, length = grid::unit(0.5, "lines"), ends = "both") |
610 |
) |
|
611 | 2x |
} else if (ci[1] < xlim[1] && ci[2] <= xlim[2]) { |
612 |
# <- |
|
613 | ! |
grid::linesGrob( |
614 | ! |
x = grid::unit(c(xlim[1], ci[2]), "native"), |
615 | ! |
y = y, |
616 | ! |
arrow = grid::arrow(angle = 30, length = grid::unit(0.5, "lines"), ends = "first") |
617 |
) |
|
618 | 2x |
} else if (ci[1] >= xlim[1] && ci[2] > xlim[2]) { |
619 |
# -> |
|
620 | 2x |
grid::linesGrob( |
621 | 2x |
x = grid::unit(c(ci[1], xlim[2]), "native"), |
622 | 2x |
y = y, |
623 | 2x |
arrow = grid::arrow(angle = 30, length = grid::unit(0.5, "lines"), ends = "last") |
624 |
) |
|
625 |
} |
|
626 |
} else { |
|
627 | ! |
NULL |
628 |
} |
|
629 | ||
630 | 7x |
g_circle <- if (!is.na(x) && x >= xlim[1] && x <= xlim[2]) { |
631 | 6x |
grid::circleGrob( |
632 | 6x |
x = grid::unit(x, "native"), |
633 | 6x |
y = y, |
634 | 6x |
r = grid::unit(1 / 3.5 * symbol_size, "lines"), |
635 | 6x |
name = "point" |
636 |
) |
|
637 |
} else { |
|
638 | 1x |
NULL |
639 |
} |
|
640 | ||
641 | 7x |
grid::gTree( |
642 | 7x |
children = grid::gList( |
643 | 7x |
grid::gTree( |
644 | 7x |
children = grid::gList( |
645 | 7x |
grid::gList( |
646 | 7x |
g_line, |
647 | 7x |
g_circle |
648 |
) |
|
649 |
), |
|
650 | 7x |
vp = datavp, |
651 | 7x |
gp = grid::gpar(col = col, fill = col) |
652 |
) |
|
653 |
), |
|
654 | 7x |
vp = grid::vpPath(paste0("forest-", row_index)) |
655 |
) |
|
656 |
} else { |
|
657 | 2x |
NULL |
658 |
} |
|
659 |
} |
|
660 | ||
661 |
#' Create a Viewport Tree for the Forest Plot |
|
662 |
#' |
|
663 |
#' @return A viewport tree. |
|
664 |
#' |
|
665 |
#' @examples |
|
666 |
#' library(grid) |
|
667 |
#' |
|
668 |
#' tbl <- rtable( |
|
669 |
#' header = rheader( |
|
670 |
#' rrow("", "E", rcell("CI", colspan = 2)), |
|
671 |
#' rrow("", "A", "B", "C") |
|
672 |
#' ), |
|
673 |
#' rrow("row 1", 1, 0.8, 1.1), |
|
674 |
#' rrow("row 2", 1.4, 0.8, 1.6), |
|
675 |
#' rrow("row 3", 1.2, 0.8, 1.2) |
|
676 |
#' ) |
|
677 |
#' |
|
678 |
#' # Internal function - forest_viewport |
|
679 |
#' \dontrun{ |
|
680 |
#' v <- forest_viewport(tbl) |
|
681 |
#' |
|
682 |
#' grid::grid.newpage() |
|
683 |
#' showViewport(v) |
|
684 |
#' } |
|
685 |
#' |
|
686 |
#' @keywords internal |
|
687 |
forest_viewport <- function(tbl, |
|
688 |
width_row_names = NULL, |
|
689 |
width_columns = NULL, |
|
690 |
width_forest = grid::unit(1, "null"), |
|
691 |
gap_column = grid::unit(1, "lines"), |
|
692 |
gap_header = grid::unit(1, "lines"), |
|
693 |
mat_form = NULL) { |
|
694 | 2x |
checkmate::assert_class(tbl, "VTableTree") |
695 | 2x |
checkmate::assert_true(grid::is.unit(width_forest)) |
696 | 2x |
if (!is.null(width_row_names)) { |
697 | ! |
checkmate::assert_true(grid::is.unit(width_row_names)) |
698 |
} |
|
699 | 2x |
if (!is.null(width_columns)) { |
700 | ! |
checkmate::assert_true(grid::is.unit(width_columns)) |
701 |
} |
|
702 | ||
703 | 2x |
if (is.null(mat_form)) mat_form <- matrix_form(tbl) |
704 | ||
705 | 2x |
mat_form$strings[!mat_form$display] <- "" |
706 | ||
707 | 2x |
nr <- nrow(tbl) |
708 | 2x |
nc <- ncol(tbl) |
709 | 2x |
nr_h <- attr(mat_form, "nrow_header") |
710 | ||
711 | 2x |
if (is.null(width_row_names) || is.null(width_columns)) { |
712 | 2x |
tbl_widths <- formatters::propose_column_widths(mat_form) |
713 | 2x |
strs_with_width <- strrep("x", tbl_widths) # that works for mono spaced fonts |
714 | 2x |
if (is.null(width_row_names)) width_row_names <- grid::stringWidth(strs_with_width[1]) |
715 | 2x |
if (is.null(width_columns)) width_columns <- grid::stringWidth(strs_with_width[-1]) |
716 |
} |
|
717 | ||
718 |
# Widths for row name, cols, forest. |
|
719 | 2x |
widths <- grid::unit.c( |
720 | 2x |
width_row_names + gap_column, |
721 | 2x |
width_columns + gap_column, |
722 | 2x |
width_forest |
723 |
) |
|
724 | ||
725 | 2x |
n_lines_per_row <- apply( |
726 | 2x |
X = mat_form$strings, |
727 | 2x |
MARGIN = 1, |
728 | 2x |
FUN = function(row) { |
729 | 13x |
tmp <- vapply( |
730 | 13x |
gregexpr("\n", row, fixed = TRUE), |
731 | 13x |
attr, numeric(1), |
732 | 13x |
"match.length" |
733 | 13x |
) + 1 |
734 | 13x |
max(c(tmp, 1)) |
735 |
} |
|
736 |
) |
|
737 | ||
738 | 2x |
i_header <- seq_len(nr_h) |
739 | ||
740 | 2x |
height_body_rows <- grid::unit(n_lines_per_row[-i_header] * 1.2, "lines") |
741 | 2x |
height_header_rows <- grid::unit(n_lines_per_row[i_header] * 1.2, "lines") |
742 | ||
743 | 2x |
height_body <- grid::unit(sum(n_lines_per_row[-i_header]) * 1.2, "lines") |
744 | 2x |
height_header <- grid::unit(sum(n_lines_per_row[i_header]) * 1.2, "lines") |
745 | ||
746 | 2x |
nc_g <- nc + 2 # number of columns incl. row names and forest |
747 | ||
748 | 2x |
vp_tbl <- grid::vpTree( |
749 | 2x |
parent = grid::viewport( |
750 | 2x |
name = "vp_table_layout", |
751 | 2x |
layout = grid::grid.layout( |
752 | 2x |
nrow = 3, ncol = 1, |
753 | 2x |
heights = grid::unit.c(height_header, gap_header, height_body) |
754 |
) |
|
755 |
), |
|
756 | 2x |
children = grid::vpList( |
757 | 2x |
vp_forest_table_part(nr_h, nc_g, 1, 1, widths, height_header_rows, "vp_header"), |
758 | 2x |
vp_forest_table_part(nr, nc_g, 3, 1, widths, height_body_rows, "vp_body"), |
759 | 2x |
grid::viewport(name = "vp_spacer", layout.pos.row = 2, layout.pos.col = 1) |
760 |
) |
|
761 |
) |
|
762 | 2x |
vp_tbl |
763 |
} |
|
764 | ||
765 |
#' Viewport Forest Plot: Table Part |
|
766 |
#' |
|
767 |
#' Prepares a viewport for the table included in the forest plot. |
|
768 |
#' |
|
769 |
#' @noRd |
|
770 |
vp_forest_table_part <- function(nrow, |
|
771 |
ncol, |
|
772 |
l_row, |
|
773 |
l_col, |
|
774 |
widths, |
|
775 |
heights, |
|
776 |
name) { |
|
777 | 4x |
grid::vpTree( |
778 | 4x |
grid::viewport( |
779 | 4x |
name = name, |
780 | 4x |
layout.pos.row = l_row, |
781 | 4x |
layout.pos.col = l_col, |
782 | 4x |
layout = grid::grid.layout(nrow = nrow, ncol = ncol, widths = widths, heights = heights) |
783 |
), |
|
784 | 4x |
children = grid::vpList( |
785 | 4x |
do.call( |
786 | 4x |
grid::vpList, |
787 | 4x |
lapply( |
788 | 4x |
seq_len(nrow), function(i) { |
789 | 13x |
grid::viewport(layout.pos.row = i, layout.pos.col = 1, name = paste0("rowname-", i)) |
790 |
} |
|
791 |
) |
|
792 |
), |
|
793 | 4x |
do.call( |
794 | 4x |
grid::vpList, |
795 | 4x |
apply( |
796 | 4x |
expand.grid(seq_len(nrow), seq_len(ncol - 2)), |
797 | 4x |
1, |
798 | 4x |
function(x) { |
799 | 71x |
i <- x[1] |
800 | 71x |
j <- x[2] |
801 | 71x |
grid::viewport(layout.pos.row = i, layout.pos.col = j + 1, name = paste0("cell-", i, "-", j)) |
802 |
} |
|
803 |
) |
|
804 |
), |
|
805 | 4x |
do.call( |
806 | 4x |
grid::vpList, |
807 | 4x |
lapply( |
808 | 4x |
seq_len(nrow), |
809 | 4x |
function(i) { |
810 | 13x |
grid::viewport(layout.pos.row = i, layout.pos.col = ncol, name = paste0("forest-", i)) |
811 |
} |
|
812 |
) |
|
813 |
) |
|
814 |
) |
|
815 |
) |
|
816 |
} |
|
817 | ||
818 |
#' Forest Rendering |
|
819 |
#' |
|
820 |
#' Renders the forest grob. |
|
821 |
#' |
|
822 |
#' @noRd |
|
823 |
grid.forest <- function(...) { # nolint |
|
824 | ! |
grid::grid.draw(forest_grob(...)) |
825 |
} |
1 |
#' Helper Functions for Multivariate Logistic Regression |
|
2 |
#' |
|
3 |
#' @description `r lifecycle::badge("stable")` |
|
4 |
#' |
|
5 |
#' Helper functions used in calculations for logistic regression. |
|
6 |
#' |
|
7 |
#' @inheritParams argument_convention |
|
8 |
#' @param fit_glm (`glm`)\cr logistic regression model fitted by [stats::glm()] with "binomial" family. |
|
9 |
#' Limited functionality is also available for conditional logistic regression models fitted by |
|
10 |
#' [survival::clogit()], currently this is used only by [extract_rsp_biomarkers()]. |
|
11 |
#' @param x (`string` or `character`)\cr a variable or interaction term in `fit_glm` (depending on the |
|
12 |
#' helper function). |
|
13 |
#' |
|
14 |
#' @examples |
|
15 |
#' library(dplyr) |
|
16 |
#' library(broom) |
|
17 |
#' |
|
18 |
#' adrs_f <- tern_ex_adrs %>% |
|
19 |
#' filter(PARAMCD == "BESRSPI") %>% |
|
20 |
#' filter(RACE %in% c("ASIAN", "WHITE", "BLACK OR AFRICAN AMERICAN")) %>% |
|
21 |
#' mutate( |
|
22 |
#' Response = case_when(AVALC %in% c("PR", "CR") ~ 1, TRUE ~ 0), |
|
23 |
#' RACE = factor(RACE), |
|
24 |
#' SEX = factor(SEX) |
|
25 |
#' ) |
|
26 |
#' formatters::var_labels(adrs_f) <- c(formatters::var_labels(tern_ex_adrs), Response = "Response") |
|
27 |
#' mod1 <- fit_logistic( |
|
28 |
#' data = adrs_f, |
|
29 |
#' variables = list( |
|
30 |
#' response = "Response", |
|
31 |
#' arm = "ARMCD", |
|
32 |
#' covariates = c("AGE", "RACE") |
|
33 |
#' ) |
|
34 |
#' ) |
|
35 |
#' mod2 <- fit_logistic( |
|
36 |
#' data = adrs_f, |
|
37 |
#' variables = list( |
|
38 |
#' response = "Response", |
|
39 |
#' arm = "ARMCD", |
|
40 |
#' covariates = c("AGE", "RACE"), |
|
41 |
#' interaction = "AGE" |
|
42 |
#' ) |
|
43 |
#' ) |
|
44 |
#' |
|
45 |
#' @name h_logistic_regression |
|
46 |
NULL |
|
47 | ||
48 |
#' @describeIn h_logistic_regression Helper function to extract interaction variable names from a fitted |
|
49 |
#' model assuming only one interaction term. |
|
50 |
#' |
|
51 |
#' @return Vector of names of interaction variables. |
|
52 |
#' |
|
53 |
#' @export |
|
54 |
h_get_interaction_vars <- function(fit_glm) { |
|
55 | 27x |
checkmate::assert_class(fit_glm, "glm") |
56 | 27x |
terms_name <- attr(stats::terms(fit_glm), "term.labels") |
57 | 27x |
terms_order <- attr(stats::terms(fit_glm), "order") |
58 | 27x |
interaction_term <- terms_name[terms_order == 2] |
59 | 27x |
checkmate::assert_string(interaction_term) |
60 | 27x |
strsplit(interaction_term, split = ":")[[1]] |
61 |
} |
|
62 | ||
63 |
#' @describeIn h_logistic_regression Helper function to get the right coefficient name from the |
|
64 |
#' interaction variable names and the given levels. The main value here is that the order |
|
65 |
#' of first and second variable is checked in the `interaction_vars` input. |
|
66 |
#' |
|
67 |
#' @param interaction_vars (`character` of length 2)\cr interaction variable names. |
|
68 |
#' @param first_var_with_level (`character` of length 2)\cr the first variable name with |
|
69 |
#' the interaction level. |
|
70 |
#' @param second_var_with_level (`character` of length 2)\cr the second variable name with |
|
71 |
#' the interaction level. |
|
72 |
#' |
|
73 |
#' @return Name of coefficient. |
|
74 |
#' |
|
75 |
#' @export |
|
76 |
h_interaction_coef_name <- function(interaction_vars, |
|
77 |
first_var_with_level, |
|
78 |
second_var_with_level) { |
|
79 | 45x |
checkmate::assert_character(interaction_vars, len = 2, any.missing = FALSE) |
80 | 45x |
checkmate::assert_character(first_var_with_level, len = 2, any.missing = FALSE) |
81 | 45x |
checkmate::assert_character(second_var_with_level, len = 2, any.missing = FALSE) |
82 | 45x |
checkmate::assert_subset(c(first_var_with_level[1], second_var_with_level[1]), interaction_vars) |
83 | ||
84 | 45x |
first_name <- paste(first_var_with_level, collapse = "") |
85 | 45x |
second_name <- paste(second_var_with_level, collapse = "") |
86 | 45x |
if (first_var_with_level[1] == interaction_vars[1]) { |
87 | 34x |
paste(first_name, second_name, sep = ":") |
88 | 11x |
} else if (second_var_with_level[1] == interaction_vars[1]) { |
89 | 11x |
paste(second_name, first_name, sep = ":") |
90 |
} |
|
91 |
} |
|
92 | ||
93 |
#' @describeIn h_logistic_regression Helper function to calculate the odds ratio estimates |
|
94 |
#' for the case when both the odds ratio and the interaction variable are categorical. |
|
95 |
#' |
|
96 |
#' @param odds_ratio_var (`string`)\cr the odds ratio variable. |
|
97 |
#' @param interaction_var (`string`)\cr the interaction variable. |
|
98 |
#' |
|
99 |
#' @return Odds ratio. |
|
100 |
#' |
|
101 |
#' @export |
|
102 |
h_or_cat_interaction <- function(odds_ratio_var, |
|
103 |
interaction_var, |
|
104 |
fit_glm, |
|
105 |
conf_level = 0.95) { |
|
106 | 7x |
interaction_vars <- h_get_interaction_vars(fit_glm) |
107 | 7x |
checkmate::assert_string(odds_ratio_var) |
108 | 7x |
checkmate::assert_string(interaction_var) |
109 | 7x |
checkmate::assert_subset(c(odds_ratio_var, interaction_var), interaction_vars) |
110 | 7x |
checkmate::assert_vector(interaction_vars, len = 2) |
111 | ||
112 | 7x |
xs_level <- fit_glm$xlevels |
113 | 7x |
xs_coef <- stats::coef(fit_glm) |
114 | 7x |
xs_vcov <- stats::vcov(fit_glm) |
115 | 7x |
y <- list() |
116 | 7x |
for (var_level in xs_level[[odds_ratio_var]][-1]) { |
117 | 12x |
x <- list() |
118 | 12x |
for (ref_level in xs_level[[interaction_var]]) { |
119 | 32x |
coef_names <- paste0(odds_ratio_var, var_level) |
120 | 32x |
if (ref_level != xs_level[[interaction_var]][1]) { |
121 | 20x |
interaction_coef_name <- h_interaction_coef_name( |
122 | 20x |
interaction_vars, |
123 | 20x |
c(odds_ratio_var, var_level), |
124 | 20x |
c(interaction_var, ref_level) |
125 |
) |
|
126 | 20x |
coef_names <- c( |
127 | 20x |
coef_names, |
128 | 20x |
interaction_coef_name |
129 |
) |
|
130 |
} |
|
131 | 32x |
if (length(coef_names) > 1) { |
132 | 20x |
ones <- t(c(1, 1)) |
133 | 20x |
est <- as.numeric(ones %*% xs_coef[coef_names]) |
134 | 20x |
se <- sqrt(as.numeric(ones %*% xs_vcov[coef_names, coef_names] %*% t(ones))) |
135 |
} else { |
|
136 | 12x |
est <- xs_coef[coef_names] |
137 | 12x |
se <- sqrt(as.numeric(xs_vcov[coef_names, coef_names])) |
138 |
} |
|
139 | 32x |
or <- exp(est) |
140 | 32x |
ci <- exp(est + c(lcl = -1, ucl = 1) * stats::qnorm((1 + conf_level) / 2) * se) |
141 | 32x |
x[[ref_level]] <- list(or = or, ci = ci) |
142 |
} |
|
143 | 12x |
y[[var_level]] <- x |
144 |
} |
|
145 | 7x |
y |
146 |
} |
|
147 | ||
148 |
#' @describeIn h_logistic_regression Helper function to calculate the odds ratio estimates |
|
149 |
#' for the case when either the odds ratio or the interaction variable is continuous. |
|
150 |
#' |
|
151 |
#' @param at (`NULL` or `numeric`)\cr optional values for the interaction variable. Otherwise |
|
152 |
#' the median is used. |
|
153 |
#' |
|
154 |
#' @return Odds ratio. |
|
155 |
#' |
|
156 |
#' @note We don't provide a function for the case when both variables are continuous because |
|
157 |
#' this does not arise in this table, as the treatment arm variable will always be involved |
|
158 |
#' and categorical. |
|
159 |
#' |
|
160 |
#' @export |
|
161 |
h_or_cont_interaction <- function(odds_ratio_var, |
|
162 |
interaction_var, |
|
163 |
fit_glm, |
|
164 |
at = NULL, |
|
165 |
conf_level = 0.95) { |
|
166 | 9x |
interaction_vars <- h_get_interaction_vars(fit_glm) |
167 | 9x |
checkmate::assert_string(odds_ratio_var) |
168 | 9x |
checkmate::assert_string(interaction_var) |
169 | 9x |
checkmate::assert_subset(c(odds_ratio_var, interaction_var), interaction_vars) |
170 | 9x |
checkmate::assert_vector(interaction_vars, len = 2) |
171 | 9x |
checkmate::assert_numeric(at, min.len = 1, null.ok = TRUE, any.missing = FALSE) |
172 | 9x |
xs_level <- fit_glm$xlevels |
173 | 9x |
xs_coef <- stats::coef(fit_glm) |
174 | 9x |
xs_vcov <- stats::vcov(fit_glm) |
175 | 9x |
xs_class <- attr(fit_glm$terms, "dataClasses") |
176 | 9x |
model_data <- fit_glm$model |
177 | 9x |
if (!is.null(at)) { |
178 | 2x |
checkmate::assert_set_equal(xs_class[interaction_var], "numeric") |
179 |
} |
|
180 | 9x |
y <- list() |
181 | 9x |
if (xs_class[interaction_var] == "numeric") { |
182 | 6x |
if (is.null(at)) { |
183 | 4x |
at <- ceiling(stats::median(model_data[[interaction_var]])) |
184 |
} |
|
185 | ||
186 | 6x |
for (var_level in xs_level[[odds_ratio_var]][-1]) { |
187 | 12x |
x <- list() |
188 | 12x |
for (increment in at) { |
189 | 18x |
coef_names <- paste0(odds_ratio_var, var_level) |
190 | 18x |
if (increment != 0) { |
191 | 18x |
interaction_coef_name <- h_interaction_coef_name( |
192 | 18x |
interaction_vars, |
193 | 18x |
c(odds_ratio_var, var_level), |
194 | 18x |
c(interaction_var, "") |
195 |
) |
|
196 | 18x |
coef_names <- c( |
197 | 18x |
coef_names, |
198 | 18x |
interaction_coef_name |
199 |
) |
|
200 |
} |
|
201 | 18x |
if (length(coef_names) > 1) { |
202 | 18x |
xvec <- t(c(1, increment)) |
203 | 18x |
est <- as.numeric(xvec %*% xs_coef[coef_names]) |
204 | 18x |
se <- sqrt(as.numeric(xvec %*% xs_vcov[coef_names, coef_names] %*% t(xvec))) |
205 |
} else { |
|
206 | ! |
est <- xs_coef[coef_names] |
207 | ! |
se <- sqrt(as.numeric(xs_vcov[coef_names, coef_names])) |
208 |
} |
|
209 | 18x |
or <- exp(est) |
210 | 18x |
ci <- exp(est + c(lcl = -1, ucl = 1) * stats::qnorm((1 + conf_level) / 2) * se) |
211 | 18x |
x[[as.character(increment)]] <- list(or = or, ci = ci) |
212 |
} |
|
213 | 12x |
y[[var_level]] <- x |
214 |
} |
|
215 |
} else { |
|
216 | 3x |
checkmate::assert_set_equal(xs_class[odds_ratio_var], "numeric") |
217 | 3x |
checkmate::assert_set_equal(xs_class[interaction_var], "factor") |
218 | 3x |
for (var_level in xs_level[[interaction_var]]) { |
219 | 9x |
coef_names <- odds_ratio_var |
220 | 9x |
if (var_level != xs_level[[interaction_var]][1]) { |
221 | 6x |
interaction_coef_name <- h_interaction_coef_name( |
222 | 6x |
interaction_vars, |
223 | 6x |
c(odds_ratio_var, ""), |
224 | 6x |
c(interaction_var, var_level) |
225 |
) |
|
226 | 6x |
coef_names <- c( |
227 | 6x |
coef_names, |
228 | 6x |
interaction_coef_name |
229 |
) |
|
230 |
} |
|
231 | 9x |
if (length(coef_names) > 1) { |
232 | 6x |
xvec <- t(c(1, 1)) |
233 | 6x |
est <- as.numeric(xvec %*% xs_coef[coef_names]) |
234 | 6x |
se <- sqrt(as.numeric(xvec %*% xs_vcov[coef_names, coef_names] %*% t(xvec))) |
235 |
} else { |
|
236 | 3x |
est <- xs_coef[coef_names] |
237 | 3x |
se <- sqrt(as.numeric(xs_vcov[coef_names, coef_names])) |
238 |
} |
|
239 | 9x |
or <- exp(est) |
240 | 9x |
ci <- exp(est + c(lcl = -1, ucl = 1) * stats::qnorm((1 + conf_level) / 2) * se) |
241 | 9x |
y[[var_level]] <- list(or = or, ci = ci) |
242 |
} |
|
243 |
} |
|
244 | 9x |
y |
245 |
} |
|
246 | ||
247 |
#' @describeIn h_logistic_regression Helper function to calculate the odds ratio estimates |
|
248 |
#' in case of an interaction. This is a wrapper for [h_or_cont_interaction()] and |
|
249 |
#' [h_or_cat_interaction()]. |
|
250 |
#' |
|
251 |
#' @return Odds ratio. |
|
252 |
#' |
|
253 |
#' @export |
|
254 |
h_or_interaction <- function(odds_ratio_var, |
|
255 |
interaction_var, |
|
256 |
fit_glm, |
|
257 |
at = NULL, |
|
258 |
conf_level = 0.95) { |
|
259 | 13x |
xs_class <- attr(fit_glm$terms, "dataClasses") |
260 | 13x |
if (any(xs_class[c(odds_ratio_var, interaction_var)] == "numeric")) { |
261 | 7x |
h_or_cont_interaction( |
262 | 7x |
odds_ratio_var, |
263 | 7x |
interaction_var, |
264 | 7x |
fit_glm, |
265 | 7x |
at = at, |
266 | 7x |
conf_level = conf_level |
267 |
) |
|
268 | 6x |
} else if (all(xs_class[c(odds_ratio_var, interaction_var)] == "factor")) { |
269 | 6x |
h_or_cat_interaction( |
270 | 6x |
odds_ratio_var, |
271 | 6x |
interaction_var, |
272 | 6x |
fit_glm, |
273 | 6x |
conf_level = conf_level |
274 |
) |
|
275 |
} else { |
|
276 | ! |
stop("wrong interaction variable class, the interaction variable is not a numeric nor a factor") |
277 |
} |
|
278 |
} |
|
279 | ||
280 |
#' @describeIn h_logistic_regression Helper function to construct term labels from simple terms and the table |
|
281 |
#' of numbers of patients. |
|
282 |
#' |
|
283 |
#' @param terms (`character`)\cr simple terms. |
|
284 |
#' @param table (`table`)\cr table containing numbers for terms. |
|
285 |
#' |
|
286 |
#' @return Term labels containing numbers of patients. |
|
287 |
#' |
|
288 |
#' @export |
|
289 |
h_simple_term_labels <- function(terms, |
|
290 |
table) { |
|
291 | 45x |
checkmate::assert_true(is.table(table)) |
292 | 45x |
checkmate::assert_multi_class(terms, classes = c("factor", "character")) |
293 | 45x |
terms <- as.character(terms) |
294 | 45x |
term_n <- table[terms] |
295 | 45x |
paste0(terms, ", n = ", term_n) |
296 |
} |
|
297 | ||
298 |
#' @describeIn h_logistic_regression Helper function to construct term labels from interaction terms and the table |
|
299 |
#' of numbers of patients. |
|
300 |
#' |
|
301 |
#' @param terms1 (`character`)\cr terms for first dimension (rows). |
|
302 |
#' @param terms2 (`character`)\cr terms for second dimension (rows). |
|
303 |
#' @param any (`flag`)\cr whether any of `term1` and `term2` can be fulfilled to count the |
|
304 |
#' number of patients. In that case they can only be scalar (strings). |
|
305 |
#' |
|
306 |
#' @return Term labels containing numbers of patients. |
|
307 |
#' |
|
308 |
#' @export |
|
309 |
h_interaction_term_labels <- function(terms1, |
|
310 |
terms2, |
|
311 |
table, |
|
312 |
any = FALSE) { |
|
313 | 8x |
checkmate::assert_true(is.table(table)) |
314 | 8x |
checkmate::assert_flag(any) |
315 | 8x |
checkmate::assert_multi_class(terms1, classes = c("factor", "character")) |
316 | 8x |
checkmate::assert_multi_class(terms2, classes = c("factor", "character")) |
317 | 8x |
terms1 <- as.character(terms1) |
318 | 8x |
terms2 <- as.character(terms2) |
319 | 8x |
if (any) { |
320 | 4x |
checkmate::assert_scalar(terms1) |
321 | 4x |
checkmate::assert_scalar(terms2) |
322 | 4x |
paste0( |
323 | 4x |
terms1, " or ", terms2, ", n = ", |
324 |
# Note that we double count in the initial sum the cell [terms1, terms2], therefore subtract. |
|
325 | 4x |
sum(c(table[terms1, ], table[, terms2])) - table[terms1, terms2] |
326 |
) |
|
327 |
} else { |
|
328 | 4x |
term_n <- table[cbind(terms1, terms2)] |
329 | 4x |
paste0(terms1, " * ", terms2, ", n = ", term_n) |
330 |
} |
|
331 |
} |
|
332 | ||
333 |
#' @describeIn h_logistic_regression Helper function to tabulate the main effect |
|
334 |
#' results of a (conditional) logistic regression model. |
|
335 |
#' |
|
336 |
#' @return Tabulated main effect results from a logistic regression model. |
|
337 |
#' |
|
338 |
#' @examples |
|
339 |
#' h_glm_simple_term_extract("AGE", mod1) |
|
340 |
#' h_glm_simple_term_extract("ARMCD", mod1) |
|
341 |
#' |
|
342 |
#' @export |
|
343 |
h_glm_simple_term_extract <- function(x, fit_glm) { |
|
344 | 61x |
checkmate::assert_multi_class(fit_glm, c("glm", "clogit")) |
345 | 61x |
checkmate::assert_string(x) |
346 | ||
347 | 61x |
xs_class <- attr(fit_glm$terms, "dataClasses") |
348 | 61x |
xs_level <- fit_glm$xlevels |
349 | 61x |
xs_coef <- summary(fit_glm)$coefficients |
350 | 61x |
stats <- if (inherits(fit_glm, "glm")) { |
351 | 49x |
c("estimate" = "Estimate", "std_error" = "Std. Error", "pvalue" = "Pr(>|z|)") |
352 |
} else { |
|
353 | 12x |
c("estimate" = "coef", "std_error" = "se(coef)", "pvalue" = "Pr(>|z|)") |
354 |
} |
|
355 |
# Make sure x is not an interaction term. |
|
356 | 61x |
checkmate::assert_subset(x, names(xs_class)) |
357 | 61x |
x_sel <- if (xs_class[x] == "numeric") x else paste0(x, xs_level[[x]][-1]) |
358 | 61x |
x_stats <- as.data.frame(xs_coef[x_sel, stats, drop = FALSE], stringsAsFactors = FALSE) |
359 | 61x |
colnames(x_stats) <- names(stats) |
360 | 61x |
x_stats$estimate <- as.list(x_stats$estimate) |
361 | 61x |
x_stats$std_error <- as.list(x_stats$std_error) |
362 | 61x |
x_stats$pvalue <- as.list(x_stats$pvalue) |
363 | 61x |
x_stats$df <- as.list(1) |
364 | 61x |
if (xs_class[x] == "numeric") { |
365 | 46x |
x_stats$term <- x |
366 | 46x |
x_stats$term_label <- if (inherits(fit_glm, "glm")) { |
367 | 34x |
formatters::var_labels(fit_glm$data[x], fill = TRUE) |
368 |
} else { |
|
369 |
# We just fill in here with the `term` itself as we don't have the data available. |
|
370 | 12x |
x |
371 |
} |
|
372 | 46x |
x_stats$is_variable_summary <- FALSE |
373 | 46x |
x_stats$is_term_summary <- TRUE |
374 |
} else { |
|
375 | 15x |
checkmate::assert_class(fit_glm, "glm") |
376 |
# The reason is that we don't have the original data set in the `clogit` object |
|
377 |
# and therefore cannot determine the `x_numbers` here. |
|
378 | 15x |
x_numbers <- table(fit_glm$data[[x]]) |
379 | 15x |
x_stats$term <- xs_level[[x]][-1] |
380 | 15x |
x_stats$term_label <- h_simple_term_labels(x_stats$term, x_numbers) |
381 | 15x |
x_stats$is_variable_summary <- FALSE |
382 | 15x |
x_stats$is_term_summary <- TRUE |
383 | 15x |
main_effects <- car::Anova(fit_glm, type = 3, test.statistic = "Wald") |
384 | 15x |
x_main <- data.frame( |
385 | 15x |
pvalue = main_effects[x, "Pr(>Chisq)", drop = TRUE], |
386 | 15x |
term = xs_level[[x]][1], |
387 | 15x |
term_label = paste("Reference", h_simple_term_labels(xs_level[[x]][1], x_numbers)), |
388 | 15x |
df = main_effects[x, "Df", drop = TRUE], |
389 | 15x |
stringsAsFactors = FALSE |
390 |
) |
|
391 | 15x |
x_main$pvalue <- as.list(x_main$pvalue) |
392 | 15x |
x_main$df <- as.list(x_main$df) |
393 | 15x |
x_main$estimate <- list(numeric(0)) |
394 | 15x |
x_main$std_error <- list(numeric(0)) |
395 | 15x |
if (length(xs_level[[x]][-1]) == 1) { |
396 | 6x |
x_main$pvalue <- list(numeric(0)) |
397 | 6x |
x_main$df <- list(numeric(0)) |
398 |
} |
|
399 | 15x |
x_main$is_variable_summary <- TRUE |
400 | 15x |
x_main$is_term_summary <- FALSE |
401 | 15x |
x_stats <- rbind(x_main, x_stats) |
402 |
} |
|
403 | 61x |
x_stats$variable <- x |
404 | 61x |
x_stats$variable_label <- if (inherits(fit_glm, "glm")) { |
405 | 49x |
formatters::var_labels(fit_glm$data[x], fill = TRUE) |
406 |
} else { |
|
407 | 12x |
x |
408 |
} |
|
409 | 61x |
x_stats$interaction <- "" |
410 | 61x |
x_stats$interaction_label <- "" |
411 | 61x |
x_stats$reference <- "" |
412 | 61x |
x_stats$reference_label <- "" |
413 | 61x |
rownames(x_stats) <- NULL |
414 | 61x |
x_stats[c( |
415 | 61x |
"variable", |
416 | 61x |
"variable_label", |
417 | 61x |
"term", |
418 | 61x |
"term_label", |
419 | 61x |
"interaction", |
420 | 61x |
"interaction_label", |
421 | 61x |
"reference", |
422 | 61x |
"reference_label", |
423 | 61x |
"estimate", |
424 | 61x |
"std_error", |
425 | 61x |
"df", |
426 | 61x |
"pvalue", |
427 | 61x |
"is_variable_summary", |
428 | 61x |
"is_term_summary" |
429 |
)] |
|
430 |
} |
|
431 | ||
432 |
#' @describeIn h_logistic_regression Helper function to tabulate the interaction term |
|
433 |
#' results of a logistic regression model. |
|
434 |
#' |
|
435 |
#' @return Tabulated interaction term results from a logistic regression model. |
|
436 |
#' |
|
437 |
#' @examples |
|
438 |
#' h_glm_interaction_extract("ARMCD:AGE", mod2) |
|
439 |
#' |
|
440 |
#' @export |
|
441 |
h_glm_interaction_extract <- function(x, fit_glm) { |
|
442 | 6x |
vars <- h_get_interaction_vars(fit_glm) |
443 | 6x |
xs_class <- attr(fit_glm$terms, "dataClasses") |
444 | ||
445 | 6x |
checkmate::assert_string(x) |
446 | ||
447 |
# Only take two-way interaction |
|
448 | 6x |
checkmate::assert_vector(vars, len = 2) |
449 | ||
450 |
# Only consider simple case: first variable in interaction is arm, a categorical variable |
|
451 | 6x |
checkmate::assert_disjunct(xs_class[vars[1]], "numeric") |
452 | ||
453 | 6x |
xs_level <- fit_glm$xlevels |
454 | 6x |
xs_coef <- summary(fit_glm)$coefficients |
455 | 6x |
main_effects <- car::Anova(fit_glm, type = 3, test.statistic = "Wald") |
456 | 6x |
stats <- c("estimate" = "Estimate", "std_error" = "Std. Error", "pvalue" = "Pr(>|z|)") |
457 | 6x |
v1_comp <- xs_level[[vars[1]]][-1] |
458 | 6x |
if (xs_class[vars[2]] == "numeric") { |
459 | 3x |
x_stats <- as.data.frame( |
460 | 3x |
xs_coef[paste0(vars[1], v1_comp, ":", vars[2]), stats, drop = FALSE], |
461 | 3x |
stringsAsFactors = FALSE |
462 |
) |
|
463 | 3x |
colnames(x_stats) <- names(stats) |
464 | 3x |
x_stats$term <- v1_comp |
465 | 3x |
x_numbers <- table(fit_glm$data[[vars[1]]]) |
466 | 3x |
x_stats$term_label <- h_simple_term_labels(v1_comp, x_numbers) |
467 | 3x |
v1_ref <- xs_level[[vars[1]]][1] |
468 | 3x |
term_main <- v1_ref |
469 | 3x |
ref_label <- h_simple_term_labels(v1_ref, x_numbers) |
470 | 3x |
} else if (xs_class[vars[2]] != "numeric") { |
471 | 3x |
v2_comp <- xs_level[[vars[2]]][-1] |
472 | 3x |
v1_v2_grid <- expand.grid(v1 = v1_comp, v2 = v2_comp) |
473 | 3x |
x_sel <- paste( |
474 | 3x |
paste0(vars[1], v1_v2_grid$v1), |
475 | 3x |
paste0(vars[2], v1_v2_grid$v2), |
476 | 3x |
sep = ":" |
477 |
) |
|
478 | 3x |
x_stats <- as.data.frame(xs_coef[x_sel, stats, drop = FALSE], stringsAsFactors = FALSE) |
479 | 3x |
colnames(x_stats) <- names(stats) |
480 | 3x |
x_stats$term <- paste(v1_v2_grid$v1, "*", v1_v2_grid$v2) |
481 | 3x |
x_numbers <- table(fit_glm$data[[vars[1]]], fit_glm$data[[vars[2]]]) |
482 | 3x |
x_stats$term_label <- h_interaction_term_labels(v1_v2_grid$v1, v1_v2_grid$v2, x_numbers) |
483 | 3x |
v1_ref <- xs_level[[vars[1]]][1] |
484 | 3x |
v2_ref <- xs_level[[vars[2]]][1] |
485 | 3x |
term_main <- paste(vars[1], vars[2], sep = " * ") |
486 | 3x |
ref_label <- h_interaction_term_labels(v1_ref, v2_ref, x_numbers, any = TRUE) |
487 |
} |
|
488 | 6x |
x_stats$df <- as.list(1) |
489 | 6x |
x_stats$pvalue <- as.list(x_stats$pvalue) |
490 | 6x |
x_stats$is_variable_summary <- FALSE |
491 | 6x |
x_stats$is_term_summary <- TRUE |
492 | 6x |
x_main <- data.frame( |
493 | 6x |
pvalue = main_effects[x, "Pr(>Chisq)", drop = TRUE], |
494 | 6x |
term = term_main, |
495 | 6x |
term_label = paste("Reference", ref_label), |
496 | 6x |
df = main_effects[x, "Df", drop = TRUE], |
497 | 6x |
stringsAsFactors = FALSE |
498 |
) |
|
499 | 6x |
x_main$pvalue <- as.list(x_main$pvalue) |
500 | 6x |
x_main$df <- as.list(x_main$df) |
501 | 6x |
x_main$estimate <- list(numeric(0)) |
502 | 6x |
x_main$std_error <- list(numeric(0)) |
503 | 6x |
x_main$is_variable_summary <- TRUE |
504 | 6x |
x_main$is_term_summary <- FALSE |
505 | ||
506 | 6x |
x_stats <- rbind(x_main, x_stats) |
507 | 6x |
x_stats$variable <- x |
508 | 6x |
x_stats$variable_label <- paste( |
509 | 6x |
"Interaction of", |
510 | 6x |
formatters::var_labels(fit_glm$data[vars[1]], fill = TRUE), |
511 |
"*", |
|
512 | 6x |
formatters::var_labels(fit_glm$data[vars[2]], fill = TRUE) |
513 |
) |
|
514 | 6x |
x_stats$interaction <- "" |
515 | 6x |
x_stats$interaction_label <- "" |
516 | 6x |
x_stats$reference <- "" |
517 | 6x |
x_stats$reference_label <- "" |
518 | 6x |
rownames(x_stats) <- NULL |
519 | 6x |
x_stats[c( |
520 | 6x |
"variable", |
521 | 6x |
"variable_label", |
522 | 6x |
"term", |
523 | 6x |
"term_label", |
524 | 6x |
"interaction", |
525 | 6x |
"interaction_label", |
526 | 6x |
"reference", |
527 | 6x |
"reference_label", |
528 | 6x |
"estimate", |
529 | 6x |
"std_error", |
530 | 6x |
"df", |
531 | 6x |
"pvalue", |
532 | 6x |
"is_variable_summary", |
533 | 6x |
"is_term_summary" |
534 |
)] |
|
535 |
} |
|
536 | ||
537 |
#' @describeIn h_logistic_regression Helper function to tabulate the interaction |
|
538 |
#' results of a logistic regression model. This basically is a wrapper for |
|
539 |
#' [h_or_interaction()] and [h_glm_simple_term_extract()] which puts the results |
|
540 |
#' in the right data frame format. |
|
541 |
#' |
|
542 |
#' @return A `data.frame` of tabulated interaction term results from a logistic regression model. |
|
543 |
#' |
|
544 |
#' @examples |
|
545 |
#' h_glm_inter_term_extract("AGE", "ARMCD", mod2) |
|
546 |
#' |
|
547 |
#' @export |
|
548 |
h_glm_inter_term_extract <- function(odds_ratio_var, |
|
549 |
interaction_var, |
|
550 |
fit_glm, |
|
551 |
...) { |
|
552 |
# First obtain the main effects. |
|
553 | 11x |
main_stats <- h_glm_simple_term_extract(odds_ratio_var, fit_glm) |
554 | 11x |
main_stats$is_reference_summary <- FALSE |
555 | 11x |
main_stats$odds_ratio <- NA |
556 | 11x |
main_stats$lcl <- NA |
557 | 11x |
main_stats$ucl <- NA |
558 | ||
559 |
# Then we get the odds ratio estimates and put into df form. |
|
560 | 11x |
or_numbers <- h_or_interaction(odds_ratio_var, interaction_var, fit_glm, ...) |
561 | 11x |
is_num_or_var <- attr(fit_glm$terms, "dataClasses")[odds_ratio_var] == "numeric" |
562 | ||
563 | 11x |
if (is_num_or_var) { |
564 |
# Numeric OR variable case. |
|
565 | 3x |
references <- names(or_numbers) |
566 | 3x |
n_ref <- length(references) |
567 | ||
568 | 3x |
extract_from_list <- function(l, name, pos = 1) { |
569 | 9x |
unname(unlist( |
570 | 9x |
lapply(or_numbers, function(x) { |
571 | 27x |
x[[name]][pos] |
572 |
}) |
|
573 |
)) |
|
574 |
} |
|
575 | 3x |
or_stats <- data.frame( |
576 | 3x |
variable = odds_ratio_var, |
577 | 3x |
variable_label = unname(formatters::var_labels(fit_glm$data[odds_ratio_var], fill = TRUE)), |
578 | 3x |
term = odds_ratio_var, |
579 | 3x |
term_label = unname(formatters::var_labels(fit_glm$data[odds_ratio_var], fill = TRUE)), |
580 | 3x |
interaction = interaction_var, |
581 | 3x |
interaction_label = unname(formatters::var_labels(fit_glm$data[interaction_var], fill = TRUE)), |
582 | 3x |
reference = references, |
583 | 3x |
reference_label = references, |
584 | 3x |
estimate = NA, |
585 | 3x |
std_error = NA, |
586 | 3x |
odds_ratio = extract_from_list(or_numbers, "or"), |
587 | 3x |
lcl = extract_from_list(or_numbers, "ci", pos = "lcl"), |
588 | 3x |
ucl = extract_from_list(or_numbers, "ci", pos = "ucl"), |
589 | 3x |
df = NA, |
590 | 3x |
pvalue = NA, |
591 | 3x |
is_variable_summary = FALSE, |
592 | 3x |
is_term_summary = FALSE, |
593 | 3x |
is_reference_summary = TRUE |
594 |
) |
|
595 |
} else { |
|
596 |
# Categorical OR variable case. |
|
597 | 8x |
references <- names(or_numbers[[1]]) |
598 | 8x |
n_ref <- length(references) |
599 | ||
600 | 8x |
extract_from_list <- function(l, name, pos = 1) { |
601 | 24x |
unname(unlist( |
602 | 24x |
lapply(or_numbers, function(x) { |
603 | 42x |
lapply(x, function(y) y[[name]][pos]) |
604 |
}) |
|
605 |
)) |
|
606 |
} |
|
607 | 8x |
or_stats <- data.frame( |
608 | 8x |
variable = odds_ratio_var, |
609 | 8x |
variable_label = unname(formatters::var_labels(fit_glm$data[odds_ratio_var], fill = TRUE)), |
610 | 8x |
term = rep(names(or_numbers), each = n_ref), |
611 | 8x |
term_label = h_simple_term_labels(rep(names(or_numbers), each = n_ref), table(fit_glm$data[[odds_ratio_var]])), |
612 | 8x |
interaction = interaction_var, |
613 | 8x |
interaction_label = unname(formatters::var_labels(fit_glm$data[interaction_var], fill = TRUE)), |
614 | 8x |
reference = unlist(lapply(or_numbers, names)), |
615 | 8x |
reference_label = unlist(lapply(or_numbers, names)), |
616 | 8x |
estimate = NA, |
617 | 8x |
std_error = NA, |
618 | 8x |
odds_ratio = extract_from_list(or_numbers, "or"), |
619 | 8x |
lcl = extract_from_list(or_numbers, "ci", pos = "lcl"), |
620 | 8x |
ucl = extract_from_list(or_numbers, "ci", pos = "ucl"), |
621 | 8x |
df = NA, |
622 | 8x |
pvalue = NA, |
623 | 8x |
is_variable_summary = FALSE, |
624 | 8x |
is_term_summary = FALSE, |
625 | 8x |
is_reference_summary = TRUE |
626 |
) |
|
627 |
} |
|
628 | ||
629 | 11x |
df <- rbind( |
630 | 11x |
main_stats[, names(or_stats)], |
631 | 11x |
or_stats |
632 |
) |
|
633 | 11x |
df[order(-df$is_variable_summary, df$term, -df$is_term_summary, df$reference), ] |
634 |
} |
|
635 | ||
636 |
#' @describeIn h_logistic_regression Helper function to tabulate the results including |
|
637 |
#' odds ratios and confidence intervals of simple terms. |
|
638 |
#' |
|
639 |
#' @return Tabulated statistics for the given variable(s) from the logistic regression model. |
|
640 |
#' |
|
641 |
#' @examples |
|
642 |
#' h_logistic_simple_terms("AGE", mod1) |
|
643 |
#' |
|
644 |
#' @export |
|
645 |
h_logistic_simple_terms <- function(x, fit_glm, conf_level = 0.95) { |
|
646 | 40x |
checkmate::assert_multi_class(fit_glm, c("glm", "clogit")) |
647 | 40x |
if (inherits(fit_glm, "glm")) { |
648 | 29x |
checkmate::assert_set_equal(fit_glm$family$family, "binomial") |
649 |
} |
|
650 | 40x |
terms_name <- attr(stats::terms(fit_glm), "term.labels") |
651 | 40x |
xs_class <- attr(fit_glm$terms, "dataClasses") |
652 | 40x |
interaction <- terms_name[which(!terms_name %in% names(xs_class))] |
653 | 40x |
checkmate::assert_subset(x, terms_name) |
654 | 40x |
if (length(interaction) != 0) { |
655 |
# Make sure any item in x is not part of interaction term |
|
656 | 1x |
checkmate::assert_disjunct(x, unlist(strsplit(interaction, ":"))) |
657 |
} |
|
658 | 40x |
x_stats <- lapply(x, h_glm_simple_term_extract, fit_glm) |
659 | 40x |
x_stats <- do.call(rbind, x_stats) |
660 | 40x |
q_norm <- stats::qnorm((1 + conf_level) / 2) |
661 | 40x |
x_stats$odds_ratio <- lapply(x_stats$estimate, exp) |
662 | 40x |
x_stats$lcl <- Map(function(or, se) exp(log(or) - q_norm * se), x_stats$odds_ratio, x_stats$std_error) |
663 | 40x |
x_stats$ucl <- Map(function(or, se) exp(log(or) + q_norm * se), x_stats$odds_ratio, x_stats$std_error) |
664 | 40x |
x_stats$ci <- Map(function(lcl, ucl) c(lcl, ucl), lcl = x_stats$lcl, ucl = x_stats$ucl) |
665 | 40x |
x_stats |
666 |
} |
|
667 | ||
668 |
#' @describeIn h_logistic_regression Helper function to tabulate the results including |
|
669 |
#' odds ratios and confidence intervals of interaction terms. |
|
670 |
#' |
|
671 |
#' @return Tabulated statistics for the given variable(s) from the logistic regression model. |
|
672 |
#' |
|
673 |
#' @examples |
|
674 |
#' h_logistic_inter_terms(c("RACE", "AGE", "ARMCD", "AGE:ARMCD"), mod2) |
|
675 |
#' |
|
676 |
#' @export |
|
677 |
h_logistic_inter_terms <- function(x, |
|
678 |
fit_glm, |
|
679 |
conf_level = 0.95, |
|
680 |
at = NULL) { |
|
681 |
# Find out the interaction variables and interaction term. |
|
682 | 4x |
inter_vars <- h_get_interaction_vars(fit_glm) |
683 | 4x |
checkmate::assert_vector(inter_vars, len = 2) |
684 | ||
685 | ||
686 | 4x |
inter_term_index <- intersect(grep(inter_vars[1], x), grep(inter_vars[2], x)) |
687 | 4x |
inter_term <- x[inter_term_index] |
688 | ||
689 |
# For the non-interaction vars we need the standard stuff. |
|
690 | 4x |
normal_terms <- setdiff(x, union(inter_vars, inter_term)) |
691 | ||
692 | 4x |
x_stats <- lapply(normal_terms, h_glm_simple_term_extract, fit_glm) |
693 | 4x |
x_stats <- do.call(rbind, x_stats) |
694 | 4x |
q_norm <- stats::qnorm((1 + conf_level) / 2) |
695 | 4x |
x_stats$odds_ratio <- lapply(x_stats$estimate, exp) |
696 | 4x |
x_stats$lcl <- Map(function(or, se) exp(log(or) - q_norm * se), x_stats$odds_ratio, x_stats$std_error) |
697 | 4x |
x_stats$ucl <- Map(function(or, se) exp(log(or) + q_norm * se), x_stats$odds_ratio, x_stats$std_error) |
698 | 4x |
normal_stats <- x_stats |
699 | 4x |
normal_stats$is_reference_summary <- FALSE |
700 | ||
701 |
# Now the interaction term itself. |
|
702 | 4x |
inter_term_stats <- h_glm_interaction_extract(inter_term, fit_glm) |
703 | 4x |
inter_term_stats$odds_ratio <- NA |
704 | 4x |
inter_term_stats$lcl <- NA |
705 | 4x |
inter_term_stats$ucl <- NA |
706 | 4x |
inter_term_stats$is_reference_summary <- FALSE |
707 | ||
708 | 4x |
is_intervar1_numeric <- attr(fit_glm$terms, "dataClasses")[inter_vars[1]] == "numeric" |
709 | ||
710 |
# Interaction stuff. |
|
711 | 4x |
inter_stats_one <- h_glm_inter_term_extract( |
712 | 4x |
inter_vars[1], |
713 | 4x |
inter_vars[2], |
714 | 4x |
fit_glm, |
715 | 4x |
conf_level = conf_level, |
716 | 4x |
at = `if`(is_intervar1_numeric, NULL, at) |
717 |
) |
|
718 | 4x |
inter_stats_two <- h_glm_inter_term_extract( |
719 | 4x |
inter_vars[2], |
720 | 4x |
inter_vars[1], |
721 | 4x |
fit_glm, |
722 | 4x |
conf_level = conf_level, |
723 | 4x |
at = `if`(is_intervar1_numeric, at, NULL) |
724 |
) |
|
725 | ||
726 |
# Now just combine everything in one data frame. |
|
727 | 4x |
col_names <- c( |
728 | 4x |
"variable", |
729 | 4x |
"variable_label", |
730 | 4x |
"term", |
731 | 4x |
"term_label", |
732 | 4x |
"interaction", |
733 | 4x |
"interaction_label", |
734 | 4x |
"reference", |
735 | 4x |
"reference_label", |
736 | 4x |
"estimate", |
737 | 4x |
"std_error", |
738 | 4x |
"df", |
739 | 4x |
"pvalue", |
740 | 4x |
"odds_ratio", |
741 | 4x |
"lcl", |
742 | 4x |
"ucl", |
743 | 4x |
"is_variable_summary", |
744 | 4x |
"is_term_summary", |
745 | 4x |
"is_reference_summary" |
746 |
) |
|
747 | 4x |
df <- rbind( |
748 | 4x |
inter_stats_one[, col_names], |
749 | 4x |
inter_stats_two[, col_names], |
750 | 4x |
inter_term_stats[, col_names] |
751 |
) |
|
752 | 4x |
if (length(normal_terms) > 0) { |
753 | 4x |
df <- rbind( |
754 | 4x |
normal_stats[, col_names], |
755 | 4x |
df |
756 |
) |
|
757 |
} |
|
758 | 4x |
df$ci <- combine_vectors(df$lcl, df$ucl) |
759 | 4x |
df |
760 |
} |
1 |
#' Kaplan-Meier Plot |
|
2 |
#' |
|
3 |
#' @description `r lifecycle::badge("stable")` |
|
4 |
#' |
|
5 |
#' From a survival model, a graphic is rendered along with tabulated annotation |
|
6 |
#' including the number of patient at risk at given time and the median survival |
|
7 |
#' per group. |
|
8 |
#' |
|
9 |
#' @inheritParams grid::gTree |
|
10 |
#' @inheritParams argument_convention |
|
11 |
#' @param df (`data.frame`)\cr data set containing all analysis variables. |
|
12 |
#' @param variables (named `list`)\cr variable names. Details are: |
|
13 |
#' * `tte` (`numeric`)\cr variable indicating time-to-event duration values. |
|
14 |
#' * `is_event` (`logical`)\cr event variable. `TRUE` if event, `FALSE` if time to event is censored. |
|
15 |
#' * `arm` (`factor`)\cr the treatment group variable. |
|
16 |
#' * `strat` (`character` or `NULL`)\cr variable names indicating stratification factors. |
|
17 |
#' @param control_surv (`list`)\cr parameters for comparison details, specified by using |
|
18 |
#' the helper function [control_surv_timepoint()]. Some possible parameter options are: |
|
19 |
#' * `conf_level` (`proportion`)\cr confidence level of the interval for survival rate. |
|
20 |
#' * `conf_type` (`string`)\cr "plain" (default), "log", "log-log" for confidence interval type, |
|
21 |
#' see more in [survival::survfit()]. Note that the option "none" is no longer supported. |
|
22 |
#' @param xticks (`numeric`, `number`, or `NULL`)\cr numeric vector of ticks or single number with spacing |
|
23 |
#' between ticks on the x axis. If `NULL` (default), [labeling::extended()] is used to determine |
|
24 |
#' an optimal tick position on the x axis. |
|
25 |
#' @param yval (`string`)\cr value of y-axis. Options are `Survival` (default) and `Failure` probability. |
|
26 |
#' @param censor_show (`flag`)\cr whether to show censored. |
|
27 |
#' @param xlab (`string`)\cr label of x-axis. |
|
28 |
#' @param ylab (`string`)\cr label of y-axis. |
|
29 |
#' @param title (`string`)\cr title for plot. |
|
30 |
#' @param footnotes (`string`)\cr footnotes for plot. |
|
31 |
#' @param col (`character`)\cr lines colors. Length of a vector sho |