1 |
#' Line plot with optional table |
|
2 |
#' |
|
3 |
#' @description `r lifecycle::badge("stable")` |
|
4 |
#' |
|
5 |
#' Line plot with the optional table. |
|
6 |
#' |
|
7 |
#' @inheritParams argument_convention |
|
8 |
#' @param alt_counts_df (`data.frame` or `NULL`)\cr data set that will be used (only) |
|
9 |
#' to counts objects in groups for stratification. |
|
10 |
#' @param variables (named `character`) vector of variable names in `df` which should include: |
|
11 |
#' * `x` (`string`)\cr name of x-axis variable. |
|
12 |
#' * `y` (`string`)\cr name of y-axis variable. |
|
13 |
#' * `group_var` (`string` or `NULL`)\cr name of grouping variable (or strata), i.e. treatment arm. |
|
14 |
#' Can be `NA` to indicate lack of groups. |
|
15 |
#' * `subject_var` (`string` or `NULL`)\cr name of subject variable. Only applies if `group_var` is |
|
16 |
#' not NULL. |
|
17 |
#' * `paramcd` (`string` or `NA`)\cr name of the variable for parameter's code. Used for y-axis label and plot's |
|
18 |
#' subtitle. Can be `NA` if `paramcd` is not to be added to the y-axis label or subtitle. |
|
19 |
#' * `y_unit` (`string` or `NA`)\cr name of variable with units of `y`. Used for y-axis label and plot's subtitle. |
|
20 |
#' Can be `NA` if y unit is not to be added to the y-axis label or subtitle. |
|
21 |
#' * `facet_var` (`string` or `NA`)\cr name of the secondary grouping variable used for plot faceting, i.e. treatment |
|
22 |
#' arm. Can be `NA` to indicate lack of groups. |
|
23 |
#' @param mid (`character` or `NULL`)\cr names of the statistics that will be plotted as midpoints. |
|
24 |
#' All the statistics indicated in `mid` variable must be present in the object returned by `sfun`, |
|
25 |
#' and be of a `double` or `numeric` type vector of length one. |
|
26 |
#' @param interval (`character` or `NULL`)\cr names of the statistics that will be plotted as intervals. |
|
27 |
#' All the statistics indicated in `interval` variable must be present in the object returned by `sfun`, |
|
28 |
#' and be of a `double` or `numeric` type vector of length two. Set `interval = NULL` if intervals should not be |
|
29 |
#' added to the plot. |
|
30 |
#' @param whiskers (`character`)\cr names of the interval whiskers that will be plotted. Names must match names |
|
31 |
#' of the list element `interval` that will be returned by `sfun` (e.g. `mean_ci_lwr` element of |
|
32 |
#' `sfun(x)[["mean_ci"]]`). It is possible to specify one whisker only, or to suppress all whiskers by setting |
|
33 |
#' `interval = NULL`. |
|
34 |
#' @param table (`character` or `NULL`)\cr names of the statistics that will be displayed in the table below the plot. |
|
35 |
#' All the statistics indicated in `table` variable must be present in the object returned by `sfun`. |
|
36 |
#' @param sfun (`function`)\cr the function to compute the values of required statistics. It must return a named `list` |
|
37 |
#' with atomic vectors. The names of the `list` elements refer to the names of the statistics and are used by `mid`, |
|
38 |
#' `interval`, `table`. It must be able to accept as input a vector with data for which statistics are computed. |
|
39 |
#' @param ... optional arguments to `sfun`. |
|
40 |
#' @param mid_type (`string`)\cr controls the type of the `mid` plot, it can be point (`"p"`), line (`"l"`), |
|
41 |
#' or point and line (`"pl"`). |
|
42 |
#' @param mid_point_size (`numeric(1)`)\cr font size of the `mid` plot points. |
|
43 |
#' @param position (`character` or `call`)\cr geom element position adjustment, either as a string, or the result of |
|
44 |
#' a call to a position adjustment function. |
|
45 |
#' @param legend_title (`string`)\cr legend title. |
|
46 |
#' @param legend_position (`string`)\cr the position of the plot legend (`"none"`, `"left"`, `"right"`, `"bottom"`, |
|
47 |
#' `"top"`, or a two-element numeric vector). |
|
48 |
#' @param ggtheme (`theme`)\cr a graphical theme as provided by `ggplot2` to control styling of the plot. |
|
49 |
#' @param xticks (`numeric` or `NULL`)\cr numeric vector of tick positions or a single number with spacing |
|
50 |
#' between ticks on the x-axis, for use when `variables$x` is numeric. If `NULL` (default), [labeling::extended()] is |
|
51 |
#' used to determine optimal tick positions on the x-axis. If `variables$x` is not numeric, this argument is ignored. |
|
52 |
#' @param x_lab (`string` or `NULL`)\cr x-axis label. If `NULL` then no label will be added. |
|
53 |
#' @param y_lab (`string` or `NULL`)\cr y-axis label. If `NULL` then no label will be added. |
|
54 |
#' @param y_lab_add_paramcd (`flag`)\cr whether `paramcd`, i.e. `unique(df[[variables["paramcd"]]])` should be added |
|
55 |
#' to the y-axis label (`y_lab`). |
|
56 |
#' @param y_lab_add_unit (`flag`)\cr whether y-axis unit, i.e. `unique(df[[variables["y_unit"]]])` should be added |
|
57 |
#' to the y-axis label (`y_lab`). |
|
58 |
#' @param title (`string`)\cr plot title. |
|
59 |
#' @param subtitle (`string`)\cr plot subtitle. |
|
60 |
#' @param subtitle_add_paramcd (`flag`)\cr whether `paramcd`, i.e. `unique(df[[variables["paramcd"]]])` should be |
|
61 |
#' added to the plot's subtitle (`subtitle`). |
|
62 |
#' @param subtitle_add_unit (`flag`)\cr whether the y-axis unit, i.e. `unique(df[[variables["y_unit"]]])` should be |
|
63 |
#' added to the plot's subtitle (`subtitle`). |
|
64 |
#' @param caption (`string`)\cr optional caption below the plot. |
|
65 |
#' @param table_format (named `character` or `NULL`)\cr format patterns for descriptive statistics used in the |
|
66 |
#' (optional) table appended to the plot. It is passed directly to the `h_format_row` function through the `format` |
|
67 |
#' parameter. Names of `table_format` must match the names of statistics returned by `sfun` function. |
|
68 |
#' @param table_labels (named `character` or `NULL`)\cr labels for descriptive statistics used in the (optional) table |
|
69 |
#' appended to the plot. Names of `table_labels` must match the names of statistics returned by `sfun` function. |
|
70 |
#' @param table_font_size (`numeric(1)`)\cr font size of the text in the table. |
|
71 |
#' @param newpage `r lifecycle::badge("deprecated")` not used. |
|
72 |
#' @param col (`character`)\cr color(s). See `?ggplot2::aes_colour_fill_alpha` for example values. |
|
73 |
#' @param linetype (`character`)\cr line type(s). See `?ggplot2::aes_linetype_size_shape` for example values. |
|
74 |
#' @param errorbar_width (`numeric(1)`)\cr width of the error bars. |
|
75 |
#' |
|
76 |
#' @return A `ggplot` line plot (and statistics table if applicable). |
|
77 |
#' |
|
78 |
#' @examples |
|
79 |
#' library(nestcolor) |
|
80 |
#' |
|
81 |
#' adsl <- tern_ex_adsl |
|
82 |
#' adlb <- tern_ex_adlb %>% dplyr::filter(ANL01FL == "Y", PARAMCD == "ALT", AVISIT != "SCREENING") |
|
83 |
#' adlb$AVISIT <- droplevels(adlb$AVISIT) |
|
84 |
#' adlb <- dplyr::mutate(adlb, AVISIT = forcats::fct_reorder(AVISIT, AVISITN, min)) |
|
85 |
#' |
|
86 |
#' # Mean with CI |
|
87 |
#' g_lineplot(adlb, adsl, subtitle = "Laboratory Test:") |
|
88 |
#' |
|
89 |
#' # Mean with CI, no stratification with group_var |
|
90 |
#' g_lineplot(adlb, variables = control_lineplot_vars(group_var = NA)) |
|
91 |
#' |
|
92 |
#' # Mean, upper whisker of CI, no group_var(strata) counts N |
|
93 |
#' g_lineplot( |
|
94 |
#' adlb, |
|
95 |
#' whiskers = "mean_ci_upr", |
|
96 |
#' title = "Plot of Mean and Upper 95% Confidence Limit by Visit" |
|
97 |
#' ) |
|
98 |
#' |
|
99 |
#' # Median with CI |
|
100 |
#' g_lineplot( |
|
101 |
#' adlb, |
|
102 |
#' adsl, |
|
103 |
#' mid = "median", |
|
104 |
#' interval = "median_ci", |
|
105 |
#' whiskers = c("median_ci_lwr", "median_ci_upr"), |
|
106 |
#' title = "Plot of Median and 95% Confidence Limits by Visit" |
|
107 |
#' ) |
|
108 |
#' |
|
109 |
#' # Mean, +/- SD |
|
110 |
#' g_lineplot(adlb, adsl, |
|
111 |
#' interval = "mean_sdi", |
|
112 |
#' whiskers = c("mean_sdi_lwr", "mean_sdi_upr"), |
|
113 |
#' title = "Plot of Median +/- SD by Visit" |
|
114 |
#' ) |
|
115 |
#' |
|
116 |
#' # Mean with CI plot with stats table |
|
117 |
#' g_lineplot(adlb, adsl, table = c("n", "mean", "mean_ci")) |
|
118 |
#' |
|
119 |
#' # Mean with CI, table and customized confidence level |
|
120 |
#' g_lineplot( |
|
121 |
#' adlb, |
|
122 |
#' adsl, |
|
123 |
#' table = c("n", "mean", "mean_ci"), |
|
124 |
#' control = control_analyze_vars(conf_level = 0.80), |
|
125 |
#' title = "Plot of Mean and 80% Confidence Limits by Visit" |
|
126 |
#' ) |
|
127 |
#' |
|
128 |
#' # Mean with CI, table, filtered data |
|
129 |
#' adlb_f <- dplyr::filter(adlb, ARMCD != "ARM A" | AVISIT == "BASELINE") |
|
130 |
#' g_lineplot(adlb_f, table = c("n", "mean")) |
|
131 |
#' |
|
132 |
#' @export |
|
133 |
g_lineplot <- function(df, |
|
134 |
alt_counts_df = NULL, |
|
135 |
variables = control_lineplot_vars(), |
|
136 |
mid = "mean", |
|
137 |
interval = "mean_ci", |
|
138 |
whiskers = c("mean_ci_lwr", "mean_ci_upr"), |
|
139 |
table = NULL, |
|
140 |
sfun = s_summary, |
|
141 |
..., |
|
142 |
mid_type = "pl", |
|
143 |
mid_point_size = 2, |
|
144 |
position = ggplot2::position_dodge(width = 0.4), |
|
145 |
legend_title = NULL, |
|
146 |
legend_position = "bottom", |
|
147 |
ggtheme = nestcolor::theme_nest(), |
|
148 |
xticks = NULL, |
|
149 |
xlim = NULL, |
|
150 |
ylim = NULL, |
|
151 |
x_lab = obj_label(df[[variables[["x"]]]]), |
|
152 |
y_lab = NULL, |
|
153 |
y_lab_add_paramcd = TRUE, |
|
154 |
y_lab_add_unit = TRUE, |
|
155 |
title = "Plot of Mean and 95% Confidence Limits by Visit", |
|
156 |
subtitle = "", |
|
157 |
subtitle_add_paramcd = TRUE, |
|
158 |
subtitle_add_unit = TRUE, |
|
159 |
caption = NULL, |
|
160 |
table_format = NULL, |
|
161 |
table_labels = NULL, |
|
162 |
table_font_size = 3, |
|
163 |
errorbar_width = 0.45, |
|
164 |
newpage = lifecycle::deprecated(), |
|
165 |
col = NULL, |
|
166 |
linetype = NULL) { |
|
167 | 12x |
checkmate::assert_character(variables, any.missing = TRUE) |
168 | 12x |
checkmate::assert_character(mid, null.ok = TRUE) |
169 | 12x |
checkmate::assert_character(interval, null.ok = TRUE) |
170 | 12x |
checkmate::assert_character(col, null.ok = TRUE) |
171 | 12x |
checkmate::assert_character(linetype, null.ok = TRUE) |
172 | 12x |
checkmate::assert_numeric(xticks, null.ok = TRUE) |
173 | 12x |
checkmate::assert_numeric(xlim, finite = TRUE, any.missing = FALSE, len = 2, sorted = TRUE, null.ok = TRUE) |
174 | 12x |
checkmate::assert_numeric(ylim, finite = TRUE, any.missing = FALSE, len = 2, sorted = TRUE, null.ok = TRUE) |
175 | 12x |
checkmate::assert_number(errorbar_width, lower = 0) |
176 | 12x |
checkmate::assert_string(title, null.ok = TRUE) |
177 | 12x |
checkmate::assert_string(subtitle, null.ok = TRUE) |
178 | ||
179 | 12x |
if (!is.null(table)) { |
180 | 4x |
table_format <- get_formats_from_stats(table) |
181 | 4x |
table_labels <- get_labels_from_stats(table) |
182 |
} |
|
183 | ||
184 | 12x |
extra_args <- list(...) |
185 | 12x |
if ("control" %in% names(extra_args)) { |
186 | 4x |
if (!is.null(table) && all(table_labels == get_labels_from_stats(table))) { |
187 | 3x |
table_labels <- table_labels %>% labels_use_control(extra_args[["control"]]) |
188 |
} |
|
189 |
} |
|
190 | ||
191 | 12x |
if (is.character(interval)) { |
192 | 12x |
checkmate::assert_vector(whiskers, min.len = 0, max.len = 2) |
193 |
} |
|
194 | ||
195 | 12x |
if (length(whiskers) == 1) { |
196 | ! |
checkmate::assert_character(mid) |
197 |
} |
|
198 | ||
199 | 12x |
if (is.character(mid)) { |
200 | 12x |
checkmate::assert_scalar(mid_type) |
201 | 12x |
checkmate::assert_subset(mid_type, c("pl", "p", "l")) |
202 |
} |
|
203 | ||
204 | 12x |
x <- variables[["x"]] |
205 | 12x |
y <- variables[["y"]] |
206 | 12x |
paramcd <- variables["paramcd"] # NA if paramcd == NA or it is not in variables |
207 | 12x |
y_unit <- variables["y_unit"] # NA if y_unit == NA or it is not in variables |
208 | 12x |
if (is.na(variables["group_var"])) { |
209 | 1x |
group_var <- NULL # NULL if group_var == NA or it is not in variables |
210 |
} else { |
|
211 | 11x |
group_var <- variables[["group_var"]] |
212 | 11x |
subject_var <- variables[["subject_var"]] |
213 |
} |
|
214 | 12x |
if (is.na(variables["facet_var"])) { |
215 | 11x |
facet_var <- NULL # NULL if facet_var == NA or it is not in variables |
216 |
} else { |
|
217 | 1x |
facet_var <- variables[["facet_var"]] |
218 |
} |
|
219 | 12x |
checkmate::assert_flag(y_lab_add_paramcd, null.ok = TRUE) |
220 | 12x |
checkmate::assert_flag(subtitle_add_paramcd, null.ok = TRUE) |
221 | 12x |
if ((!is.null(y_lab) && y_lab_add_paramcd) || (!is.null(subtitle) && subtitle_add_paramcd)) { |
222 | 12x |
checkmate::assert_false(is.na(paramcd)) |
223 | 12x |
checkmate::assert_scalar(unique(df[[paramcd]])) |
224 |
} |
|
225 | ||
226 | 12x |
checkmate::assert_flag(y_lab_add_unit, null.ok = TRUE) |
227 | 12x |
checkmate::assert_flag(subtitle_add_unit, null.ok = TRUE) |
228 | 12x |
if ((!is.null(y_lab) && y_lab_add_unit) || (!is.null(subtitle) && subtitle_add_unit)) { |
229 | 12x |
checkmate::assert_false(is.na(y_unit)) |
230 | 12x |
checkmate::assert_scalar(unique(df[[y_unit]])) |
231 |
} |
|
232 | ||
233 | 12x |
if (!is.null(group_var) && !is.null(alt_counts_df)) { |
234 | 7x |
checkmate::assert_set_equal(unique(alt_counts_df[[group_var]]), unique(df[[group_var]])) |
235 |
} |
|
236 | ||
237 |
####################################### | |
|
238 |
# ---- Compute required statistics ---- |
|
239 |
####################################### | |
|
240 |
# Remove unused levels for x-axis |
|
241 | 12x |
if (is.factor(df[[x]])) { |
242 | 11x |
df[[x]] <- droplevels(df[[x]]) |
243 |
} |
|
244 | ||
245 | 12x |
if (!is.null(facet_var) && !is.null(group_var)) { |
246 | 1x |
df_grp <- tidyr::expand(df, .data[[facet_var]], .data[[group_var]], .data[[x]]) # expand based on levels of factors |
247 | 11x |
} else if (!is.null(group_var)) { |
248 | 10x |
df_grp <- tidyr::expand(df, .data[[group_var]], .data[[x]]) # expand based on levels of factors |
249 |
} else { |
|
250 | 1x |
df_grp <- tidyr::expand(df, NULL, .data[[x]]) |
251 |
} |
|
252 | ||
253 | 12x |
df_grp <- df_grp %>% |
254 | 12x |
dplyr::full_join(y = df[, c(facet_var, group_var, x, y)], by = c(facet_var, group_var, x), multiple = "all") %>% |
255 | 12x |
dplyr::group_by_at(c(facet_var, group_var, x)) |
256 | ||
257 | 12x |
df_stats <- df_grp %>% |
258 | 12x |
dplyr::summarise( |
259 | 12x |
data.frame(t(do.call(c, unname(sfun(.data[[y]])[c(mid, interval)])))), |
260 | 12x |
.groups = "drop" |
261 |
) |
|
262 | ||
263 | 12x |
df_stats <- df_stats[!is.na(df_stats[[mid]]), ] |
264 | ||
265 |
# add number of objects N in group_var (strata) |
|
266 | 12x |
if (!is.null(group_var) && !is.null(alt_counts_df)) { |
267 | 7x |
strata_N <- paste0(group_var, "_N") # nolint |
268 | ||
269 | 7x |
df_N <- stats::aggregate(eval(parse(text = subject_var)) ~ eval(parse(text = group_var)), data = alt_counts_df, FUN = function(x) length(unique(x))) # nolint |
270 | 7x |
colnames(df_N) <- c(group_var, "N") # nolint |
271 | 7x |
df_N[[strata_N]] <- paste0(df_N[[group_var]], " (N = ", df_N$N, ")") # nolint |
272 | ||
273 |
# keep strata factor levels |
|
274 | 7x |
matches <- sapply(unique(df_N[[group_var]]), function(x) { |
275 | 19x |
regex_pattern <- gsub("([][(){}^$.|*+?\\\\])", "\\\\\\1", x) |
276 | 19x |
unique(df_N[[paste0(group_var, "_N")]])[grepl( |
277 | 19x |
paste0("^", regex_pattern), |
278 | 19x |
unique(df_N[[paste0(group_var, "_N")]]) |
279 |
)] |
|
280 |
}) |
|
281 | 7x |
df_N[[paste0(group_var, "_N")]] <- factor(df_N[[group_var]]) # nolint |
282 | 7x |
levels(df_N[[paste0(group_var, "_N")]]) <- unlist(matches) # nolint |
283 | ||
284 |
# strata_N should not be in colnames(df_stats) |
|
285 | 7x |
checkmate::assert_disjunct(strata_N, colnames(df_stats)) |
286 | ||
287 | 7x |
df_stats <- merge(x = df_stats, y = df_N[, c(group_var, strata_N)], by = group_var) |
288 | 5x |
} else if (!is.null(group_var)) { |
289 | 4x |
strata_N <- group_var # nolint |
290 |
} else { |
|
291 | 1x |
strata_N <- NULL # nolint |
292 |
} |
|
293 | ||
294 |
############################################### | |
|
295 |
# ---- Prepare certain plot's properties. ---- |
|
296 |
############################################### | |
|
297 |
# legend title |
|
298 | 12x |
if (is.null(legend_title) && !is.null(group_var) && legend_position != "none") { |
299 | 11x |
legend_title <- attr(df[[group_var]], "label") |
300 |
} |
|
301 | ||
302 |
# y label |
|
303 | 12x |
if (!is.null(y_lab)) { |
304 | 4x |
if (y_lab_add_paramcd) { |
305 | 4x |
y_lab <- paste(y_lab, unique(df[[paramcd]])) |
306 |
} |
|
307 | ||
308 | 4x |
if (y_lab_add_unit) { |
309 | 4x |
y_lab <- paste0(y_lab, " (", unique(df[[y_unit]]), ")") |
310 |
} |
|
311 | ||
312 | 4x |
y_lab <- trimws(y_lab) |
313 |
} |
|
314 | ||
315 |
# subtitle |
|
316 | 12x |
if (!is.null(subtitle)) { |
317 | 12x |
if (subtitle_add_paramcd) { |
318 | 12x |
subtitle <- paste(subtitle, unique(df[[paramcd]])) |
319 |
} |
|
320 | ||
321 | 12x |
if (subtitle_add_unit) { |
322 | 12x |
subtitle <- paste0(subtitle, " (", unique(df[[y_unit]]), ")") |
323 |
} |
|
324 | ||
325 | 12x |
subtitle <- trimws(subtitle) |
326 |
} |
|
327 | ||
328 |
############################### | |
|
329 |
# ---- Build plot object. ---- |
|
330 |
############################### | |
|
331 | 12x |
p <- ggplot2::ggplot( |
332 | 12x |
data = df_stats, |
333 | 12x |
mapping = ggplot2::aes( |
334 | 12x |
x = .data[[x]], y = .data[[mid]], |
335 | 12x |
color = if (is.null(strata_N)) NULL else .data[[strata_N]], |
336 | 12x |
shape = if (is.null(strata_N)) NULL else .data[[strata_N]], |
337 | 12x |
lty = if (is.null(strata_N)) NULL else .data[[strata_N]], |
338 | 12x |
group = if (is.null(strata_N)) NULL else .data[[strata_N]] |
339 |
) |
|
340 |
) |
|
341 | ||
342 | 12x |
if (!is.null(group_var) && nlevels(df_stats[[strata_N]]) > 6) { |
343 | 1x |
p <- p + |
344 | 1x |
scale_shape_manual(values = seq(15, 15 + nlevels(df_stats[[strata_N]]))) |
345 |
} |
|
346 | ||
347 | 12x |
if (!is.null(mid)) { |
348 |
# points |
|
349 | 12x |
if (grepl("p", mid_type, fixed = TRUE)) { |
350 | 12x |
p <- p + ggplot2::geom_point(position = position, size = mid_point_size, na.rm = TRUE) |
351 |
} |
|
352 | ||
353 |
# lines - plotted only if there is a strata grouping (group_var) |
|
354 | 12x |
if (grepl("l", mid_type, fixed = TRUE) && !is.null(strata_N)) { # nolint |
355 | 11x |
p <- p + ggplot2::geom_line(position = position, na.rm = TRUE) |
356 |
} |
|
357 |
} |
|
358 | ||
359 |
# interval |
|
360 | 12x |
if (!is.null(interval)) { |
361 | 12x |
p <- p + |
362 | 12x |
ggplot2::geom_errorbar( |
363 | 12x |
ggplot2::aes(ymin = .data[[whiskers[1]]], ymax = .data[[whiskers[max(1, length(whiskers))]]]), |
364 | 12x |
width = errorbar_width, |
365 | 12x |
position = position |
366 |
) |
|
367 | ||
368 | 12x |
if (length(whiskers) == 1) { # lwr or upr only; mid is then required |
369 |
# workaround as geom_errorbar does not provide single-direction whiskers |
|
370 | ! |
p <- p + |
371 | ! |
ggplot2::geom_linerange( |
372 | ! |
data = df_stats[!is.na(df_stats[[whiskers]]), ], # as na.rm =TRUE does not suppress warnings |
373 | ! |
ggplot2::aes(ymin = .data[[mid]], ymax = .data[[whiskers]]), |
374 | ! |
position = position, |
375 | ! |
na.rm = TRUE, |
376 | ! |
show.legend = FALSE |
377 |
) |
|
378 |
} |
|
379 |
} |
|
380 | ||
381 | 12x |
if (is.numeric(df_stats[[x]])) { |
382 | 1x |
if (length(xticks) == 1) xticks <- seq(from = min(df_stats[[x]]), to = max(df_stats[[x]]), by = xticks) |
383 | 1x |
p <- p + ggplot2::scale_x_continuous(breaks = if (!is.null(xticks)) xticks else waiver(), limits = xlim) |
384 |
} |
|
385 | ||
386 | 12x |
p <- p + |
387 | 12x |
ggplot2::scale_y_continuous(labels = scales::comma, limits = ylim) + |
388 | 12x |
ggplot2::labs( |
389 | 12x |
title = title, |
390 | 12x |
subtitle = subtitle, |
391 | 12x |
caption = caption, |
392 | 12x |
color = legend_title, |
393 | 12x |
lty = legend_title, |
394 | 12x |
shape = legend_title, |
395 | 12x |
x = x_lab, |
396 | 12x |
y = y_lab |
397 |
) |
|
398 | ||
399 | 12x |
if (!is.null(col)) { |
400 | 1x |
p <- p + |
401 | 1x |
ggplot2::scale_color_manual(values = col) |
402 |
} |
|
403 | 12x |
if (!is.null(linetype)) { |
404 | 1x |
p <- p + |
405 | 1x |
ggplot2::scale_linetype_manual(values = linetype) |
406 |
} |
|
407 | ||
408 | 12x |
if (!is.null(facet_var)) { |
409 | 1x |
p <- p + |
410 | 1x |
facet_grid(cols = vars(df_stats[[facet_var]])) |
411 |
} |
|
412 | ||
413 | 12x |
if (!is.null(ggtheme)) { |
414 | 12x |
p <- p + ggtheme |
415 |
} else { |
|
416 | ! |
p <- p + |
417 | ! |
ggplot2::theme_bw() + |
418 | ! |
ggplot2::theme( |
419 | ! |
legend.key.width = grid::unit(1, "cm"), |
420 | ! |
legend.position = legend_position, |
421 | ! |
legend.direction = ifelse( |
422 | ! |
legend_position %in% c("top", "bottom"), |
423 | ! |
"horizontal", |
424 | ! |
"vertical" |
425 |
) |
|
426 |
) |
|
427 |
} |
|
428 | ||
429 |
############################################################# | |
|
430 |
# ---- Optionally, add table to the bottom of the plot. ---- |
|
431 |
############################################################# | |
|
432 | 12x |
if (!is.null(table)) { |
433 | 4x |
df_stats_table <- df_grp %>% |
434 | 4x |
dplyr::summarise( |
435 | 4x |
h_format_row( |
436 | 4x |
x = sfun(.data[[y]], ...)[table], |
437 | 4x |
format = table_format, |
438 | 4x |
labels = table_labels |
439 |
), |
|
440 | 4x |
.groups = "drop" |
441 |
) |
|
442 | ||
443 | 4x |
stats_lev <- rev(setdiff(colnames(df_stats_table), c(group_var, x))) |
444 | ||
445 | 4x |
df_stats_table <- df_stats_table %>% |
446 | 4x |
tidyr::pivot_longer( |
447 | 4x |
cols = -dplyr::all_of(c(group_var, x)), |
448 | 4x |
names_to = "stat", |
449 | 4x |
values_to = "value", |
450 | 4x |
names_ptypes = list(stat = factor(levels = stats_lev)) |
451 |
) |
|
452 | ||
453 | 4x |
tbl <- ggplot2::ggplot( |
454 | 4x |
df_stats_table, |
455 | 4x |
ggplot2::aes(x = .data[[x]], y = .data[["stat"]], label = .data[["value"]]) |
456 |
) + |
|
457 | 4x |
ggplot2::geom_text(size = table_font_size) + |
458 | 4x |
ggplot2::theme_bw() + |
459 | 4x |
ggplot2::theme( |
460 | 4x |
panel.border = ggplot2::element_blank(), |
461 | 4x |
panel.grid.major = ggplot2::element_blank(), |
462 | 4x |
panel.grid.minor = ggplot2::element_blank(), |
463 | 4x |
axis.ticks = ggplot2::element_blank(), |
464 | 4x |
axis.title = ggplot2::element_blank(), |
465 | 4x |
axis.text.x = ggplot2::element_blank(), |
466 | 4x |
axis.text.y = ggplot2::element_text(margin = ggplot2::margin(t = 0, r = 0, b = 0, l = 5)), |
467 | 4x |
strip.text = ggplot2::element_text(hjust = 0), |
468 | 4x |
strip.text.x = ggplot2::element_text(margin = ggplot2::margin(1.5, 0, 1.5, 0, "pt")), |
469 | 4x |
strip.background = ggplot2::element_rect(fill = "grey95", color = NA), |
470 | 4x |
legend.position = "none" |
471 |
) |
|
472 | ||
473 | 4x |
if (!is.null(group_var)) { |
474 | 4x |
tbl <- tbl + ggplot2::facet_wrap(facets = group_var, ncol = 1) |
475 |
} |
|
476 | ||
477 |
# align plot and table |
|
478 | 4x |
cowplot::plot_grid(p, tbl, ncol = 1, align = "v", axis = "tblr") |
479 |
} else { |
|
480 | 8x |
p |
481 |
} |
|
482 |
} |
|
483 | ||
484 |
#' Helper function to format the optional `g_lineplot` table |
|
485 |
#' |
|
486 |
#' @description `r lifecycle::badge("stable")` |
|
487 |
#' |
|
488 |
#' @param x (named `list`)\cr list of numerical values to be formatted and optionally labeled. |
|
489 |
#' Elements of `x` must be `numeric` vectors. |
|
490 |
#' @param format (named `character` or `NULL`)\cr format patterns for `x`. Names of the `format` must |
|
491 |
#' match the names of `x`. This parameter is passed directly to the `rtables::format_rcell` |
|
492 |
#' function through the `format` parameter. |
|
493 |
#' @param labels (named `character` or `NULL`)\cr optional labels for `x`. Names of the `labels` must |
|
494 |
#' match the names of `x`. When a label is not specified for an element of `x`, |
|
495 |
#' then this function tries to use `label` or `names` (in this order) attribute of that element |
|
496 |
#' (depending on which one exists and it is not `NULL` or `NA` or `NaN`). If none of these attributes |
|
497 |
#' are attached to a given element of `x`, then the label is automatically generated. |
|
498 |
#' |
|
499 |
#' @return A single row `data.frame` object. |
|
500 |
#' |
|
501 |
#' @examples |
|
502 |
#' mean_ci <- c(48, 51) |
|
503 |
#' x <- list(mean = 50, mean_ci = mean_ci) |
|
504 |
#' format <- c(mean = "xx.x", mean_ci = "(xx.xx, xx.xx)") |
|
505 |
#' labels <- c(mean = "My Mean") |
|
506 |
#' h_format_row(x, format, labels) |
|
507 |
#' |
|
508 |
#' attr(mean_ci, "label") <- "Mean 95% CI" |
|
509 |
#' x <- list(mean = 50, mean_ci = mean_ci) |
|
510 |
#' h_format_row(x, format, labels) |
|
511 |
#' |
|
512 |
#' @export |
|
513 |
h_format_row <- function(x, format, labels = NULL) { |
|
514 |
# cell: one row, one column data.frame |
|
515 | 73x |
format_cell <- function(x, format, label = NULL) { |
516 | 182x |
fc <- format_rcell(x = x, format = unlist(format)) |
517 | 182x |
if (is.na(fc)) { |
518 | ! |
fc <- "NA" |
519 |
} |
|
520 | 182x |
x_label <- attr(x, "label") |
521 | 182x |
if (!is.null(label) && !is.na(label)) { |
522 | 181x |
names(fc) <- label |
523 | 1x |
} else if (!is.null(x_label) && !is.na(x_label)) { |
524 | ! |
names(fc) <- x_label |
525 | 1x |
} else if (length(x) == length(fc)) { |
526 | ! |
names(fc) <- names(x) |
527 |
} |
|
528 | 182x |
as.data.frame(t(fc)) |
529 |
} |
|
530 | ||
531 | 73x |
row <- do.call( |
532 | 73x |
cbind, |
533 | 73x |
lapply( |
534 | 73x |
names(x), function(xn) format_cell(x[[xn]], format = format[xn], label = labels[xn]) |
535 |
) |
|
536 |
) |
|
537 | ||
538 | 73x |
row |
539 |
} |
|
540 | ||
541 |
#' Control function for `g_lineplot()` |
|
542 |
#' |
|
543 |
#' @description `r lifecycle::badge("stable")` |
|
544 |
#' |
|
545 |
#' Default values for `variables` parameter in `g_lineplot` function. |
|
546 |
#' A variable's default value can be overwritten for any variable. |
|
547 |
#' |
|
548 |
#' @param x (`string`)\cr x-variable name. |
|
549 |
#' @param y (`string`)\cr y-variable name. |
|
550 |
#' @param group_var (`string` or `NA`)\cr group variable name. |
|
551 |
#' @param subject_var (`string` or `NA`)\cr subject variable name. |
|
552 |
#' @param facet_var (`string` or `NA`)\cr faceting variable name. |
|
553 |
#' @param paramcd (`string` or `NA`)\cr parameter code variable name. |
|
554 |
#' @param y_unit (`string` or `NA`)\cr y-axis unit variable name. |
|
555 |
#' |
|
556 |
#' @return A named character vector of variable names. |
|
557 |
#' |
|
558 |
#' @examples |
|
559 |
#' control_lineplot_vars() |
|
560 |
#' control_lineplot_vars(group_var = NA) |
|
561 |
#' |
|
562 |
#' @export |
|
563 |
control_lineplot_vars <- function(x = "AVISIT", |
|
564 |
y = "AVAL", |
|
565 |
group_var = "ARM", |
|
566 |
facet_var = NA, |
|
567 |
paramcd = "PARAMCD", |
|
568 |
y_unit = "AVALU", |
|
569 |
subject_var = "USUBJID") { |
|
570 | 15x |
checkmate::assert_string(x) |
571 | 15x |
checkmate::assert_string(y) |
572 | 15x |
checkmate::assert_string(group_var, na.ok = TRUE, null.ok = TRUE) |
573 | 15x |
checkmate::assert_string(facet_var, na.ok = TRUE, null.ok = TRUE) |
574 | 15x |
checkmate::assert_string(subject_var, na.ok = TRUE, null.ok = TRUE) |
575 | 15x |
checkmate::assert_string(paramcd, na.ok = TRUE, null.ok = TRUE) |
576 | 15x |
checkmate::assert_string(y_unit, na.ok = TRUE, null.ok = TRUE) |
577 | ||
578 | 15x |
variables <- c( |
579 | 15x |
x = x, y = y, group_var = group_var, paramcd = paramcd, |
580 | 15x |
y_unit = y_unit, subject_var = subject_var, facet_var = facet_var |
581 |
) |
|
582 | 15x |
return(variables) |
583 |
} |
1 |
#' Count patients with toxicity grades that have worsened from baseline by highest grade post-baseline |
|
2 |
#' |
|
3 |
#' @description `r lifecycle::badge("stable")` |
|
4 |
#' |
|
5 |
#' The analyze function [count_abnormal_lab_worsen_by_baseline()] creates a layout element to count patients with |
|
6 |
#' analysis toxicity grades which have worsened from baseline, categorized by highest (worst) grade post-baseline. |
|
7 |
#' |
|
8 |
#' This function analyzes primary analysis variable `var` which indicates analysis toxicity grades. Additional |
|
9 |
#' analysis variables that can be supplied as a list via the `variables` parameter are `id` (defaults to `USUBJID`), |
|
10 |
#' a variable to indicate unique subject identifiers, `baseline_var` (defaults to `BTOXGR`), a variable to indicate |
|
11 |
#' baseline toxicity grades, and `direction_var` (defaults to `GRADDIR`), a variable to indicate toxicity grade |
|
12 |
#' directions of interest to include (e.g. `"H"` (high), `"L"` (low), or `"B"` (both)). |
|
13 |
#' |
|
14 |
#' For the direction(s) specified in `direction_var`, patient counts by worst grade for patients who have |
|
15 |
#' worsened from baseline are calculated as follows: |
|
16 |
#' * `1` to `4`: The number of patients who have worsened from their baseline grades with worst |
|
17 |
#' grades 1-4, respectively. |
|
18 |
#' * `Any`: The total number of patients who have worsened from their baseline grades. |
|
19 |
#' |
|
20 |
#' Fractions are calculated by dividing the above counts by the number of patients who's analysis toxicity grades |
|
21 |
#' have worsened from baseline toxicity grades during treatment. |
|
22 |
#' |
|
23 |
#' Prior to using this function in your table layout you must use [rtables::split_rows_by()] to create a row |
|
24 |
#' split on variable `direction_var`. |
|
25 |
#' |
|
26 |
#' @inheritParams argument_convention |
|
27 |
#' @param variables (named `list` of `string`)\cr list of additional analysis variables including: |
|
28 |
#' * `id` (`string`)\cr subject variable name. |
|
29 |
#' * `baseline_var` (`string`)\cr name of the data column containing baseline toxicity variable. |
|
30 |
#' * `direction_var` (`string`)\cr see `direction_var` for more details. |
|
31 |
#' @param .stats (`character`)\cr statistics to select for the table. Run `get_stats("abnormal_by_worst_grade_worsen")` |
|
32 |
#' to see all available statistics. |
|
33 |
#' |
|
34 |
#' @seealso Relevant helper functions [h_adlb_worsen()] and [h_worsen_counter()] which are used within |
|
35 |
#' [s_count_abnormal_lab_worsen_by_baseline()] to process input data. |
|
36 |
#' |
|
37 |
#' @name abnormal_by_worst_grade_worsen |
|
38 |
#' @order 1 |
|
39 |
NULL |
|
40 | ||
41 |
#' Helper function to prepare ADLB with worst labs |
|
42 |
#' |
|
43 |
#' @description `r lifecycle::badge("stable")` |
|
44 |
#' |
|
45 |
#' Helper function to prepare a `df` for generate the patient count shift table. |
|
46 |
#' |
|
47 |
#' @param adlb (`data.frame`)\cr ADLB data frame. |
|
48 |
#' @param worst_flag_low (named `vector`)\cr worst low post-baseline lab grade flag variable. See how this is |
|
49 |
#' implemented in the following examples. |
|
50 |
#' @param worst_flag_high (named `vector`)\cr worst high post-baseline lab grade flag variable. See how this is |
|
51 |
#' implemented in the following examples. |
|
52 |
#' @param direction_var (`string`)\cr name of the direction variable specifying the direction of the shift table of |
|
53 |
#' interest. Only lab records flagged by `L`, `H` or `B` are included in the shift table. |
|
54 |
#' * `L`: low direction only |
|
55 |
#' * `H`: high direction only |
|
56 |
#' * `B`: both low and high directions |
|
57 |
#' |
|
58 |
#' @return `h_adlb_worsen()` returns the `adlb` `data.frame` containing only the |
|
59 |
#' worst labs specified according to `worst_flag_low` or `worst_flag_high` for the |
|
60 |
#' direction specified according to `direction_var`. For instance, for a lab that is |
|
61 |
#' needed for the low direction only, only records flagged by `worst_flag_low` are |
|
62 |
#' selected. For a lab that is needed for both low and high directions, the worst |
|
63 |
#' low records are selected for the low direction, and the worst high record are selected |
|
64 |
#' for the high direction. |
|
65 |
#' |
|
66 |
#' @seealso [abnormal_by_worst_grade_worsen] |
|
67 |
#' |
|
68 |
#' @examples |
|
69 |
#' library(dplyr) |
|
70 |
#' |
|
71 |
#' # The direction variable, GRADDR, is based on metadata |
|
72 |
#' adlb <- tern_ex_adlb %>% |
|
73 |
#' mutate( |
|
74 |
#' GRADDR = case_when( |
|
75 |
#' PARAMCD == "ALT" ~ "B", |
|
76 |
#' PARAMCD == "CRP" ~ "L", |
|
77 |
#' PARAMCD == "IGA" ~ "H" |
|
78 |
#' ) |
|
79 |
#' ) %>% |
|
80 |
#' filter(SAFFL == "Y" & ONTRTFL == "Y" & GRADDR != "") |
|
81 |
#' |
|
82 |
#' df <- h_adlb_worsen( |
|
83 |
#' adlb, |
|
84 |
#' worst_flag_low = c("WGRLOFL" = "Y"), |
|
85 |
#' worst_flag_high = c("WGRHIFL" = "Y"), |
|
86 |
#' direction_var = "GRADDR" |
|
87 |
#' ) |
|
88 |
#' |
|
89 |
#' @export |
|
90 |
h_adlb_worsen <- function(adlb, |
|
91 |
worst_flag_low = NULL, |
|
92 |
worst_flag_high = NULL, |
|
93 |
direction_var) { |
|
94 | 5x |
checkmate::assert_string(direction_var) |
95 | 5x |
checkmate::assert_subset(as.character(unique(adlb[[direction_var]])), c("B", "L", "H")) |
96 | 5x |
assert_df_with_variables(adlb, list("Col" = direction_var)) |
97 | ||
98 | 5x |
if (any(unique(adlb[[direction_var]]) == "H")) { |
99 | 4x |
assert_df_with_variables(adlb, list("High" = names(worst_flag_high))) |
100 |
} |
|
101 | ||
102 | 5x |
if (any(unique(adlb[[direction_var]]) == "L")) { |
103 | 4x |
assert_df_with_variables(adlb, list("Low" = names(worst_flag_low))) |
104 |
} |
|
105 | ||
106 | 5x |
if (any(unique(adlb[[direction_var]]) == "B")) { |
107 | 3x |
assert_df_with_variables( |
108 | 3x |
adlb, |
109 | 3x |
list( |
110 | 3x |
"Low" = names(worst_flag_low), |
111 | 3x |
"High" = names(worst_flag_high) |
112 |
) |
|
113 |
) |
|
114 |
} |
|
115 | ||
116 |
# extract patients with worst post-baseline lab, either low or high or both |
|
117 | 5x |
worst_flag <- c(worst_flag_low, worst_flag_high) |
118 | 5x |
col_names <- names(worst_flag) |
119 | 5x |
filter_values <- worst_flag |
120 | 5x |
temp <- Map( |
121 | 5x |
function(x, y) which(adlb[[x]] == y), |
122 | 5x |
col_names, |
123 | 5x |
filter_values |
124 |
) |
|
125 | 5x |
position_satisfy_filters <- Reduce(union, temp) |
126 | ||
127 |
# select variables of interest |
|
128 | 5x |
adlb_f <- adlb[position_satisfy_filters, ] |
129 | ||
130 |
# generate subsets for different directionality |
|
131 | 5x |
adlb_f_h <- adlb_f[which(adlb_f[[direction_var]] == "H"), ] |
132 | 5x |
adlb_f_l <- adlb_f[which(adlb_f[[direction_var]] == "L"), ] |
133 | 5x |
adlb_f_b <- adlb_f[which(adlb_f[[direction_var]] == "B"), ] |
134 | ||
135 |
# for labs requiring both high and low, data is duplicated and will be stacked on top of each other |
|
136 | 5x |
adlb_f_b_h <- adlb_f_b |
137 | 5x |
adlb_f_b_l <- adlb_f_b |
138 | ||
139 |
# extract data with worst lab |
|
140 | 5x |
if (!is.null(worst_flag_high) && !is.null(worst_flag_low)) { |
141 |
# change H to High, L to Low |
|
142 | 3x |
adlb_f_h[[direction_var]] <- rep("High", nrow(adlb_f_h)) |
143 | 3x |
adlb_f_l[[direction_var]] <- rep("Low", nrow(adlb_f_l)) |
144 | ||
145 |
# change, B to High and Low |
|
146 | 3x |
adlb_f_b_h[[direction_var]] <- rep("High", nrow(adlb_f_b_h)) |
147 | 3x |
adlb_f_b_l[[direction_var]] <- rep("Low", nrow(adlb_f_b_l)) |
148 | ||
149 | 3x |
adlb_out_h <- adlb_f_h[which(adlb_f_h[[names(worst_flag_high)]] == worst_flag_high), ] |
150 | 3x |
adlb_out_b_h <- adlb_f_b_h[which(adlb_f_b_h[[names(worst_flag_high)]] == worst_flag_high), ] |
151 | 3x |
adlb_out_l <- adlb_f_l[which(adlb_f_l[[names(worst_flag_low)]] == worst_flag_low), ] |
152 | 3x |
adlb_out_b_l <- adlb_f_b_l[which(adlb_f_b_l[[names(worst_flag_low)]] == worst_flag_low), ] |
153 | ||
154 | 3x |
out <- rbind(adlb_out_h, adlb_out_b_h, adlb_out_l, adlb_out_b_l) |
155 | 2x |
} else if (!is.null(worst_flag_high)) { |
156 | 1x |
adlb_f_h[[direction_var]] <- rep("High", nrow(adlb_f_h)) |
157 | 1x |
adlb_f_b_h[[direction_var]] <- rep("High", nrow(adlb_f_b_h)) |
158 | ||
159 | 1x |
adlb_out_h <- adlb_f_h[which(adlb_f_h[[names(worst_flag_high)]] == worst_flag_high), ] |
160 | 1x |
adlb_out_b_h <- adlb_f_b_h[which(adlb_f_b_h[[names(worst_flag_high)]] == worst_flag_high), ] |
161 | ||
162 | 1x |
out <- rbind(adlb_out_h, adlb_out_b_h) |
163 | 1x |
} else if (!is.null(worst_flag_low)) { |
164 | 1x |
adlb_f_l[[direction_var]] <- rep("Low", nrow(adlb_f_l)) |
165 | 1x |
adlb_f_b_l[[direction_var]] <- rep("Low", nrow(adlb_f_b_l)) |
166 | ||
167 | 1x |
adlb_out_l <- adlb_f_l[which(adlb_f_l[[names(worst_flag_low)]] == worst_flag_low), ] |
168 | 1x |
adlb_out_b_l <- adlb_f_b_l[which(adlb_f_b_l[[names(worst_flag_low)]] == worst_flag_low), ] |
169 | ||
170 | 1x |
out <- rbind(adlb_out_l, adlb_out_b_l) |
171 |
} |
|
172 | ||
173 |
# label |
|
174 | 5x |
formatters::var_labels(out) <- formatters::var_labels(adlb_f, fill = FALSE) |
175 |
# NA |
|
176 | 5x |
out |
177 |
} |
|
178 | ||
179 |
#' Helper function to analyze patients for `s_count_abnormal_lab_worsen_by_baseline()` |
|
180 |
#' |
|
181 |
#' @description `r lifecycle::badge("stable")` |
|
182 |
#' |
|
183 |
#' Helper function to count the number of patients and the fraction of patients according to |
|
184 |
#' highest post-baseline lab grade variable `.var`, baseline lab grade variable `baseline_var`, |
|
185 |
#' and the direction of interest specified in `direction_var`. |
|
186 |
#' |
|
187 |
#' @inheritParams argument_convention |
|
188 |
#' @inheritParams h_adlb_worsen |
|
189 |
#' @param baseline_var (`string`)\cr name of the baseline lab grade variable. |
|
190 |
#' |
|
191 |
#' @return The counts and fraction of patients |
|
192 |
#' whose worst post-baseline lab grades are worse than their baseline grades, for |
|
193 |
#' post-baseline worst grades "1", "2", "3", "4" and "Any". |
|
194 |
#' |
|
195 |
#' @seealso [abnormal_by_worst_grade_worsen] |
|
196 |
#' |
|
197 |
#' @examples |
|
198 |
#' library(dplyr) |
|
199 |
#' |
|
200 |
#' # The direction variable, GRADDR, is based on metadata |
|
201 |
#' adlb <- tern_ex_adlb %>% |
|
202 |
#' mutate( |
|
203 |
#' GRADDR = case_when( |
|
204 |
#' PARAMCD == "ALT" ~ "B", |
|
205 |
#' PARAMCD == "CRP" ~ "L", |
|
206 |
#' PARAMCD == "IGA" ~ "H" |
|
207 |
#' ) |
|
208 |
#' ) %>% |
|
209 |
#' filter(SAFFL == "Y" & ONTRTFL == "Y" & GRADDR != "") |
|
210 |
#' |
|
211 |
#' df <- h_adlb_worsen( |
|
212 |
#' adlb, |
|
213 |
#' worst_flag_low = c("WGRLOFL" = "Y"), |
|
214 |
#' worst_flag_high = c("WGRHIFL" = "Y"), |
|
215 |
#' direction_var = "GRADDR" |
|
216 |
#' ) |
|
217 |
#' |
|
218 |
#' # `h_worsen_counter` |
|
219 |
#' h_worsen_counter( |
|
220 |
#' df %>% filter(PARAMCD == "CRP" & GRADDR == "Low"), |
|
221 |
#' id = "USUBJID", |
|
222 |
#' .var = "ATOXGR", |
|
223 |
#' baseline_var = "BTOXGR", |
|
224 |
#' direction_var = "GRADDR" |
|
225 |
#' ) |
|
226 |
#' |
|
227 |
#' @export |
|
228 |
h_worsen_counter <- function(df, id, .var, baseline_var, direction_var) { |
|
229 | 17x |
checkmate::assert_string(id) |
230 | 17x |
checkmate::assert_string(.var) |
231 | 17x |
checkmate::assert_string(baseline_var) |
232 | 17x |
checkmate::assert_scalar(unique(df[[direction_var]])) |
233 | 17x |
checkmate::assert_subset(unique(df[[direction_var]]), c("High", "Low")) |
234 | 17x |
assert_df_with_variables(df, list(val = c(id, .var, baseline_var, direction_var))) |
235 | ||
236 |
# remove post-baseline missing |
|
237 | 17x |
df <- df[df[[.var]] != "<Missing>", ] |
238 | ||
239 |
# obtain directionality |
|
240 | 17x |
direction <- unique(df[[direction_var]]) |
241 | ||
242 | 17x |
if (direction == "Low") { |
243 | 10x |
grade <- -1:-4 |
244 | 10x |
worst_grade <- -4 |
245 | 7x |
} else if (direction == "High") { |
246 | 7x |
grade <- 1:4 |
247 | 7x |
worst_grade <- 4 |
248 |
} |
|
249 | ||
250 | 17x |
if (nrow(df) > 0) { |
251 | 17x |
by_grade <- lapply(grade, function(i) { |
252 |
# filter baseline values that is less than i or <Missing> |
|
253 | 68x |
df_temp <- df[df[[baseline_var]] %in% c((i + sign(i) * -1):(-1 * worst_grade), "<Missing>"), ] |
254 |
# num: number of patients with post-baseline worst lab equal to i |
|
255 | 68x |
num <- length(unique(df_temp[df_temp[[.var]] %in% i, id, drop = TRUE])) |
256 |
# denom: number of patients with baseline values less than i or <missing> and post-baseline in the same direction |
|
257 | 68x |
denom <- length(unique(df_temp[[id]])) |
258 | 68x |
rm(df_temp) |
259 | 68x |
c(num = num, denom = denom) |
260 |
}) |
|
261 |
} else { |
|
262 | ! |
by_grade <- lapply(1, function(i) { |
263 | ! |
c(num = 0, denom = 0) |
264 |
}) |
|
265 |
} |
|
266 | ||
267 | 17x |
names(by_grade) <- as.character(seq_along(by_grade)) |
268 | ||
269 |
# baseline grade less 4 or missing |
|
270 | 17x |
df_temp <- df[!df[[baseline_var]] %in% worst_grade, ] |
271 | ||
272 |
# denom: number of patients with baseline values less than 4 or <missing> and post-baseline in the same direction |
|
273 | 17x |
denom <- length(unique(df_temp[, id, drop = TRUE])) |
274 | ||
275 |
# condition 1: missing baseline and in the direction of abnormality |
|
276 | 17x |
con1 <- which(df_temp[[baseline_var]] == "<Missing>" & df_temp[[.var]] %in% grade) |
277 | 17x |
df_temp_nm <- df_temp[which(df_temp[[baseline_var]] != "<Missing>" & df_temp[[.var]] %in% grade), ] |
278 | ||
279 |
# condition 2: if post-baseline values are present then post-baseline values must be worse than baseline |
|
280 | 17x |
if (direction == "Low") { |
281 | 10x |
con2 <- which(as.numeric(as.character(df_temp_nm[[.var]])) < as.numeric(as.character(df_temp_nm[[baseline_var]]))) |
282 |
} else { |
|
283 | 7x |
con2 <- which(as.numeric(as.character(df_temp_nm[[.var]])) > as.numeric(as.character(df_temp_nm[[baseline_var]]))) |
284 |
} |
|
285 | ||
286 |
# number of patients satisfy either conditions 1 or 2 |
|
287 | 17x |
num <- length(unique(df_temp[union(con1, con2), id, drop = TRUE])) |
288 | ||
289 | 17x |
list(fraction = c(by_grade, list("Any" = c(num = num, denom = denom)))) |
290 |
} |
|
291 | ||
292 |
#' @describeIn abnormal_by_worst_grade_worsen Statistics function for patients whose worst post-baseline |
|
293 |
#' lab grades are worse than their baseline grades. |
|
294 |
#' |
|
295 |
#' @return |
|
296 |
#' * `s_count_abnormal_lab_worsen_by_baseline()` returns the counts and fraction of patients whose worst |
|
297 |
#' post-baseline lab grades are worse than their baseline grades, for post-baseline worst grades |
|
298 |
#' "1", "2", "3", "4" and "Any". |
|
299 |
#' |
|
300 |
#' @keywords internal |
|
301 |
s_count_abnormal_lab_worsen_by_baseline <- function(df, # nolint |
|
302 |
.var = "ATOXGR", |
|
303 |
variables = list( |
|
304 |
id = "USUBJID", |
|
305 |
baseline_var = "BTOXGR", |
|
306 |
direction_var = "GRADDR" |
|
307 |
)) { |
|
308 | 1x |
checkmate::assert_string(.var) |
309 | 1x |
checkmate::assert_set_equal(names(variables), c("id", "baseline_var", "direction_var")) |
310 | 1x |
checkmate::assert_string(variables$id) |
311 | 1x |
checkmate::assert_string(variables$baseline_var) |
312 | 1x |
checkmate::assert_string(variables$direction_var) |
313 | 1x |
assert_df_with_variables(df, c(aval = .var, variables[1:3])) |
314 | 1x |
assert_list_of_variables(variables) |
315 | ||
316 | 1x |
h_worsen_counter(df, variables$id, .var, variables$baseline_var, variables$direction_var) |
317 |
} |
|
318 | ||
319 |
#' @describeIn abnormal_by_worst_grade_worsen Formatted analysis function which is used as `afun` |
|
320 |
#' in `count_abnormal_lab_worsen_by_baseline()`. |
|
321 |
#' |
|
322 |
#' @return |
|
323 |
#' * `a_count_abnormal_lab_worsen_by_baseline()` returns the corresponding list with |
|
324 |
#' formatted [rtables::CellValue()]. |
|
325 |
#' |
|
326 |
#' @keywords internal |
|
327 |
a_count_abnormal_lab_worsen_by_baseline <- make_afun( # nolint |
|
328 |
s_count_abnormal_lab_worsen_by_baseline, |
|
329 |
.formats = c(fraction = format_fraction), |
|
330 |
.ungroup_stats = "fraction" |
|
331 |
) |
|
332 | ||
333 |
#' @describeIn abnormal_by_worst_grade_worsen Layout-creating function which can take statistics function |
|
334 |
#' arguments and additional format arguments. This function is a wrapper for [rtables::analyze()]. |
|
335 |
#' |
|
336 |
#' @return |
|
337 |
#' * `count_abnormal_lab_worsen_by_baseline()` returns a layout object suitable for passing to further layouting |
|
338 |
#' functions, or to [rtables::build_table()]. Adding this function to an `rtable` layout will add formatted |
|
339 |
#' rows containing the statistics from `s_count_abnormal_lab_worsen_by_baseline()` to the table layout. |
|
340 |
#' |
|
341 |
#' @examples |
|
342 |
#' library(dplyr) |
|
343 |
#' |
|
344 |
#' # The direction variable, GRADDR, is based on metadata |
|
345 |
#' adlb <- tern_ex_adlb %>% |
|
346 |
#' mutate( |
|
347 |
#' GRADDR = case_when( |
|
348 |
#' PARAMCD == "ALT" ~ "B", |
|
349 |
#' PARAMCD == "CRP" ~ "L", |
|
350 |
#' PARAMCD == "IGA" ~ "H" |
|
351 |
#' ) |
|
352 |
#' ) %>% |
|
353 |
#' filter(SAFFL == "Y" & ONTRTFL == "Y" & GRADDR != "") |
|
354 |
#' |
|
355 |
#' df <- h_adlb_worsen( |
|
356 |
#' adlb, |
|
357 |
#' worst_flag_low = c("WGRLOFL" = "Y"), |
|
358 |
#' worst_flag_high = c("WGRHIFL" = "Y"), |
|
359 |
#' direction_var = "GRADDR" |
|
360 |
#' ) |
|
361 |
#' |
|
362 |
#' basic_table() %>% |
|
363 |
#' split_cols_by("ARMCD") %>% |
|
364 |
#' add_colcounts() %>% |
|
365 |
#' split_rows_by("PARAMCD") %>% |
|
366 |
#' split_rows_by("GRADDR") %>% |
|
367 |
#' count_abnormal_lab_worsen_by_baseline( |
|
368 |
#' var = "ATOXGR", |
|
369 |
#' variables = list( |
|
370 |
#' id = "USUBJID", |
|
371 |
#' baseline_var = "BTOXGR", |
|
372 |
#' direction_var = "GRADDR" |
|
373 |
#' ) |
|
374 |
#' ) %>% |
|
375 |
#' append_topleft("Direction of Abnormality") %>% |
|
376 |
#' build_table(df = df, alt_counts_df = tern_ex_adsl) |
|
377 |
#' |
|
378 |
#' @export |
|
379 |
#' @order 2 |
|
380 |
count_abnormal_lab_worsen_by_baseline <- function(lyt, # nolint |
|
381 |
var, |
|
382 |
variables = list( |
|
383 |
id = "USUBJID", |
|
384 |
baseline_var = "BTOXGR", |
|
385 |
direction_var = "GRADDR" |
|
386 |
), |
|
387 |
na_str = default_na_str(), |
|
388 |
nested = TRUE, |
|
389 |
..., |
|
390 |
table_names = NULL, |
|
391 |
.stats = NULL, |
|
392 |
.formats = NULL, |
|
393 |
.labels = NULL, |
|
394 |
.indent_mods = NULL) { |
|
395 | 1x |
checkmate::assert_string(var) |
396 | ||
397 | 1x |
extra_args <- list(variables = variables, ...) |
398 | ||
399 | 1x |
afun <- make_afun( |
400 | 1x |
a_count_abnormal_lab_worsen_by_baseline, |
401 | 1x |
.stats = .stats, |
402 | 1x |
.formats = .formats, |
403 | 1x |
.labels = .labels, |
404 | 1x |
.indent_mods = .indent_mods |
405 |
) |
|
406 | ||
407 | 1x |
lyt <- analyze( |
408 | 1x |
lyt = lyt, |
409 | 1x |
vars = var, |
410 | 1x |
afun = afun, |
411 | 1x |
na_str = na_str, |
412 | 1x |
nested = nested, |
413 | 1x |
extra_args = extra_args, |
414 | 1x |
show_labels = "hidden" |
415 |
) |
|
416 | ||
417 | 1x |
lyt |
418 |
} |
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 |
#' @name desctools_binom |
|
8 |
NULL |
|
9 | ||
10 |
#' Recycle list of parameters |
|
11 |
#' |
|
12 |
#' This function recycles all supplied elements to the maximal dimension. |
|
13 |
#' |
|
14 |
#' @param ... (`any`)\cr elements to recycle. |
|
15 |
#' |
|
16 |
#' @return A `list`. |
|
17 |
#' |
|
18 |
#' @keywords internal |
|
19 |
#' @noRd |
|
20 |
h_recycle <- function(...) { |
|
21 | 64x |
lst <- list(...) |
22 | 64x |
maxdim <- max(lengths(lst)) |
23 | 64x |
res <- lapply(lst, rep, length.out = maxdim) |
24 | 64x |
attr(res, "maxdim") <- maxdim |
25 | 64x |
return(res) |
26 |
} |
|
27 | ||
28 |
#' @describeIn desctools_binom Several confidence intervals for the difference between proportions. |
|
29 |
#' |
|
30 |
#' @return A `matrix` of 3 values: |
|
31 |
#' * `est`: estimate of proportion difference. |
|
32 |
#' * `lwr.ci`: estimate of lower end of the confidence interval. |
|
33 |
#' * `upr.ci`: estimate of upper end of the confidence interval. |
|
34 |
#' |
|
35 |
#' @keywords internal |
|
36 |
desctools_binom <- function(x1, |
|
37 |
n1, |
|
38 |
x2, |
|
39 |
n2, |
|
40 |
conf.level = 0.95, # nolint |
|
41 |
sides = c("two.sided", "left", "right"), |
|
42 |
method = c( |
|
43 |
"ac", "wald", "waldcc", "score", "scorecc", "mn", "mee", "blj", "ha", "hal", "jp" |
|
44 |
)) { |
|
45 | 20x |
if (missing(sides)) { |
46 | 20x |
sides <- match.arg(sides) |
47 |
} |
|
48 | 20x |
if (missing(method)) { |
49 | 1x |
method <- match.arg(method) |
50 |
} |
|
51 | 20x |
iBinomDiffCI <- function(x1, n1, x2, n2, conf.level, sides, method) { # nolint |
52 | 20x |
if (sides != "two.sided") { |
53 | ! |
conf.level <- 1 - 2 * (1 - conf.level) # nolint |
54 |
} |
|
55 | 20x |
alpha <- 1 - conf.level |
56 | 20x |
kappa <- stats::qnorm(1 - alpha / 2) |
57 | 20x |
p1_hat <- x1 / n1 |
58 | 20x |
p2_hat <- x2 / n2 |
59 | 20x |
est <- p1_hat - p2_hat |
60 | 20x |
switch(method, |
61 | 20x |
wald = { |
62 | 2x |
vd <- p1_hat * (1 - p1_hat) / n1 + p2_hat * (1 - p2_hat) / n2 |
63 | 2x |
term2 <- kappa * sqrt(vd) |
64 | 2x |
ci_lwr <- max(-1, est - term2) |
65 | 2x |
ci_upr <- min(1, est + term2) |
66 |
}, |
|
67 | 20x |
waldcc = { |
68 | 4x |
vd <- p1_hat * (1 - p1_hat) / n1 + p2_hat * (1 - p2_hat) / n2 |
69 | 4x |
term2 <- kappa * sqrt(vd) |
70 | 4x |
term2 <- term2 + 0.5 * (1 / n1 + 1 / n2) |
71 | 4x |
ci_lwr <- max(-1, est - term2) |
72 | 4x |
ci_upr <- min(1, est + term2) |
73 |
}, |
|
74 | 20x |
ac = { |
75 | 2x |
n1 <- n1 + 2 |
76 | 2x |
n2 <- n2 + 2 |
77 | 2x |
x1 <- x1 + 1 |
78 | 2x |
x2 <- x2 + 1 |
79 | 2x |
p1_hat <- x1 / n1 |
80 | 2x |
p2_hat <- x2 / n2 |
81 | 2x |
est1 <- p1_hat - p2_hat |
82 | 2x |
vd <- p1_hat * (1 - p1_hat) / n1 + p2_hat * (1 - p2_hat) / n2 |
83 | 2x |
term2 <- kappa * sqrt(vd) |
84 | 2x |
ci_lwr <- max(-1, est1 - term2) |
85 | 2x |
ci_upr <- min(1, est1 + term2) |
86 |
}, |
|
87 | 20x |
exact = { |
88 | ! |
ci_lwr <- NA |
89 | ! |
ci_upr <- NA |
90 |
}, |
|
91 | 20x |
score = { |
92 | 2x |
w1 <- desctools_binomci( |
93 | 2x |
x = x1, n = n1, conf.level = conf.level, |
94 | 2x |
method = "wilson" |
95 |
) |
|
96 | 2x |
w2 <- desctools_binomci( |
97 | 2x |
x = x2, n = n2, conf.level = conf.level, |
98 | 2x |
method = "wilson" |
99 |
) |
|
100 | 2x |
l1 <- w1[2] |
101 | 2x |
u1 <- w1[3] |
102 | 2x |
l2 <- w2[2] |
103 | 2x |
u2 <- w2[3] |
104 | 2x |
ci_lwr <- est - kappa * sqrt(l1 * (1 - l1) / n1 + u2 * (1 - u2) / n2) |
105 | 2x |
ci_upr <- est + kappa * sqrt(u1 * (1 - u1) / n1 + l2 * (1 - l2) / n2) |
106 |
}, |
|
107 | 20x |
scorecc = { |
108 | 1x |
w1 <- desctools_binomci( |
109 | 1x |
x = x1, n = n1, conf.level = conf.level, |
110 | 1x |
method = "wilsoncc" |
111 |
) |
|
112 | 1x |
w2 <- desctools_binomci( |
113 | 1x |
x = x2, n = n2, conf.level = conf.level, |
114 | 1x |
method = "wilsoncc" |
115 |
) |
|
116 | 1x |
l1 <- w1[2] |
117 | 1x |
u1 <- w1[3] |
118 | 1x |
l2 <- w2[2] |
119 | 1x |
u2 <- w2[3] |
120 | 1x |
ci_lwr <- max(-1, est - sqrt((p1_hat - l1)^2 + (u2 - p2_hat)^2)) |
121 | 1x |
ci_upr <- min(1, est + sqrt((u1 - p1_hat)^2 + (p2_hat - l2)^2)) |
122 |
}, |
|
123 | 20x |
mee = { |
124 | 1x |
.score <- function(p1, n1, p2, n2, dif) { |
125 | ! |
if (dif > 1) dif <- 1 |
126 | ! |
if (dif < -1) dif <- -1 |
127 | 24x |
diff <- p1 - p2 - dif |
128 | 24x |
if (abs(diff) == 0) { |
129 | ! |
res <- 0 |
130 |
} else { |
|
131 | 24x |
t <- n2 / n1 |
132 | 24x |
a <- 1 + t |
133 | 24x |
b <- -(1 + t + p1 + t * p2 + dif * (t + 2)) |
134 | 24x |
c <- dif * dif + dif * (2 * p1 + t + 1) + p1 + t * p2 |
135 | 24x |
d <- -p1 * dif * (1 + dif) |
136 | 24x |
v <- (b / a / 3)^3 - b * c / (6 * a * a) + d / a / 2 |
137 | 24x |
if (abs(v) < .Machine$double.eps) v <- 0 |
138 | 24x |
s <- sqrt((b / a / 3)^2 - c / a / 3) |
139 | 24x |
u <- ifelse(v > 0, 1, -1) * s |
140 | 24x |
w <- (3.141592654 + acos(v / u^3)) / 3 |
141 | 24x |
p1d <- 2 * u * cos(w) - b / a / 3 |
142 | 24x |
p2d <- p1d - dif |
143 | 24x |
n <- n1 + n2 |
144 | 24x |
res <- (p1d * (1 - p1d) / n1 + p2d * (1 - p2d) / n2) |
145 |
} |
|
146 | 24x |
return(sqrt(res)) |
147 |
} |
|
148 | 1x |
pval <- function(delta) { |
149 | 24x |
z <- (est - delta) / .score(p1_hat, n1, p2_hat, n2, delta) |
150 | 24x |
2 * min(stats::pnorm(z), 1 - stats::pnorm(z)) |
151 |
} |
|
152 | 1x |
ci_lwr <- max(-1, stats::uniroot(function(delta) { |
153 | 12x |
pval(delta) - alpha |
154 | 1x |
}, interval = c(-1 + 1e-06, est - 1e-06))$root) |
155 | 1x |
ci_upr <- min(1, stats::uniroot(function(delta) { |
156 | 12x |
pval(delta) - alpha |
157 | 1x |
}, interval = c(est + 1e-06, 1 - 1e-06))$root) |
158 |
}, |
|
159 | 20x |
blj = { |
160 | 1x |
p1_dash <- (x1 + 0.5) / (n1 + 1) |
161 | 1x |
p2_dash <- (x2 + 0.5) / (n2 + 1) |
162 | 1x |
vd <- p1_dash * (1 - p1_dash) / n1 + p2_dash * (1 - p2_dash) / n2 |
163 | 1x |
term2 <- kappa * sqrt(vd) |
164 | 1x |
est_dash <- p1_dash - p2_dash |
165 | 1x |
ci_lwr <- max(-1, est_dash - term2) |
166 | 1x |
ci_upr <- min(1, est_dash + term2) |
167 |
}, |
|
168 | 20x |
ha = { |
169 | 4x |
term2 <- 1 / |
170 | 4x |
(2 * min(n1, n2)) + kappa * sqrt(p1_hat * (1 - p1_hat) / (n1 - 1) + p2_hat * (1 - p2_hat) / (n2 - 1)) |
171 | 4x |
ci_lwr <- max(-1, est - term2) |
172 | 4x |
ci_upr <- min(1, est + term2) |
173 |
}, |
|
174 | 20x |
mn = { |
175 | 1x |
.conf <- function(x1, n1, x2, n2, z, lower = FALSE) { |
176 | 2x |
p1 <- x1 / n1 |
177 | 2x |
p2 <- x2 / n2 |
178 | 2x |
p_hat <- p1 - p2 |
179 | 2x |
dp <- 1 + ifelse(lower, 1, -1) * p_hat |
180 | 2x |
i <- 1 |
181 | 2x |
while (i <= 50) { |
182 | 46x |
dp <- 0.5 * dp |
183 | 46x |
y <- p_hat + ifelse(lower, -1, 1) * dp |
184 | 46x |
score <- .score(p1, n1, p2, n2, y) |
185 | 46x |
if (score < z) { |
186 | 20x |
p_hat <- y |
187 |
} |
|
188 | 46x |
if ((dp < 1e-07) || (abs(z - score) < 1e-06)) { |
189 | 2x |
(break)() |
190 |
} else { |
|
191 | 44x |
i <- i + 1 |
192 |
} |
|
193 |
} |
|
194 | 2x |
return(y) |
195 |
} |
|
196 | 1x |
.score <- function(p1, n1, p2, n2, dif) { |
197 | 46x |
diff <- p1 - p2 - dif |
198 | 46x |
if (abs(diff) == 0) { |
199 | ! |
res <- 0 |
200 |
} else { |
|
201 | 46x |
t <- n2 / n1 |
202 | 46x |
a <- 1 + t |
203 | 46x |
b <- -(1 + t + p1 + t * p2 + dif * (t + 2)) |
204 | 46x |
c <- dif * dif + dif * (2 * p1 + t + 1) + p1 + t * p2 |
205 | 46x |
d <- -p1 * dif * (1 + dif) |
206 | 46x |
v <- (b / a / 3)^3 - b * c / (6 * a * a) + d / a / 2 |
207 | 46x |
s <- sqrt((b / a / 3)^2 - c / a / 3) |
208 | 46x |
u <- ifelse(v > 0, 1, -1) * s |
209 | 46x |
w <- (3.141592654 + acos(v / u^3)) / 3 |
210 | 46x |
p1d <- 2 * u * cos(w) - b / a / 3 |
211 | 46x |
p2d <- p1d - dif |
212 | 46x |
n <- n1 + n2 |
213 | 46x |
var <- (p1d * (1 - p1d) / n1 + p2d * (1 - p2d) / n2) * n / (n - 1) |
214 | 46x |
res <- diff^2 / var |
215 |
} |
|
216 | 46x |
return(res) |
217 |
} |
|
218 | 1x |
z <- stats::qchisq(conf.level, 1) |
219 | 1x |
ci_lwr <- max(-1, .conf(x1, n1, x2, n2, z, TRUE)) |
220 | 1x |
ci_upr <- min(1, .conf(x1, n1, x2, n2, z, FALSE)) |
221 |
}, |
|
222 | 20x |
beal = { |
223 | ! |
a <- p1_hat + p2_hat |
224 | ! |
b <- p1_hat - p2_hat |
225 | ! |
u <- ((1 / n1) + (1 / n2)) / 4 |
226 | ! |
v <- ((1 / n1) - (1 / n2)) / 4 |
227 | ! |
V <- u * ((2 - a) * a - b^2) + 2 * v * (1 - a) * b # nolint |
228 | ! |
z <- stats::qchisq(p = 1 - alpha / 2, df = 1) |
229 | ! |
A <- sqrt(z * (V + z * u^2 * (2 - a) * a + z * v^2 * (1 - a)^2)) # nolint |
230 | ! |
B <- (b + z * v * (1 - a)) / (1 + z * u) # nolint |
231 | ! |
ci_lwr <- max(-1, B - A / (1 + z * u)) |
232 | ! |
ci_upr <- min(1, B + A / (1 + z * u)) |
233 |
}, |
|
234 | 20x |
hal = { |
235 | 1x |
psi <- (p1_hat + p2_hat) / 2 |
236 | 1x |
u <- (1 / n1 + 1 / n2) / 4 |
237 | 1x |
v <- (1 / n1 - 1 / n2) / 4 |
238 | 1x |
z <- kappa |
239 | 1x |
theta <- ((p1_hat - p2_hat) + z^2 * v * (1 - 2 * psi)) / (1 + z^2 * u) |
240 | 1x |
w <- z / (1 + z^2 * u) * sqrt(u * (4 * psi * (1 - psi) - (p1_hat - p2_hat)^2) + 2 * v * (1 - 2 * psi) * |
241 | 1x |
(p1_hat - p2_hat) + 4 * z^2 * u^2 * (1 - psi) * psi + z^2 * v^2 * (1 - 2 * psi)^2) # nolint |
242 | 1x |
c(theta + w, theta - w) |
243 | 1x |
ci_lwr <- max(-1, theta - w) |
244 | 1x |
ci_upr <- min(1, theta + w) |
245 |
}, |
|
246 | 20x |
jp = { |
247 | 1x |
psi <- 0.5 * ((x1 + 0.5) / (n1 + 1) + (x2 + 0.5) / (n2 + 1)) |
248 | 1x |
u <- (1 / n1 + 1 / n2) / 4 |
249 | 1x |
v <- (1 / n1 - 1 / n2) / 4 |
250 | 1x |
z <- kappa |
251 | 1x |
theta <- ((p1_hat - p2_hat) + z^2 * v * (1 - 2 * psi)) / (1 + z^2 * u) |
252 | 1x |
w <- z / (1 + z^2 * u) * sqrt(u * (4 * psi * (1 - psi) - (p1_hat - p2_hat)^2) + 2 * v * (1 - 2 * psi) * |
253 | 1x |
(p1_hat - p2_hat) + 4 * z^2 * u^2 * (1 - psi) * psi + z^2 * v^2 * (1 - 2 * psi)^2) # nolint |
254 | 1x |
c(theta + w, theta - w) |
255 | 1x |
ci_lwr <- max(-1, theta - w) |
256 | 1x |
ci_upr <- min(1, theta + w) |
257 |
}, |
|
258 |
) |
|
259 | 20x |
ci <- c( |
260 | 20x |
est = est, lwr.ci = min(ci_lwr, ci_upr), |
261 | 20x |
upr.ci = max(ci_lwr, ci_upr) |
262 |
) |
|
263 | 20x |
if (sides == "left") { |
264 | ! |
ci[3] <- 1 |
265 | 20x |
} else if (sides == "right") { |
266 | ! |
ci[2] <- -1 |
267 |
} |
|
268 | 20x |
return(ci) |
269 |
} |
|
270 | 20x |
method <- match.arg(arg = method, several.ok = TRUE) |
271 | 20x |
sides <- match.arg(arg = sides, several.ok = TRUE) |
272 | 20x |
lst <- h_recycle( |
273 | 20x |
x1 = x1, n1 = n1, x2 = x2, n2 = n2, conf.level = conf.level, |
274 | 20x |
sides = sides, method = method |
275 |
) |
|
276 | 20x |
res <- t(sapply(1:attr(lst, "maxdim"), function(i) { |
277 | 20x |
iBinomDiffCI( |
278 | 20x |
x1 = lst$x1[i], |
279 | 20x |
n1 = lst$n1[i], x2 = lst$x2[i], n2 = lst$n2[i], conf.level = lst$conf.level[i], |
280 | 20x |
sides = lst$sides[i], method = lst$method[i] |
281 |
) |
|
282 |
})) |
|
283 | 20x |
lgn <- h_recycle(x1 = if (is.null(names(x1))) { |
284 | 20x |
paste("x1", seq_along(x1), sep = ".") |
285 |
} else { |
|
286 | ! |
names(x1) |
287 | 20x |
}, n1 = if (is.null(names(n1))) { |
288 | 20x |
paste("n1", seq_along(n1), sep = ".") |
289 |
} else { |
|
290 | ! |
names(n1) |
291 | 20x |
}, x2 = if (is.null(names(x2))) { |
292 | 20x |
paste("x2", seq_along(x2), sep = ".") |
293 |
} else { |
|
294 | ! |
names(x2) |
295 | 20x |
}, n2 = if (is.null(names(n2))) { |
296 | 20x |
paste("n2", seq_along(n2), sep = ".") |
297 |
} else { |
|
298 | ! |
names(n2) |
299 | 20x |
}, conf.level = conf.level, sides = sides, method = method) |
300 | 20x |
xn <- apply(as.data.frame(lgn[sapply(lgn, function(x) { |
301 | 140x |
length(unique(x)) != |
302 | 140x |
1 |
303 | 20x |
})]), 1, paste, collapse = ":") |
304 | 20x |
rownames(res) <- xn |
305 | 20x |
return(res) |
306 |
} |
|
307 | ||
308 |
#' @describeIn desctools_binom Compute confidence intervals for binomial proportions. |
|
309 |
#' |
|
310 |
#' @param x (`integer(1)`)\cr number of successes. |
|
311 |
#' @param n (`integer(1)`)\cr number of trials. |
|
312 |
#' @param conf.level (`proportion`)\cr confidence level, defaults to 0.95. |
|
313 |
#' @param sides (`string`)\cr side of the confidence interval to compute. Must be one of `"two-sided"` (default), |
|
314 |
#' `"left"`, or `"right"`. |
|
315 |
#' @param method (`string`)\cr method to use. Can be one out of: `"wald"`, `"wilson"`, `"wilsoncc"`, |
|
316 |
#' `"agresti-coull"`, `"jeffreys"`, `"modified wilson"`, `"modified jeffreys"`, `"clopper-pearson"`, `"arcsine"`, |
|
317 |
#' `"logit"`, `"witting"`, `"pratt"`, `"midp"`, `"lik"`, and `"blaker"`. |
|
318 |
#' |
|
319 |
#' @return A `matrix` with 3 columns containing: |
|
320 |
#' * `est`: estimate of proportion difference. |
|
321 |
#' * `lwr.ci`: lower end of the confidence interval. |
|
322 |
#' * `upr.ci`: upper end of the confidence interval. |
|
323 |
#' |
|
324 |
#' @keywords internal |
|
325 |
desctools_binomci <- function(x, |
|
326 |
n, |
|
327 |
conf.level = 0.95, # nolint |
|
328 |
sides = c("two.sided", "left", "right"), |
|
329 |
method = c( |
|
330 |
"wilson", "wald", "waldcc", "agresti-coull", |
|
331 |
"jeffreys", "modified wilson", "wilsoncc", "modified jeffreys", |
|
332 |
"clopper-pearson", "arcsine", "logit", "witting", "pratt", |
|
333 |
"midp", "lik", "blaker" |
|
334 |
), |
|
335 |
rand = 123, |
|
336 |
tol = 1e-05) { |
|
337 | 24x |
if (missing(method)) { |
338 | 1x |
method <- "wilson" |
339 |
} |
|
340 | 24x |
if (missing(sides)) { |
341 | 23x |
sides <- "two.sided" |
342 |
} |
|
343 | 24x |
iBinomCI <- function(x, n, conf.level = 0.95, sides = c("two.sided", "left", "right"), # nolint |
344 | 24x |
method = c( |
345 | 24x |
"wilson", "wilsoncc", "wald", |
346 | 24x |
"waldcc", "agresti-coull", "jeffreys", "modified wilson", |
347 | 24x |
"modified jeffreys", "clopper-pearson", "arcsine", "logit", |
348 | 24x |
"witting", "pratt", "midp", "lik", "blaker" |
349 |
), |
|
350 | 24x |
rand = 123, |
351 | 24x |
tol = 1e-05) { |
352 | 24x |
if (length(x) != 1) { |
353 | ! |
stop("'x' has to be of length 1 (number of successes)") |
354 |
} |
|
355 | 24x |
if (length(n) != 1) { |
356 | ! |
stop("'n' has to be of length 1 (number of trials)") |
357 |
} |
|
358 | 24x |
if (length(conf.level) != 1) { |
359 | ! |
stop("'conf.level' has to be of length 1 (confidence level)") |
360 |
} |
|
361 | 24x |
if (conf.level < 0.5 || conf.level > 1) { |
362 | ! |
stop("'conf.level' has to be in [0.5, 1]") |
363 |
} |
|
364 | 24x |
sides <- match.arg(sides, choices = c( |
365 | 24x |
"two.sided", "left", |
366 | 24x |
"right" |
367 | 24x |
), several.ok = FALSE) |
368 | 24x |
if (sides != "two.sided") { |
369 | 1x |
conf.level <- 1 - 2 * (1 - conf.level) # nolint |
370 |
} |
|
371 | 24x |
alpha <- 1 - conf.level |
372 | 24x |
kappa <- stats::qnorm(1 - alpha / 2) |
373 | 24x |
p_hat <- x / n |
374 | 24x |
q_hat <- 1 - p_hat |
375 | 24x |
est <- p_hat |
376 | 24x |
switch(match.arg(arg = method, choices = c( |
377 | 24x |
"wilson", |
378 | 24x |
"wald", "waldcc", "wilsoncc", "agresti-coull", "jeffreys", |
379 | 24x |
"modified wilson", "modified jeffreys", "clopper-pearson", |
380 | 24x |
"arcsine", "logit", "witting", "pratt", "midp", "lik", |
381 | 24x |
"blaker" |
382 |
)), |
|
383 | 24x |
wald = { |
384 | 1x |
term2 <- kappa * sqrt(p_hat * q_hat) / sqrt(n) |
385 | 1x |
ci_lwr <- max(0, p_hat - term2) |
386 | 1x |
ci_upr <- min(1, p_hat + term2) |
387 |
}, |
|
388 | 24x |
waldcc = { |
389 | 1x |
term2 <- kappa * sqrt(p_hat * q_hat) / sqrt(n) |
390 | 1x |
term2 <- term2 + 1 / (2 * n) |
391 | 1x |
ci_lwr <- max(0, p_hat - term2) |
392 | 1x |
ci_upr <- min(1, p_hat + term2) |
393 |
}, |
|
394 | 24x |
wilson = { |
395 | 6x |
term1 <- (x + kappa^2 / 2) / (n + kappa^2) |
396 | 6x |
term2 <- kappa * sqrt(n) / (n + kappa^2) * sqrt(p_hat * q_hat + kappa^2 / (4 * n)) |
397 | 6x |
ci_lwr <- max(0, term1 - term2) |
398 | 6x |
ci_upr <- min(1, term1 + term2) |
399 |
}, |
|
400 | 24x |
wilsoncc = { |
401 | 3x |
lci <- ( |
402 | 3x |
2 * x + kappa^2 - 1 - kappa * sqrt(kappa^2 - 2 - 1 / n + 4 * p_hat * (n * q_hat + 1)) |
403 | 3x |
) / (2 * (n + kappa^2)) |
404 | 3x |
uci <- ( |
405 | 3x |
2 * x + kappa^2 + 1 + kappa * sqrt(kappa^2 + 2 - 1 / n + 4 * p_hat * (n * q_hat - 1)) |
406 | 3x |
) / (2 * (n + kappa^2)) |
407 | 3x |
ci_lwr <- max(0, ifelse(p_hat == 0, 0, lci)) |
408 | 3x |
ci_upr <- min(1, ifelse(p_hat == 1, 1, uci)) |
409 |
}, |
|
410 | 24x |
`agresti-coull` = { |
411 | 1x |
x_tilde <- x + kappa^2 / 2 |
412 | 1x |
n_tilde <- n + kappa^2 |
413 | 1x |
p_tilde <- x_tilde / n_tilde |
414 | 1x |
q_tilde <- 1 - p_tilde |
415 | 1x |
est <- p_tilde |
416 | 1x |
term2 <- kappa * sqrt(p_tilde * q_tilde) / sqrt(n_tilde) |
417 | 1x |
ci_lwr <- max(0, p_tilde - term2) |
418 | 1x |
ci_upr <- min(1, p_tilde + term2) |
419 |
}, |
|
420 | 24x |
jeffreys = { |
421 | 1x |
if (x == 0) { |
422 | ! |
ci_lwr <- 0 |
423 |
} else { |
|
424 | 1x |
ci_lwr <- stats::qbeta( |
425 | 1x |
alpha / 2, |
426 | 1x |
x + 0.5, n - x + 0.5 |
427 |
) |
|
428 |
} |
|
429 | 1x |
if (x == n) { |
430 | ! |
ci_upr <- 1 |
431 |
} else { |
|
432 | 1x |
ci_upr <- stats::qbeta(1 - alpha / 2, x + 0.5, n - x + 0.5) |
433 |
} |
|
434 |
}, |
|
435 | 24x |
`modified wilson` = { |
436 | 1x |
term1 <- (x + kappa^2 / 2) / (n + kappa^2) |
437 | 1x |
term2 <- kappa * sqrt(n) / (n + kappa^2) * sqrt(p_hat * q_hat + kappa^2 / (4 * n)) |
438 | 1x |
if ((n <= 50 & x %in% c(1, 2)) | (n >= 51 & x %in% c(1:3))) { |
439 | ! |
ci_lwr <- 0.5 * stats::qchisq(alpha, 2 * x) / n |
440 |
} else { |
|
441 | 1x |
ci_lwr <- max(0, term1 - term2) |
442 |
} |
|
443 | 1x |
if ((n <= 50 & x %in% c(n - 1, n - 2)) | (n >= 51 & x %in% c(n - (1:3)))) { |
444 | ! |
ci_upr <- 1 - 0.5 * stats::qchisq( |
445 | ! |
alpha, |
446 | ! |
2 * (n - x) |
447 | ! |
) / n |
448 |
} else { |
|
449 | 1x |
ci_upr <- min(1, term1 + term2) |
450 |
} |
|
451 |
}, |
|
452 | 24x |
`modified jeffreys` = { |
453 | 1x |
if (x == n) { |
454 | ! |
ci_lwr <- (alpha / 2)^(1 / n) |
455 |
} else { |
|
456 | 1x |
if (x <= 1) { |
457 | ! |
ci_lwr <- 0 |
458 |
} else { |
|
459 | 1x |
ci_lwr <- stats::qbeta( |
460 | 1x |
alpha / 2, |
461 | 1x |
x + 0.5, n - x + 0.5 |
462 |
) |
|
463 |
} |
|
464 |
} |
|
465 | 1x |
if (x == 0) { |
466 | ! |
ci_upr <- 1 - (alpha / 2)^(1 / n) |
467 |
} else { |
|
468 | 1x |
if (x >= n - 1) { |
469 | ! |
ci_upr <- 1 |
470 |
} else { |
|
471 | 1x |
ci_upr <- stats::qbeta(1 - alpha / 2, x + 0.5, n - x + 0.5) |
472 |
} |
|
473 |
} |
|
474 |
}, |
|
475 | 24x |
`clopper-pearson` = { |
476 | 1x |
ci_lwr <- stats::qbeta(alpha / 2, x, n - x + 1) |
477 | 1x |
ci_upr <- stats::qbeta(1 - alpha / 2, x + 1, n - x) |
478 |
}, |
|
479 | 24x |
arcsine = { |
480 | 1x |
p_tilde <- (x + 0.375) / (n + 0.75) |
481 | 1x |
est <- p_tilde |
482 | 1x |
ci_lwr <- sin(asin(sqrt(p_tilde)) - 0.5 * kappa / sqrt(n))^2 |
483 | 1x |
ci_upr <- sin(asin(sqrt(p_tilde)) + 0.5 * kappa / sqrt(n))^2 |
484 |
}, |
|
485 | 24x |
logit = { |
486 | 1x |
lambda_hat <- log(x / (n - x)) |
487 | 1x |
V_hat <- n / (x * (n - x)) # nolint |
488 | 1x |
lambda_lower <- lambda_hat - kappa * sqrt(V_hat) |
489 | 1x |
lambda_upper <- lambda_hat + kappa * sqrt(V_hat) |
490 | 1x |
ci_lwr <- exp(lambda_lower) / (1 + exp(lambda_lower)) |
491 | 1x |
ci_upr <- exp(lambda_upper) / (1 + exp(lambda_upper)) |
492 |
}, |
|
493 | 24x |
witting = { |
494 | 1x |
set.seed(rand) |
495 | 1x |
x_tilde <- x + stats::runif(1, min = 0, max = 1) |
496 | 1x |
pbinom_abscont <- function(q, size, prob) { |
497 | 22x |
v <- trunc(q) |
498 | 22x |
term1 <- stats::pbinom(v - 1, size = size, prob = prob) |
499 | 22x |
term2 <- (q - v) * stats::dbinom(v, size = size, prob = prob) |
500 | 22x |
return(term1 + term2) |
501 |
} |
|
502 | 1x |
qbinom_abscont <- function(p, size, x) { |
503 | 2x |
fun <- function(prob, size, x, p) { |
504 | 22x |
pbinom_abscont(x, size, prob) - p |
505 |
} |
|
506 | 2x |
stats::uniroot(fun, |
507 | 2x |
interval = c(0, 1), size = size, |
508 | 2x |
x = x, p = p |
509 | 2x |
)$root |
510 |
} |
|
511 | 1x |
ci_lwr <- qbinom_abscont(1 - alpha, size = n, x = x_tilde) |
512 | 1x |
ci_upr <- qbinom_abscont(alpha, size = n, x = x_tilde) |
513 |
}, |
|
514 | 24x |
pratt = { |
515 | 1x |
if (x == 0) { |
516 | ! |
ci_lwr <- 0 |
517 | ! |
ci_upr <- 1 - alpha^(1 / n) |
518 | 1x |
} else if (x == 1) { |
519 | ! |
ci_lwr <- 1 - (1 - alpha / 2)^(1 / n) |
520 | ! |
ci_upr <- 1 - (alpha / 2)^(1 / n) |
521 | 1x |
} else if (x == (n - 1)) { |
522 | ! |
ci_lwr <- (alpha / 2)^(1 / n) |
523 | ! |
ci_upr <- (1 - alpha / 2)^(1 / n) |
524 | 1x |
} else if (x == n) { |
525 | ! |
ci_lwr <- alpha^(1 / n) |
526 | ! |
ci_upr <- 1 |
527 |
} else { |
|
528 | 1x |
z <- stats::qnorm(1 - alpha / 2) |
529 | 1x |
A <- ((x + 1) / (n - x))^2 # nolint |
530 | 1x |
B <- 81 * (x + 1) * (n - x) - 9 * n - 8 # nolint |
531 | 1x |
C <- (0 - 3) * z * sqrt(9 * (x + 1) * (n - x) * (9 * n + 5 - z^2) + n + 1) # nolint |
532 | 1x |
D <- 81 * (x + 1)^2 - 9 * (x + 1) * (2 + z^2) + 1 # nolint |
533 | 1x |
E <- 1 + A * ((B + C) / D)^3 # nolint |
534 | 1x |
ci_upr <- 1 / E |
535 | 1x |
A <- (x / (n - x - 1))^2 # nolint |
536 | 1x |
B <- 81 * x * (n - x - 1) - 9 * n - 8 # nolint |
537 | 1x |
C <- 3 * z * sqrt(9 * x * (n - x - 1) * (9 * n + 5 - z^2) + n + 1) # nolint |
538 | 1x |
D <- 81 * x^2 - 9 * x * (2 + z^2) + 1 # nolint |
539 | 1x |
E <- 1 + A * ((B + C) / D)^3 # nolint |
540 | 1x |
ci_lwr <- 1 / E |
541 |
} |
|
542 |
}, |
|
543 | 24x |
midp = { |
544 | 1x |
f_low <- function(pi, x, n) { |
545 | 12x |
1 / 2 * stats::dbinom(x, size = n, prob = pi) + stats::pbinom(x, |
546 | 12x |
size = n, prob = pi, lower.tail = FALSE |
547 |
) - |
|
548 | 12x |
(1 - conf.level) / 2 |
549 |
} |
|
550 | 1x |
f_up <- function(pi, x, n) { |
551 | 12x |
1 / 2 * stats::dbinom(x, size = n, prob = pi) + stats::pbinom(x - 1, size = n, prob = pi) - (1 - conf.level) / 2 |
552 |
} |
|
553 | 1x |
ci_lwr <- 0 |
554 | 1x |
ci_upr <- 1 |
555 | 1x |
if (x != 0) { |
556 | 1x |
ci_lwr <- stats::uniroot(f_low, |
557 | 1x |
interval = c(0, p_hat), |
558 | 1x |
x = x, n = n |
559 | 1x |
)$root |
560 |
} |
|
561 | 1x |
if (x != n) { |
562 | 1x |
ci_upr <- stats::uniroot(f_up, interval = c( |
563 | 1x |
p_hat, |
564 | 1x |
1 |
565 | 1x |
), x = x, n = n)$root |
566 |
} |
|
567 |
}, |
|
568 | 24x |
lik = { |
569 | 2x |
ci_lwr <- 0 |
570 | 2x |
ci_upr <- 1 |
571 | 2x |
z <- stats::qnorm(1 - alpha * 0.5) |
572 | 2x |
tol <- .Machine$double.eps^0.5 |
573 | 2x |
BinDev <- function(y, x, mu, wt, bound = 0, tol = .Machine$double.eps^0.5, # nolint |
574 |
...) { |
|
575 | 40x |
ll_y <- ifelse(y %in% c(0, 1), 0, stats::dbinom(x, wt, |
576 | 40x |
y, |
577 | 40x |
log = TRUE |
578 |
)) |
|
579 | 40x |
ll_mu <- ifelse(mu %in% c(0, 1), 0, stats::dbinom(x, |
580 | 40x |
wt, mu, |
581 | 40x |
log = TRUE |
582 |
)) |
|
583 | 40x |
res <- ifelse(abs(y - mu) < tol, 0, sign(y - mu) * sqrt(-2 * (ll_y - ll_mu))) |
584 | 40x |
return(res - bound) |
585 |
} |
|
586 | 2x |
if (x != 0 && tol < p_hat) { |
587 | 2x |
ci_lwr <- if (BinDev( |
588 | 2x |
tol, x, p_hat, n, -z, |
589 | 2x |
tol |
590 | 2x |
) <= 0) { |
591 | 2x |
stats::uniroot( |
592 | 2x |
f = BinDev, interval = c(tol, if (p_hat < tol || p_hat == 1) { |
593 | ! |
1 - tol |
594 |
} else { |
|
595 | 2x |
p_hat |
596 | 2x |
}), bound = -z, |
597 | 2x |
x = x, mu = p_hat, wt = n |
598 | 2x |
)$root |
599 |
} |
|
600 |
} |
|
601 | 2x |
if (x != n && p_hat < (1 - tol)) { |
602 | 2x |
ci_upr <- if ( |
603 | 2x |
BinDev(y = 1 - tol, x = x, mu = ifelse(p_hat > 1 - tol, tol, p_hat), wt = n, bound = z, tol = tol) < 0) { # nolint |
604 | ! |
ci_lwr <- if (BinDev( |
605 | ! |
tol, x, if (p_hat < tol || p_hat == 1) { |
606 | ! |
1 - tol |
607 |
} else { |
|
608 | ! |
p_hat |
609 | ! |
}, n, |
610 | ! |
-z, tol |
611 | ! |
) <= 0) { |
612 | ! |
stats::uniroot( |
613 | ! |
f = BinDev, interval = c(tol, p_hat), |
614 | ! |
bound = -z, x = x, mu = p_hat, wt = n |
615 | ! |
)$root |
616 |
} |
|
617 |
} else { |
|
618 | 2x |
stats::uniroot( |
619 | 2x |
f = BinDev, interval = c(if (p_hat > 1 - tol) { |
620 | ! |
tol |
621 |
} else { |
|
622 | 2x |
p_hat |
623 | 2x |
}, 1 - tol), bound = z, |
624 | 2x |
x = x, mu = p_hat, wt = n |
625 | 2x |
)$root |
626 |
} |
|
627 |
} |
|
628 |
}, |
|
629 | 24x |
blaker = { |
630 | 1x |
acceptbin <- function(x, n, p) { |
631 | 3954x |
p1 <- 1 - stats::pbinom(x - 1, n, p) |
632 | 3954x |
p2 <- stats::pbinom(x, n, p) |
633 | 3954x |
a1 <- p1 + stats::pbinom(stats::qbinom(p1, n, p) - 1, n, p) |
634 | 3954x |
a2 <- p2 + 1 - stats::pbinom( |
635 | 3954x |
stats::qbinom(1 - p2, n, p), n, |
636 | 3954x |
p |
637 |
) |
|
638 | 3954x |
return(min(a1, a2)) |
639 |
} |
|
640 | 1x |
ci_lwr <- 0 |
641 | 1x |
ci_upr <- 1 |
642 | 1x |
if (x != 0) { |
643 | 1x |
ci_lwr <- stats::qbeta((1 - conf.level) / 2, x, n - x + 1) |
644 | 1x |
while (acceptbin(x, n, ci_lwr + tol) < (1 - conf.level)) { |
645 | 1976x |
ci_lwr <- ci_lwr + tol |
646 |
} |
|
647 |
} |
|
648 | 1x |
if (x != n) { |
649 | 1x |
ci_upr <- stats::qbeta(1 - (1 - conf.level) / 2, x + 1, n - x) |
650 | 1x |
while (acceptbin(x, n, ci_upr - tol) < (1 - conf.level)) { |
651 | 1976x |
ci_upr <- ci_upr - tol |
652 |
} |
|
653 |
} |
|
654 |
} |
|
655 |
) |
|
656 | 24x |
ci <- c(est = est, lwr.ci = max(0, ci_lwr), upr.ci = min( |
657 | 24x |
1, |
658 | 24x |
ci_upr |
659 |
)) |
|
660 | 24x |
if (sides == "left") { |
661 | 1x |
ci[3] <- 1 |
662 | 23x |
} else if (sides == "right") { |
663 | ! |
ci[2] <- 0 |
664 |
} |
|
665 | 24x |
return(ci) |
666 |
} |
|
667 | 24x |
lst <- list( |
668 | 24x |
x = x, n = n, conf.level = conf.level, sides = sides, |
669 | 24x |
method = method, rand = rand |
670 |
) |
|
671 | 24x |
maxdim <- max(unlist(lapply(lst, length))) |
672 | 24x |
lgp <- lapply(lst, rep, length.out = maxdim) |
673 | 24x |
lgn <- h_recycle(x = if (is.null(names(x))) { |
674 | 24x |
paste("x", seq_along(x), sep = ".") |
675 |
} else { |
|
676 | ! |
names(x) |
677 | 24x |
}, n = if (is.null(names(n))) { |
678 | 24x |
paste("n", seq_along(n), sep = ".") |
679 |
} else { |
|
680 | ! |
names(n) |
681 | 24x |
}, conf.level = conf.level, sides = sides, method = method) |
682 | 24x |
xn <- apply(as.data.frame(lgn[sapply(lgn, function(x) { |
683 | 120x |
length(unique(x)) != |
684 | 120x |
1 |
685 | 24x |
})]), 1, paste, collapse = ":") |
686 | 24x |
res <- t(sapply(1:maxdim, function(i) { |
687 | 24x |
iBinomCI( |
688 | 24x |
x = lgp$x[i], |
689 | 24x |
n = lgp$n[i], conf.level = lgp$conf.level[i], sides = lgp$sides[i], |
690 | 24x |
method = lgp$method[i], rand = lgp$rand[i] |
691 |
) |
|
692 |
})) |
|
693 | 24x |
colnames(res)[1] <- c("est") |
694 | 24x |
rownames(res) <- xn |
695 | 24x |
return(res) |
696 |
} |
1 |
#' Create a forest plot from an `rtable` |
|
2 |
#' |
|
3 |
#' Given a [rtables::rtable()] object with at least one column with a single value and one column with 2 |
|
4 |
#' values, converts table to a [ggplot2::ggplot()] object and generates an accompanying forest plot. The |
|
5 |
#' table and forest plot are printed side-by-side. |
|
6 |
#' |
|
7 |
#' @description `r lifecycle::badge("stable")` |
|
8 |
#' |
|
9 |
#' @inheritParams rtable2gg |
|
10 |
#' @inheritParams argument_convention |
|
11 |
#' @param tbl (`VTableTree`)\cr `rtables` table with at least one column with a single value and one column with 2 |
|
12 |
#' values. |
|
13 |
#' @param col_x (`integer(1)` or `NULL`)\cr column index with estimator. By default tries to get this from |
|
14 |
#' `tbl` attribute `col_x`, otherwise needs to be manually specified. If `NULL`, points will be excluded |
|
15 |
#' from forest plot. |
|
16 |
#' @param col_ci (`integer(1)` or `NULL`)\cr column index with confidence intervals. By default tries to get this from |
|
17 |
#' `tbl` attribute `col_ci`, otherwise needs to be manually specified. If `NULL`, lines will be excluded |
|
18 |
#' from forest plot. |
|
19 |
#' @param vline (`numeric(1)` or `NULL`)\cr x coordinate for vertical line, if `NULL` then the line is omitted. |
|
20 |
#' @param forest_header (`character(2)`)\cr text displayed to the left and right of `vline`, respectively. |
|
21 |
#' If `vline = NULL` then `forest_header` is not printed. By default tries to get this from `tbl` attribute |
|
22 |
#' `forest_header`. If `NULL`, defaults will be extracted from the table if possible, and set to |
|
23 |
#' `"Comparison\nBetter"` and `"Treatment\nBetter"` if not. |
|
24 |
#' @param xlim (`numeric(2)`)\cr limits for x axis. |
|
25 |
#' @param logx (`flag`)\cr show the x-values on logarithm scale. |
|
26 |
#' @param x_at (`numeric`)\cr x-tick locations, if `NULL`, `x_at` is set to `vline` and both `xlim` values. |
|
27 |
#' @param width_row_names `r lifecycle::badge("deprecated")` Please use the `lbl_col_padding` argument instead. |
|
28 |
#' @param width_columns (`numeric`)\cr a vector of column widths. Each element's position in |
|
29 |
#' `colwidths` corresponds to the column of `tbl` in the same position. If `NULL`, column widths are calculated |
|
30 |
#' according to maximum number of characters per column. |
|
31 |
#' @param width_forest `r lifecycle::badge("deprecated")` Please use the `rel_width_forest` argument instead. |
|
32 |
#' @param rel_width_forest (`proportion`)\cr proportion of total width to allocate to the forest plot. Relative |
|
33 |
#' width of table is then `1 - rel_width_forest`. If `as_list = TRUE`, this parameter is ignored. |
|
34 |
#' @param font_size (`numeric(1)`)\cr font size. |
|
35 |
#' @param col_symbol_size (`numeric` or `NULL`)\cr column index from `tbl` containing data to be used |
|
36 |
#' to determine relative size for estimator plot symbol. Typically, the symbol size is proportional |
|
37 |
#' to the sample size used to calculate the estimator. If `NULL`, the same symbol size is used for all subgroups. |
|
38 |
#' By default tries to get this from `tbl` attribute `col_symbol_size`, otherwise needs to be manually specified. |
|
39 |
#' @param col (`character`)\cr color(s). |
|
40 |
#' @param ggtheme (`theme`)\cr a graphical theme as provided by `ggplot2` to control styling of the plot. |
|
41 |
#' @param as_list (`flag`)\cr whether the two `ggplot` objects should be returned as a list. If `TRUE`, a named list |
|
42 |
#' with two elements, `table` and `plot`, will be returned. If `FALSE` (default) the table and forest plot are |
|
43 |
#' printed side-by-side via [cowplot::plot_grid()]. |
|
44 |
#' @param gp `r lifecycle::badge("deprecated")` `g_forest` is now generated as a `ggplot` object. This argument |
|
45 |
#' is no longer used. |
|
46 |
#' @param draw `r lifecycle::badge("deprecated")` `g_forest` is now generated as a `ggplot` object. This argument |
|
47 |
#' is no longer used. |
|
48 |
#' @param newpage `r lifecycle::badge("deprecated")` `g_forest` is now generated as a `ggplot` object. This argument |
|
49 |
#' is no longer used. |
|
50 |
#' |
|
51 |
#' @return `ggplot` forest plot and table. |
|
52 |
#' |
|
53 |
#' @examples |
|
54 |
#' library(dplyr) |
|
55 |
#' library(forcats) |
|
56 |
#' library(nestcolor) |
|
57 |
#' |
|
58 |
#' adrs <- tern_ex_adrs |
|
59 |
#' n_records <- 20 |
|
60 |
#' adrs_labels <- formatters::var_labels(adrs, fill = TRUE) |
|
61 |
#' adrs <- adrs %>% |
|
62 |
#' filter(PARAMCD == "BESRSPI") %>% |
|
63 |
#' filter(ARM %in% c("A: Drug X", "B: Placebo")) %>% |
|
64 |
#' slice(seq_len(n_records)) %>% |
|
65 |
#' droplevels() %>% |
|
66 |
#' mutate( |
|
67 |
#' # Reorder levels of factor to make the placebo group the reference arm. |
|
68 |
#' ARM = fct_relevel(ARM, "B: Placebo"), |
|
69 |
#' rsp = AVALC == "CR" |
|
70 |
#' ) |
|
71 |
#' formatters::var_labels(adrs) <- c(adrs_labels, "Response") |
|
72 |
#' df <- extract_rsp_subgroups( |
|
73 |
#' variables = list(rsp = "rsp", arm = "ARM", subgroups = c("SEX", "STRATA2")), |
|
74 |
#' data = adrs |
|
75 |
#' ) |
|
76 |
#' # Full commonly used response table. |
|
77 |
#' |
|
78 |
#' tbl <- basic_table() %>% |
|
79 |
#' tabulate_rsp_subgroups(df) |
|
80 |
#' g_forest(tbl) |
|
81 |
#' |
|
82 |
#' # Odds ratio only table. |
|
83 |
#' |
|
84 |
#' tbl_or <- basic_table() %>% |
|
85 |
#' tabulate_rsp_subgroups(df, vars = c("n_tot", "or", "ci")) |
|
86 |
#' g_forest( |
|
87 |
#' tbl_or, |
|
88 |
#' forest_header = c("Comparison\nBetter", "Treatment\nBetter") |
|
89 |
#' ) |
|
90 |
#' |
|
91 |
#' # Survival forest plot example. |
|
92 |
#' adtte <- tern_ex_adtte |
|
93 |
#' # Save variable labels before data processing steps. |
|
94 |
#' adtte_labels <- formatters::var_labels(adtte, fill = TRUE) |
|
95 |
#' adtte_f <- adtte %>% |
|
96 |
#' filter( |
|
97 |
#' PARAMCD == "OS", |
|
98 |
#' ARM %in% c("B: Placebo", "A: Drug X"), |
|
99 |
#' SEX %in% c("M", "F") |
|
100 |
#' ) %>% |
|
101 |
#' mutate( |
|
102 |
#' # Reorder levels of ARM to display reference arm before treatment arm. |
|
103 |
#' ARM = droplevels(fct_relevel(ARM, "B: Placebo")), |
|
104 |
#' SEX = droplevels(SEX), |
|
105 |
#' AVALU = as.character(AVALU), |
|
106 |
#' is_event = CNSR == 0 |
|
107 |
#' ) |
|
108 |
#' labels <- list( |
|
109 |
#' "ARM" = adtte_labels["ARM"], |
|
110 |
#' "SEX" = adtte_labels["SEX"], |
|
111 |
#' "AVALU" = adtte_labels["AVALU"], |
|
112 |
#' "is_event" = "Event Flag" |
|
113 |
#' ) |
|
114 |
#' formatters::var_labels(adtte_f)[names(labels)] <- as.character(labels) |
|
115 |
#' df <- extract_survival_subgroups( |
|
116 |
#' variables = list( |
|
117 |
#' tte = "AVAL", |
|
118 |
#' is_event = "is_event", |
|
119 |
#' arm = "ARM", subgroups = c("SEX", "BMRKR2") |
|
120 |
#' ), |
|
121 |
#' data = adtte_f |
|
122 |
#' ) |
|
123 |
#' table_hr <- basic_table() %>% |
|
124 |
#' tabulate_survival_subgroups(df, time_unit = adtte_f$AVALU[1]) |
|
125 |
#' g_forest(table_hr) |
|
126 |
#' |
|
127 |
#' # Works with any `rtable`. |
|
128 |
#' tbl <- rtable( |
|
129 |
#' header = c("E", "CI", "N"), |
|
130 |
#' rrow("", 1, c(.8, 1.2), 200), |
|
131 |
#' rrow("", 1.2, c(1.1, 1.4), 50) |
|
132 |
#' ) |
|
133 |
#' g_forest( |
|
134 |
#' tbl = tbl, |
|
135 |
#' col_x = 1, |
|
136 |
#' col_ci = 2, |
|
137 |
#' xlim = c(0.5, 2), |
|
138 |
#' x_at = c(0.5, 1, 2), |
|
139 |
#' col_symbol_size = 3 |
|
140 |
#' ) |
|
141 |
#' |
|
142 |
#' tbl <- rtable( |
|
143 |
#' header = rheader( |
|
144 |
#' rrow("", rcell("A", colspan = 2)), |
|
145 |
#' rrow("", "c1", "c2") |
|
146 |
#' ), |
|
147 |
#' rrow("row 1", 1, c(.8, 1.2)), |
|
148 |
#' rrow("row 2", 1.2, c(1.1, 1.4)) |
|
149 |
#' ) |
|
150 |
#' g_forest( |
|
151 |
#' tbl = tbl, |
|
152 |
#' col_x = 1, |
|
153 |
#' col_ci = 2, |
|
154 |
#' xlim = c(0.5, 2), |
|
155 |
#' x_at = c(0.5, 1, 2), |
|
156 |
#' vline = 1, |
|
157 |
#' forest_header = c("Hello", "World") |
|
158 |
#' ) |
|
159 |
#' |
|
160 |
#' @export |
|
161 |
g_forest <- function(tbl, |
|
162 |
col_x = attr(tbl, "col_x"), |
|
163 |
col_ci = attr(tbl, "col_ci"), |
|
164 |
vline = 1, |
|
165 |
forest_header = attr(tbl, "forest_header"), |
|
166 |
xlim = c(0.1, 10), |
|
167 |
logx = TRUE, |
|
168 |
x_at = c(0.1, 1, 10), |
|
169 |
width_row_names = lifecycle::deprecated(), |
|
170 |
width_columns = NULL, |
|
171 |
width_forest = lifecycle::deprecated(), |
|
172 |
lbl_col_padding = 0, |
|
173 |
rel_width_forest = 0.25, |
|
174 |
font_size = 12, |
|
175 |
col_symbol_size = attr(tbl, "col_symbol_size"), |
|
176 |
col = getOption("ggplot2.discrete.colour")[1], |
|
177 |
ggtheme = NULL, |
|
178 |
as_list = FALSE, |
|
179 |
gp = lifecycle::deprecated(), |
|
180 |
draw = lifecycle::deprecated(), |
|
181 |
newpage = lifecycle::deprecated()) { |
|
182 |
# Deprecated argument warnings |
|
183 | 4x |
if (lifecycle::is_present(width_row_names)) { |
184 | 1x |
lifecycle::deprecate_warn( |
185 | 1x |
"0.9.4", "g_forest(width_row_names)", "g_forest(lbl_col_padding)", |
186 | 1x |
details = "The width of the row label column can be adjusted via the `lbl_col_padding` parameter." |
187 |
) |
|
188 |
} |
|
189 | 4x |
if (lifecycle::is_present(width_forest)) { |
190 | 1x |
lifecycle::deprecate_warn( |
191 | 1x |
"0.9.4", "g_forest(width_forest)", "g_forest(rel_width_forest)", |
192 | 1x |
details = "Relative width of the forest plot (as a proportion) can be set via the `rel_width_forest` parameter." |
193 |
) |
|
194 |
} |
|
195 | 4x |
if (lifecycle::is_present(gp)) { |
196 | 1x |
lifecycle::deprecate_warn( |
197 | 1x |
"0.9.4", "g_forest(gp)", "g_forest(ggtheme)", |
198 | 1x |
details = paste( |
199 | 1x |
"`g_forest` is now generated as a `ggplot` object.", |
200 | 1x |
"Additional display settings should be supplied via the `ggtheme` parameter." |
201 |
) |
|
202 |
) |
|
203 |
} |
|
204 | 4x |
if (lifecycle::is_present(draw)) { |
205 | 1x |
lifecycle::deprecate_warn( |
206 | 1x |
"0.9.4", "g_forest(draw)", |
207 | 1x |
details = "`g_forest` now generates `ggplot` objects. This parameter has no effect." |
208 |
) |
|
209 |
} |
|
210 | 4x |
if (lifecycle::is_present(newpage)) { |
211 | 1x |
lifecycle::deprecate_warn( |
212 | 1x |
"0.9.4", "g_forest(newpage)", |
213 | 1x |
details = "`g_forest` now generates `ggplot` objects. This parameter has no effect." |
214 |
) |
|
215 |
} |
|
216 | ||
217 | 4x |
checkmate::assert_class(tbl, "VTableTree") |
218 | 4x |
checkmate::assert_number(col_x, lower = 0, upper = ncol(tbl), null.ok = TRUE) |
219 | 4x |
checkmate::assert_number(col_ci, lower = 0, upper = ncol(tbl), null.ok = TRUE) |
220 | 4x |
checkmate::assert_number(col_symbol_size, lower = 0, upper = ncol(tbl), null.ok = TRUE) |
221 | 4x |
checkmate::assert_number(font_size, lower = 0) |
222 | 4x |
checkmate::assert_character(col, null.ok = TRUE) |
223 | 4x |
checkmate::assert_true(is.null(col) | length(col) == 1 | length(col) == nrow(tbl)) |
224 | ||
225 |
# Extract info from table |
|
226 | 4x |
mat <- matrix_form(tbl, indent_rownames = TRUE) |
227 | 4x |
mat_strings <- formatters::mf_strings(mat) |
228 | 4x |
nlines_hdr <- formatters::mf_nlheader(mat) |
229 | 4x |
nrows_body <- nrow(mat_strings) - nlines_hdr |
230 | 4x |
tbl_stats <- mat_strings[nlines_hdr, -1] |
231 | ||
232 |
# Generate and modify table as ggplot object |
|
233 | 4x |
gg_table <- rtable2gg(tbl, fontsize = font_size, colwidths = width_columns, lbl_col_padding = lbl_col_padding) + |
234 | 4x |
theme(plot.margin = margin(0, 0, 0, 0.025, "npc")) |
235 | 4x |
gg_table$scales$scales[[1]]$expand <- c(0.01, 0.01) |
236 | 4x |
gg_table$scales$scales[[2]]$limits[2] <- nrow(mat_strings) + 1 |
237 | 4x |
if (nlines_hdr == 2) { |
238 | 4x |
gg_table$scales$scales[[2]]$expand <- c(0, 0) |
239 | 4x |
arms <- unique(mat_strings[1, ][nzchar(trimws(mat_strings[1, ]))]) |
240 |
} else { |
|
241 | ! |
arms <- NULL |
242 |
} |
|
243 | ||
244 | 4x |
tbl_df <- as_result_df(tbl) |
245 | 4x |
dat_cols <- seq(which(names(tbl_df) == "node_class") + 1, ncol(tbl_df)) |
246 | 4x |
tbl_df <- tbl_df[, c(which(names(tbl_df) == "row_num"), dat_cols)] |
247 | 4x |
names(tbl_df) <- c("row_num", tbl_stats) |
248 | ||
249 |
# Check table data columns |
|
250 | 4x |
if (!is.null(col_ci)) { |
251 | 4x |
ci_col <- col_ci + 1 |
252 |
} else { |
|
253 | ! |
tbl_df[["empty_ci"]] <- rep(list(c(NA_real_, NA_real_)), nrow(tbl_df)) |
254 | ! |
ci_col <- which(names(tbl_df) == "empty_ci") |
255 |
} |
|
256 | ! |
if (length(tbl_df[, ci_col][[1]]) != 2) stop("CI column must have two elements (lower and upper limits).") |
257 | ||
258 | 4x |
if (!is.null(col_x)) { |
259 | 4x |
x_col <- col_x + 1 |
260 |
} else { |
|
261 | ! |
tbl_df[["empty_x"]] <- NA_real_ |
262 | ! |
x_col <- which(names(tbl_df) == "empty_x") |
263 |
} |
|
264 | 4x |
if (!is.null(col_symbol_size)) { |
265 | 3x |
sym_size <- unlist(tbl_df[, col_symbol_size + 1]) |
266 |
} else { |
|
267 | 1x |
sym_size <- rep(1, nrow(tbl_df)) |
268 |
} |
|
269 | ||
270 | 4x |
tbl_df[, c("ci_lwr", "ci_upr")] <- t(sapply(tbl_df[, ci_col], unlist)) |
271 | 4x |
x <- unlist(tbl_df[, x_col]) |
272 | 4x |
lwr <- unlist(tbl_df[["ci_lwr"]]) |
273 | 4x |
upr <- unlist(tbl_df[["ci_upr"]]) |
274 | 4x |
row_num <- nrow(mat_strings) - tbl_df[["row_num"]] - as.numeric(nlines_hdr == 2) |
275 | ||
276 | ! |
if (is.null(col)) col <- "#343cff" |
277 | 4x |
if (length(col) == 1) col <- rep(col, nrow(tbl_df)) |
278 | ! |
if (is.null(x_at)) x_at <- union(xlim, vline) |
279 | 4x |
x_labels <- x_at |
280 | ||
281 |
# Apply log transformation |
|
282 | 4x |
if (logx) { |
283 | 4x |
x_t <- log(x) |
284 | 4x |
lwr_t <- log(lwr) |
285 | 4x |
upr_t <- log(upr) |
286 | 4x |
xlim_t <- log(xlim) |
287 |
} else { |
|
288 | ! |
x_t <- x |
289 | ! |
lwr_t <- lwr |
290 | ! |
upr_t <- upr |
291 | ! |
xlim_t <- xlim |
292 |
} |
|
293 | ||
294 |
# Set up plot area |
|
295 | 4x |
gg_plt <- ggplot(data = tbl_df) + |
296 | 4x |
theme( |
297 | 4x |
panel.background = element_rect(fill = "transparent", color = NA_character_), |
298 | 4x |
plot.background = element_rect(fill = "transparent", color = NA_character_), |
299 | 4x |
panel.grid.major = element_blank(), |
300 | 4x |
panel.grid.minor = element_blank(), |
301 | 4x |
axis.title.x = element_blank(), |
302 | 4x |
axis.title.y = element_blank(), |
303 | 4x |
axis.line.x = element_line(), |
304 | 4x |
axis.text = element_text(size = font_size), |
305 | 4x |
legend.position = "none", |
306 | 4x |
plot.margin = margin(0, 0.1, 0.05, 0, "npc") |
307 |
) + |
|
308 | 4x |
scale_x_continuous( |
309 | 4x |
transform = ifelse(logx, "log", "identity"), |
310 | 4x |
limits = xlim, |
311 | 4x |
breaks = x_at, |
312 | 4x |
labels = x_labels, |
313 | 4x |
expand = c(0.01, 0) |
314 |
) + |
|
315 | 4x |
scale_y_continuous( |
316 | 4x |
limits = c(0, nrow(mat_strings) + 1), |
317 | 4x |
breaks = NULL, |
318 | 4x |
expand = c(0, 0) |
319 |
) + |
|
320 | 4x |
coord_cartesian(clip = "off") |
321 | ||
322 | 4x |
if (is.null(ggtheme)) { |
323 | 4x |
gg_plt <- gg_plt + annotate( |
324 | 4x |
"rect", |
325 | 4x |
xmin = xlim[1], |
326 | 4x |
xmax = xlim[2], |
327 | 4x |
ymin = 0, |
328 | 4x |
ymax = nrows_body + 0.5, |
329 | 4x |
fill = "grey92" |
330 |
) |
|
331 |
} |
|
332 | ||
333 | 4x |
if (!is.null(vline)) { |
334 |
# Set default forest header |
|
335 | 4x |
if (is.null(forest_header)) { |
336 | ! |
forest_header <- c( |
337 | ! |
paste(if (length(arms) == 2) arms[1] else "Comparison", "Better", sep = "\n"), |
338 | ! |
paste(if (length(arms) == 2) arms[2] else "Treatment", "Better", sep = "\n") |
339 |
) |
|
340 |
} |
|
341 | ||
342 |
# Add vline and forest header labels |
|
343 | 4x |
mid_pts <- if (logx) { |
344 | 4x |
c(exp(mean(log(c(xlim[1], vline)))), exp(mean(log(c(vline, xlim[2]))))) |
345 |
} else { |
|
346 | ! |
c(mean(c(xlim[1], vline)), mean(c(vline, xlim[2]))) |
347 |
} |
|
348 | 4x |
gg_plt <- gg_plt + |
349 | 4x |
annotate( |
350 | 4x |
"segment", |
351 | 4x |
x = vline, xend = vline, y = 0, yend = nrows_body + 0.5 |
352 |
) + |
|
353 | 4x |
annotate( |
354 | 4x |
"text", |
355 | 4x |
x = mid_pts[1], y = nrows_body + 1.25, |
356 | 4x |
label = forest_header[1], |
357 | 4x |
size = font_size / .pt, |
358 | 4x |
lineheight = 0.9 |
359 |
) + |
|
360 | 4x |
annotate( |
361 | 4x |
"text", |
362 | 4x |
x = mid_pts[2], y = nrows_body + 1.25, |
363 | 4x |
label = forest_header[2], |
364 | 4x |
size = font_size / .pt, |
365 | 4x |
lineheight = 0.9 |
366 |
) |
|
367 |
} |
|
368 | ||
369 |
# Add points to plot |
|
370 | 4x |
if (any(!is.na(x_t))) { |
371 | 4x |
x_t[x < xlim[1] | x > xlim[2]] <- NA |
372 | 4x |
gg_plt <- gg_plt + geom_point( |
373 | 4x |
x = x_t, |
374 | 4x |
y = row_num, |
375 | 4x |
color = col, |
376 | 4x |
aes(size = sym_size), |
377 | 4x |
na.rm = TRUE |
378 |
) |
|
379 |
} |
|
380 | ||
381 | 4x |
for (i in seq_len(nrow(tbl_df))) { |
382 |
# Determine which arrow(s) to add to CI lines |
|
383 | 17x |
which_arrow <- c(lwr_t[i] < xlim_t[1], upr_t[i] > xlim_t[2]) |
384 | 17x |
which_arrow <- dplyr::case_when( |
385 | 17x |
all(which_arrow) ~ "both", |
386 | 17x |
which_arrow[1] ~ "first", |
387 | 17x |
which_arrow[2] ~ "last", |
388 | 17x |
TRUE ~ NA_character_ |
389 |
) |
|
390 | ||
391 |
# Add CI lines |
|
392 | 17x |
gg_plt <- gg_plt + |
393 | 17x |
if (!is.na(which_arrow)) { |
394 | 15x |
annotate( |
395 | 15x |
"segment", |
396 | 15x |
x = if (!which_arrow %in% c("first", "both")) lwr[i] else xlim[1], |
397 | 15x |
xend = if (!which_arrow %in% c("last", "both")) upr[i] else xlim[2], |
398 | 15x |
y = row_num[i], yend = row_num[i], |
399 | 15x |
color = if (length(col) == 1) col else col[i], |
400 | 15x |
arrow = arrow(length = unit(0.05, "npc"), ends = which_arrow), |
401 | 15x |
na.rm = TRUE |
402 |
) |
|
403 |
} else { |
|
404 | 2x |
annotate( |
405 | 2x |
"segment", |
406 | 2x |
x = lwr[i], xend = upr[i], |
407 | 2x |
y = row_num[i], yend = row_num[i], |
408 | 2x |
color = if (length(col) == 1) col else col[i], |
409 | 2x |
na.rm = TRUE |
410 |
) |
|
411 |
} |
|
412 |
} |
|
413 | ||
414 |
# Apply custom ggtheme to plot |
|
415 | ! |
if (!is.null(ggtheme)) gg_plt <- gg_plt + ggtheme |
416 | ||
417 | 4x |
if (as_list) { |
418 | 1x |
list( |
419 | 1x |
table = gg_table, |
420 | 1x |
plot = gg_plt |
421 |
) |
|
422 |
} else { |
|
423 | 3x |
cowplot::plot_grid( |
424 | 3x |
gg_table, |
425 | 3x |
gg_plt, |
426 | 3x |
align = "h", |
427 | 3x |
axis = "tblr", |
428 | 3x |
rel_widths = c(1 - rel_width_forest, rel_width_forest) |
429 |
) |
|
430 |
} |
|
431 |
} |
|
432 | ||
433 |
#' Forest plot grob |
|
434 |
#' |
|
435 |
#' @description `r lifecycle::badge("deprecated")` |
|
436 |
#' |
|
437 |
#' @inheritParams g_forest |
|
438 |
#' @param tbl (`VTableTree`)\cr `rtables` table object. |
|
439 |
#' @param x (`numeric`)\cr coordinate of point. |
|
440 |
#' @param lower,upper (`numeric`)\cr lower/upper bound of the confidence interval. |
|
441 |
#' @param symbol_size (`numeric`)\cr vector with relative size for plot symbol. |
|
442 |
#' If `NULL`, the same symbol size is used. |
|
443 |
#' |
|
444 |
#' @details |
|
445 |
#' The heights get automatically determined. |
|
446 |
#' |
|
447 |
#' @examples |
|
448 |
#' tbl <- rtable( |
|
449 |
#' header = rheader( |
|
450 |
#' rrow("", "E", rcell("CI", colspan = 2), "N"), |
|
451 |
#' rrow("", "A", "B", "C", "D") |
|
452 |
#' ), |
|
453 |
#' rrow("row 1", 1, 0.8, 1.1, 16), |
|
454 |
#' rrow("row 2", 1.4, 0.8, 1.6, 25), |
|
455 |
#' rrow("row 3", 1.2, 0.8, 1.6, 36) |
|
456 |
#' ) |
|
457 |
#' |
|
458 |
#' x <- c(1, 1.4, 1.2) |
|
459 |
#' lower <- c(0.8, 0.8, 0.8) |
|
460 |
#' upper <- c(1.1, 1.6, 1.6) |
|
461 |
#' # numeric vector with multiplication factor to scale each circle radius |
|
462 |
#' # default radius is 1/3.5 lines |
|
463 |
#' symbol_scale <- c(1, 1.25, 1.5) |
|
464 |
#' |
|
465 |
#' # Internal function - forest_grob |
|
466 |
#' \donttest{ |
|
467 |
#' p <- forest_grob(tbl, x, lower, upper, |
|
468 |
#' vline = 1, forest_header = c("A", "B"), |
|
469 |
#' x_at = c(.1, 1, 10), xlim = c(0.1, 10), logx = TRUE, symbol_size = symbol_scale, |
|
470 |
#' vp = grid::plotViewport(margins = c(1, 1, 1, 1)) |
|
471 |
#' ) |
|
472 |
#' |
|
473 |
#' draw_grob(p) |
|
474 |
#' } |
|
475 |
#' |
|
476 |
#' @noRd |
|
477 |
#' @keywords internal |
|
478 |
forest_grob <- function(tbl, |
|
479 |
x, |
|
480 |
lower, |
|
481 |
upper, |
|
482 |
vline, |
|
483 |
forest_header, |
|
484 |
xlim = NULL, |
|
485 |
logx = FALSE, |
|
486 |
x_at = NULL, |
|
487 |
width_row_names = NULL, |
|
488 |
width_columns = NULL, |
|
489 |
width_forest = grid::unit(1, "null"), |
|
490 |
symbol_size = NULL, |
|
491 |
col = "blue", |
|
492 |
name = NULL, |
|
493 |
gp = NULL, |
|
494 |
vp = NULL) { |
|
495 | 1x |
lifecycle::deprecate_warn( |
496 | 1x |
"0.9.4", "forest_grob()", |
497 | 1x |
details = "`g_forest` now generates `ggplot` objects. This function is no longer used within `tern`." |
498 |
) |
|
499 | ||
500 | 1x |
nr <- nrow(tbl) |
501 | 1x |
if (is.null(vline)) { |
502 | ! |
checkmate::assert_true(is.null(forest_header)) |
503 |
} else { |
|
504 | 1x |
checkmate::assert_number(vline) |
505 | 1x |
checkmate::assert_character(forest_header, len = 2, null.ok = TRUE) |
506 |
} |
|
507 | ||
508 | 1x |
checkmate::assert_numeric(x, len = nr) |
509 | 1x |
checkmate::assert_numeric(lower, len = nr) |
510 | 1x |
checkmate::assert_numeric(upper, len = nr) |
511 | 1x |
checkmate::assert_numeric(symbol_size, len = nr, null.ok = TRUE) |
512 | 1x |
checkmate::assert_character(col) |
513 | ||
514 | 1x |
if (is.null(symbol_size)) { |
515 | ! |
symbol_size <- rep(1, nr) |
516 |
} |
|
517 | ||
518 | 1x |
if (is.null(xlim)) { |
519 | ! |
r <- range(c(x, lower, upper), na.rm = TRUE) |
520 | ! |
xlim <- r + c(-0.05, 0.05) * diff(r) |
521 |
} |
|
522 | ||
523 | 1x |
if (logx) { |
524 | 1x |
if (is.null(x_at)) { |
525 | ! |
x_at <- pretty(log(stats::na.omit(c(x, lower, upper)))) |
526 | ! |
x_labels <- exp(x_at) |
527 |
} else { |
|
528 | 1x |
x_labels <- x_at |
529 | 1x |
x_at <- log(x_at) |
530 |
} |
|
531 | 1x |
xlim <- log(xlim) |
532 | 1x |
x <- log(x) |
533 | 1x |
lower <- log(lower) |
534 | 1x |
upper <- log(upper) |
535 | 1x |
if (!is.null(vline)) { |
536 | 1x |
vline <- log(vline) |
537 |
} |
|
538 |
} else { |
|
539 | ! |
x_labels <- TRUE |
540 |
} |
|
541 | ||
542 | 1x |
data_forest_vp <- grid::dataViewport(xlim, c(0, 1)) |
543 | ||
544 |
# Get table content as matrix form. |
|
545 | 1x |
mf <- matrix_form(tbl) |
546 | ||
547 |
# Use `rtables` indent_string eventually. |
|
548 | 1x |
mf$strings[, 1] <- paste0( |
549 | 1x |
strrep(" ", c(rep(0, attr(mf, "nrow_header")), mf$row_info$indent)), |
550 | 1x |
mf$strings[, 1] |
551 |
) |
|
552 | ||
553 | 1x |
n_header <- attr(mf, "nrow_header") |
554 | ||
555 | ! |
if (any(mf$display[, 1] == FALSE)) stop("row names need to be always displayed") |
556 | ||
557 |
# Pre-process the data to be used in lapply and cell_in_rows. |
|
558 | 1x |
to_args_for_cell_in_rows_fun <- function(part = c("body", "header"), |
559 | 1x |
underline_colspan = FALSE) { |
560 | 2x |
part <- match.arg(part) |
561 | 2x |
if (part == "body") { |
562 | 1x |
mat_row_indices <- seq_len(nrow(tbl)) + n_header |
563 | 1x |
row_ind_offset <- -n_header |
564 |
} else { |
|
565 | 1x |
mat_row_indices <- seq_len(n_header) |
566 | 1x |
row_ind_offset <- 0 |
567 |
} |
|
568 | ||
569 | 2x |
lapply(mat_row_indices, function(i) { |
570 | 5x |
disp <- mf$display[i, -1] |
571 | 5x |
list( |
572 | 5x |
row_name = mf$strings[i, 1], |
573 | 5x |
cells = mf$strings[i, -1][disp], |
574 | 5x |
cell_spans = mf$spans[i, -1][disp], |
575 | 5x |
row_index = i + row_ind_offset, |
576 | 5x |
underline_colspan = underline_colspan |
577 |
) |
|
578 |
}) |
|
579 |
} |
|
580 | ||
581 | 1x |
args_header <- to_args_for_cell_in_rows_fun("header", underline_colspan = TRUE) |
582 | 1x |
args_body <- to_args_for_cell_in_rows_fun("body", underline_colspan = FALSE) |
583 | ||
584 | 1x |
grid::gTree( |
585 | 1x |
name = name, |
586 | 1x |
children = grid::gList( |
587 | 1x |
grid::gTree( |
588 | 1x |
children = do.call(grid::gList, lapply(args_header, do.call, what = cell_in_rows)), |
589 | 1x |
vp = grid::vpPath("vp_table_layout", "vp_header") |
590 |
), |
|
591 | 1x |
grid::gTree( |
592 | 1x |
children = do.call(grid::gList, lapply(args_body, do.call, what = cell_in_rows)), |
593 | 1x |
vp = grid::vpPath("vp_table_layout", "vp_body") |
594 |
), |
|
595 | 1x |
grid::linesGrob( |
596 | 1x |
grid::unit(c(0, 1), "npc"), |
597 | 1x |
y = grid::unit(c(.5, .5), "npc"), |
598 | 1x |
vp = grid::vpPath("vp_table_layout", "vp_spacer") |
599 |
), |
|
600 |
# forest part |
|
601 | 1x |
if (is.null(vline)) { |
602 | ! |
NULL |
603 |
} else { |
|
604 | 1x |
grid::gTree( |
605 | 1x |
children = grid::gList( |
606 | 1x |
grid::gTree( |
607 | 1x |
children = grid::gList( |
608 |
# this may overflow, to fix, look here |
|
609 |
# https://stackoverflow.com/questions/33623169/add-multi-line-footnote-to-tablegrob-while-using-gridextra-in-r # nolint |
|
610 | 1x |
grid::textGrob( |
611 | 1x |
forest_header[1], |
612 | 1x |
x = grid::unit(vline, "native") - grid::unit(1, "lines"), |
613 | 1x |
just = c("right", "center") |
614 |
), |
|
615 | 1x |
grid::textGrob( |
616 | 1x |
forest_header[2], |
617 | 1x |
x = grid::unit(vline, "native") + grid::unit(1, "lines"), |
618 | 1x |
just = c("left", "center") |
619 |
) |
|
620 |
), |
|
621 | 1x |
vp = grid::vpStack(grid::viewport(layout.pos.col = ncol(tbl) + 2), data_forest_vp) |
622 |
) |
|
623 |
), |
|
624 | 1x |
vp = grid::vpPath("vp_table_layout", "vp_header") |
625 |
) |
|
626 |
}, |
|
627 | 1x |
grid::gTree( |
628 | 1x |
children = grid::gList( |
629 | 1x |
grid::gTree( |
630 | 1x |
children = grid::gList( |
631 | 1x |
grid::rectGrob(gp = grid::gpar(col = "gray90", fill = "gray90")), |
632 | 1x |
if (is.null(vline)) { |
633 | ! |
NULL |
634 |
} else { |
|
635 | 1x |
grid::linesGrob( |
636 | 1x |
x = grid::unit(rep(vline, 2), "native"), |
637 | 1x |
y = grid::unit(c(0, 1), "npc"), |
638 | 1x |
gp = grid::gpar(lwd = 2), |
639 | 1x |
vp = data_forest_vp |
640 |
) |
|
641 |
}, |
|
642 | 1x |
grid::xaxisGrob(at = x_at, label = x_labels, vp = data_forest_vp) |
643 |
), |
|
644 | 1x |
vp = grid::viewport(layout.pos.col = ncol(tbl) + 2) |
645 |
) |
|
646 |
), |
|
647 | 1x |
vp = grid::vpPath("vp_table_layout", "vp_body") |
648 |
), |
|
649 | 1x |
grid::gTree( |
650 | 1x |
children = do.call( |
651 | 1x |
grid::gList, |
652 | 1x |
Map( |
653 | 1x |
function(xi, li, ui, row_index, size_i, col) { |
654 | 3x |
forest_dot_line( |
655 | 3x |
xi, |
656 | 3x |
li, |
657 | 3x |
ui, |
658 | 3x |
row_index, |
659 | 3x |
xlim, |
660 | 3x |
symbol_size = size_i, |
661 | 3x |
col = col, |
662 | 3x |
datavp = data_forest_vp |
663 |
) |
|
664 |
}, |
|
665 | 1x |
x, |
666 | 1x |
lower, |
667 | 1x |
upper, |
668 | 1x |
seq_along(x), |
669 | 1x |
symbol_size, |
670 | 1x |
col, |
671 | 1x |
USE.NAMES = FALSE |
672 |
) |
|
673 |
), |
|
674 | 1x |
vp = grid::vpPath("vp_table_layout", "vp_body") |
675 |
) |
|
676 |
), |
|
677 | 1x |
childrenvp = forest_viewport(tbl, width_row_names, width_columns, width_forest), |
678 | 1x |
vp = vp, |
679 | 1x |
gp = gp |
680 |
) |
|
681 |
} |
|
682 | ||
683 |
cell_in_rows <- function(row_name, |
|
684 |
cells, |
|
685 |
cell_spans, |
|
686 |
row_index, |
|
687 |
underline_colspan = FALSE) { |
|
688 | 5x |
checkmate::assert_string(row_name) |
689 | 5x |
checkmate::assert_character(cells, min.len = 1, any.missing = FALSE) |
690 | 5x |
checkmate::assert_numeric(cell_spans, len = length(cells), any.missing = FALSE) |
691 | 5x |
checkmate::assert_number(row_index) |
692 | 5x |
checkmate::assert_flag(underline_colspan) |
693 | ||
694 | 5x |
vp_name_rn <- paste0("rowname-", row_index) |
695 | 5x |
g_rowname <- if (!is.null(row_name) && row_name != "") { |
696 | 3x |
grid::textGrob( |
697 | 3x |
name = vp_name_rn, |
698 | 3x |
label = row_name, |
699 | 3x |
x = grid::unit(0, "npc"), |
700 | 3x |
just = c("left", "center"), |
701 | 3x |
vp = grid::vpPath(paste0("rowname-", row_index)) |
702 |
) |
|
703 |
} else { |
|
704 | 2x |
NULL |
705 |
} |
|
706 | ||
707 | 5x |
gl_cols <- if (!(length(cells) > 0)) { |
708 | ! |
list(NULL) |
709 |
} else { |
|
710 | 5x |
j <- 1 # column index of cell |
711 | ||
712 | 5x |
lapply(seq_along(cells), function(k) { |
713 | 19x |
cell_ascii <- cells[[k]] |
714 | 19x |
cs <- cell_spans[[k]] |
715 | ||
716 | 19x |
if (is.na(cell_ascii) || is.null(cell_ascii)) { |
717 | ! |
cell_ascii <- "NA" |
718 |
} |
|
719 | ||
720 | 19x |
cell_name <- paste0("g-cell-", row_index, "-", j) |
721 | ||
722 | 19x |
cell_grobs <- if (identical(cell_ascii, "")) { |
723 | ! |
NULL |
724 |
} else { |
|
725 | 19x |
if (cs == 1) { |
726 | 18x |
grid::textGrob( |
727 | 18x |
label = cell_ascii, |
728 | 18x |
name = cell_name, |
729 | 18x |
vp = grid::vpPath(paste0("cell-", row_index, "-", j)) |
730 |
) |
|
731 |
} else { |
|
732 |
# +1 because of rowname |
|
733 | 1x |
vp_joined_cols <- grid::viewport(layout.pos.row = row_index, layout.pos.col = seq(j + 1, j + cs)) |
734 | ||
735 | 1x |
lab <- grid::textGrob( |
736 | 1x |
label = cell_ascii, |
737 | 1x |
name = cell_name, |
738 | 1x |
vp = vp_joined_cols |
739 |
) |
|
740 | ||
741 | 1x |
if (!underline_colspan || grepl("^[[:space:]]*$", cell_ascii)) { |
742 | ! |
lab |
743 |
} else { |
|
744 | 1x |
grid::gList( |
745 | 1x |
lab, |
746 | 1x |
grid::linesGrob( |
747 | 1x |
x = grid::unit.c(grid::unit(.2, "lines"), grid::unit(1, "npc") - grid::unit(.2, "lines")), |
748 | 1x |
y = grid::unit(c(0, 0), "npc"), |
749 | 1x |
vp = vp_joined_cols |
750 |
) |
|
751 |
) |
|
752 |
} |
|
753 |
} |
|
754 |
} |
|
755 | 19x |
j <<- j + cs |
756 | ||
757 | 19x |
cell_grobs |
758 |
}) |
|
759 |
} |
|
760 | ||
761 | 5x |
grid::gList( |
762 | 5x |
g_rowname, |
763 | 5x |
do.call(grid::gList, gl_cols) |
764 |
) |
|
765 |
} |
|
766 | ||
767 |
#' Graphic object: forest dot line |
|
768 |
#' |
|
769 |
#' @description `r lifecycle::badge("deprecated")` |
|
770 |
#' |
|
771 |
#' Calculate the `grob` corresponding to the dot line within the forest plot. |
|
772 |
#' |
|
773 |
#' @noRd |
|
774 |
#' @keywords internal |
|
775 |
forest_dot_line <- function(x, |
|
776 |
lower, |
|
777 |
upper, |
|
778 |
row_index, |
|
779 |
xlim, |
|
780 |
symbol_size = 1, |
|
781 |
col = "blue", |
|
782 |
datavp) { |
|
783 | 3x |
lifecycle::deprecate_warn( |
784 | 3x |
"0.9.4", "forest_dot_line()", |
785 | 3x |
details = "`g_forest` now generates `ggplot` objects. This function is no longer used within `tern`." |
786 |
) |
|
787 | ||
788 | 3x |
ci <- c(lower, upper) |
789 | 3x |
if (any(!is.na(c(x, ci)))) { |
790 |
# line |
|
791 | 3x |
y <- grid::unit(c(0.5, 0.5), "npc") |
792 | ||
793 | 3x |
g_line <- if (all(!is.na(ci)) && ci[2] > xlim[1] && ci[1] < xlim[2]) { |
794 |
# - |
|
795 | 3x |
if (ci[1] >= xlim[1] && ci[2] <= xlim[2]) { |
796 | 3x |
grid::linesGrob(x = grid::unit(c(ci[1], ci[2]), "native"), y = y) |
797 | ! |
} else if (ci[1] < xlim[1] && ci[2] > xlim[2]) { |
798 |
# <-> |
|
799 | ! |
grid::linesGrob( |
800 | ! |
x = grid::unit(xlim, "native"), |
801 | ! |
y = y, |
802 | ! |
arrow = grid::arrow(angle = 30, length = grid::unit(0.5, "lines"), ends = "both") |
803 |
) |
|
804 | ! |
} else if (ci[1] < xlim[1] && ci[2] <= xlim[2]) { |
805 |
# <- |
|
806 | ! |
grid::linesGrob( |
807 | ! |
x = grid::unit(c(xlim[1], ci[2]), "native"), |
808 | ! |
y = y, |
809 | ! |
arrow = grid::arrow(angle = 30, length = grid::unit(0.5, "lines"), ends = "first") |
810 |
) |
|
811 | ! |
} else if (ci[1] >= xlim[1] && ci[2] > xlim[2]) { |
812 |
# -> |
|
813 | ! |
grid::linesGrob( |
814 | ! |
x = grid::unit(c(ci[1], xlim[2]), "native"), |
815 | ! |
y = y, |
816 | ! |
arrow = grid::arrow(angle = 30, length = grid::unit(0.5, "lines"), ends = "last") |
817 |
) |
|
818 |
} |
|
819 |
} else { |
|
820 | ! |
NULL |
821 |
} |
|
822 | ||
823 | 3x |
g_circle <- if (!is.na(x) && x >= xlim[1] && x <= xlim[2]) { |
824 | 3x |
grid::circleGrob( |
825 | 3x |
x = grid::unit(x, "native"), |
826 | 3x |
y = y, |
827 | 3x |
r = grid::unit(1 / 3.5 * symbol_size, "lines"), |
828 | 3x |
name = "point" |
829 |
) |
|
830 |
} else { |
|
831 | ! |
NULL |
832 |
} |
|
833 | ||
834 | 3x |
grid::gTree( |
835 | 3x |
children = grid::gList( |
836 | 3x |
grid::gTree( |
837 | 3x |
children = grid::gList( |
838 | 3x |
grid::gList( |
839 | 3x |
g_line, |
840 | 3x |
g_circle |
841 |
) |
|
842 |
), |
|
843 | 3x |
vp = datavp, |
844 | 3x |
gp = grid::gpar(col = col, fill = col) |
845 |
) |
|
846 |
), |
|
847 | 3x |
vp = grid::vpPath(paste0("forest-", row_index)) |
848 |
) |
|
849 |
} else { |
|
850 | ! |
NULL |
851 |
} |
|
852 |
} |
|
853 | ||
854 |
#' Create a viewport tree for the forest plot |
|
855 |
#' |
|
856 |
#' @description `r lifecycle::badge("deprecated")` |
|
857 |
#' |
|
858 |
#' @param tbl (`VTableTree`)\cr `rtables` table object. |
|
859 |
#' @param width_row_names (`grid::unit`)\cr width of row names. |
|
860 |
#' @param width_columns (`grid::unit`)\cr width of column spans. |
|
861 |
#' @param width_forest (`grid::unit`)\cr width of the forest plot. |
|
862 |
#' @param gap_column (`grid::unit`)\cr gap width between the columns. |
|
863 |
#' @param gap_header (`grid::unit`)\cr gap width between the header. |
|
864 |
#' @param mat_form (`MatrixPrintForm`)\cr matrix print form of the table. |
|
865 |
#' |
|
866 |
#' @return A viewport tree. |
|
867 |
#' |
|
868 |
#' @examples |
|
869 |
#' library(grid) |
|
870 |
#' |
|
871 |
#' tbl <- rtable( |
|
872 |
#' header = rheader( |
|
873 |
#' rrow("", "E", rcell("CI", colspan = 2)), |
|
874 |
#' rrow("", "A", "B", "C") |
|
875 |
#' ), |
|
876 |
#' rrow("row 1", 1, 0.8, 1.1), |
|
877 |
#' rrow("row 2", 1.4, 0.8, 1.6), |
|
878 |
#' rrow("row 3", 1.2, 0.8, 1.2) |
|
879 |
#' ) |
|
880 |
#' |
|
881 |
#' \donttest{ |
|
882 |
#' v <- forest_viewport(tbl) |
|
883 |
#' |
|
884 |
#' grid::grid.newpage() |
|
885 |
#' showViewport(v) |
|
886 |
#' } |
|
887 |
#' |
|
888 |
#' @export |
|
889 |
forest_viewport <- function(tbl, |
|
890 |
width_row_names = NULL, |
|
891 |
width_columns = NULL, |
|
892 |
width_forest = grid::unit(1, "null"), |
|
893 |
gap_column = grid::unit(1, "lines"), |
|
894 |
gap_header = grid::unit(1, "lines"), |
|
895 |
mat_form = NULL) { |
|
896 | 2x |
lifecycle::deprecate_warn( |
897 | 2x |
"0.9.4", |
898 | 2x |
"forest_viewport()", |
899 | 2x |
details = "`g_forest` now generates `ggplot` objects. This function is no longer used within `tern`." |
900 |
) |
|
901 | ||
902 | 2x |
checkmate::assert_class(tbl, "VTableTree") |
903 | 2x |
checkmate::assert_true(grid::is.unit(width_forest)) |
904 | 2x |
if (!is.null(width_row_names)) { |
905 | ! |
checkmate::assert_true(grid::is.unit(width_row_names)) |
906 |
} |
|
907 | 2x |
if (!is.null(width_columns)) { |
908 | ! |
checkmate::assert_true(grid::is.unit(width_columns)) |
909 |
} |
|
910 | ||
911 | 2x |
if (is.null(mat_form)) mat_form <- matrix_form(tbl) |
912 | ||
913 | 2x |
mat_form$strings[!mat_form$display] <- "" |
914 | ||
915 | 2x |
nr <- nrow(tbl) |
916 | 2x |
nc <- ncol(tbl) |
917 | 2x |
nr_h <- attr(mat_form, "nrow_header") |
918 | ||
919 | 2x |
if (is.null(width_row_names) || is.null(width_columns)) { |
920 | 2x |
tbl_widths <- formatters::propose_column_widths(mat_form) |
921 | 2x |
strs_with_width <- strrep("x", tbl_widths) # that works for mono spaced fonts |
922 | 2x |
if (is.null(width_row_names)) width_row_names <- grid::stringWidth(strs_with_width[1]) |
923 | 2x |
if (is.null(width_columns)) width_columns <- grid::stringWidth(strs_with_width[-1]) |
924 |
} |
|
925 | ||
926 |
# Widths for row name, cols, forest. |
|
927 | 2x |
widths <- grid::unit.c( |
928 | 2x |
width_row_names + gap_column, |
929 | 2x |
width_columns + gap_column, |
930 | 2x |
width_forest |
931 |
) |
|
932 | ||
933 | 2x |
n_lines_per_row <- apply( |
934 | 2x |
X = mat_form$strings, |
935 | 2x |
MARGIN = 1, |
936 | 2x |
FUN = function(row) { |
937 | 10x |
tmp <- vapply( |
938 | 10x |
gregexpr("\n", row, fixed = TRUE), |
939 | 10x |
attr, numeric(1), |
940 | 10x |
"match.length" |
941 | 10x |
) + 1 |
942 | 10x |
max(c(tmp, 1)) |
943 |
} |
|
944 |
) |
|
945 | ||
946 | 2x |
i_header <- seq_len(nr_h) |
947 | ||
948 | 2x |
height_body_rows <- grid::unit(n_lines_per_row[-i_header] * 1.2, "lines") |
949 | 2x |
height_header_rows <- grid::unit(n_lines_per_row[i_header] * 1.2, "lines") |
950 | ||
951 | 2x |
height_body <- grid::unit(sum(n_lines_per_row[-i_header]) * 1.2, "lines") |
952 | 2x |
height_header <- grid::unit(sum(n_lines_per_row[i_header]) * 1.2, "lines") |
953 | ||
954 | 2x |
nc_g <- nc + 2 # number of columns incl. row names and forest |
955 | ||
956 | 2x |
vp_tbl <- grid::vpTree( |
957 | 2x |
parent = grid::viewport( |
958 | 2x |
name = "vp_table_layout", |
959 | 2x |
layout = grid::grid.layout( |
960 | 2x |
nrow = 3, ncol = 1, |
961 | 2x |
heights = grid::unit.c(height_header, gap_header, height_body) |
962 |
) |
|
963 |
), |
|
964 | 2x |
children = grid::vpList( |
965 | 2x |
vp_forest_table_part(nr_h, nc_g, 1, 1, widths, height_header_rows, "vp_header"), |
966 | 2x |
vp_forest_table_part(nr, nc_g, 3, 1, widths, height_body_rows, "vp_body"), |
967 | 2x |
grid::viewport(name = "vp_spacer", layout.pos.row = 2, layout.pos.col = 1) |
968 |
) |
|
969 |
) |
|
970 | 2x |
vp_tbl |
971 |
} |
|
972 | ||
973 |
#' Viewport forest plot: table part |
|
974 |
#' |
|
975 |
#' @description `r lifecycle::badge("deprecated")` |
|
976 |
#' |
|
977 |
#' Prepares a viewport for the table included in the forest plot. |
|
978 |
#' |
|
979 |
#' @noRd |
|
980 |
#' @keywords internal |
|
981 |
vp_forest_table_part <- function(nrow, |
|
982 |
ncol, |
|
983 |
l_row, |
|
984 |
l_col, |
|
985 |
widths, |
|
986 |
heights, |
|
987 |
name) { |
|
988 | 4x |
lifecycle::deprecate_warn( |
989 | 4x |
"0.9.4", "vp_forest_table_part()", |
990 | 4x |
details = "`g_forest` now generates `ggplot` objects. This function is no longer used within `tern`." |
991 |
) |
|
992 | ||
993 | 4x |
grid::vpTree( |
994 | 4x |
grid::viewport( |
995 | 4x |
name = name, |
996 | 4x |
layout.pos.row = l_row, |
997 | 4x |
layout.pos.col = l_col, |
998 | 4x |
layout = grid::grid.layout(nrow = nrow, ncol = ncol, widths = widths, heights = heights) |
999 |
), |
|
1000 | 4x |
children = grid::vpList( |
1001 | 4x |
do.call( |
1002 | 4x |
grid::vpList, |
1003 | 4x |
lapply( |
1004 | 4x |
seq_len(nrow), function(i) { |
1005 | 10x |
grid::viewport(layout.pos.row = i, layout.pos.col = 1, name = paste0("rowname-", i)) |
1006 |
} |
|
1007 |
) |
|
1008 |
), |
|
1009 | 4x |
do.call( |
1010 | 4x |
grid::vpList, |
1011 | 4x |
apply( |
1012 | 4x |
expand.grid(seq_len(nrow), seq_len(ncol - 2)), |
1013 | 4x |
1, |
1014 | 4x |
function(x) { |
1015 | 35x |
i <- x[1] |
1016 | 35x |
j <- x[2] |
1017 | 35x |
grid::viewport(layout.pos.row = i, layout.pos.col = j + 1, name = paste0("cell-", i, "-", j)) |
1018 |
} |
|
1019 |
) |
|
1020 |
), |
|
1021 | 4x |
do.call( |
1022 | 4x |
grid::vpList, |
1023 | 4x |
lapply( |
1024 | 4x |
seq_len(nrow), |
1025 | 4x |
function(i) { |
1026 | 10x |
grid::viewport(layout.pos.row = i, layout.pos.col = ncol, name = paste0("forest-", i)) |
1027 |
} |
|
1028 |
) |
|
1029 |
) |
|
1030 |
) |
|
1031 |
) |
|
1032 |
} |
|
1033 | ||
1034 |
#' Forest rendering |
|
1035 |
#' |
|
1036 |
#' @description `r lifecycle::badge("deprecated")` |
|
1037 |
#' |
|
1038 |
#' Renders the forest grob. |
|
1039 |
#' |
|
1040 |
#' @noRd |
|
1041 |
#' @keywords internal |
|
1042 |
grid.forest <- function(...) { # nolint |
|
1043 | ! |
lifecycle::deprecate_warn( |
1044 | ! |
"0.9.4", "grid.forest()", |
1045 | ! |
details = "`g_forest` now generates `ggplot` objects. This function is no longer used within `tern`." |
1046 |
) |
|
1047 | ||
1048 | ! |
grid::grid.draw(forest_grob(...)) |
1049 |
} |
1 |
#' Univariate formula special term |
|
2 |
#' |
|
3 |
#' @description `r lifecycle::badge("stable")` |
|
4 |
#' |
|
5 |
#' The special term `univariate` indicate that the model should be fitted individually for |
|
6 |
#' every variable included in univariate. |
|
7 |
#' |
|
8 |
#' @param x (`character`)\cr a vector of variable names separated by commas. |
|
9 |
#' |
|
10 |
#' @return When used within a model formula, produces univariate models for each variable provided. |
|
11 |
#' |
|
12 |
#' @details |
|
13 |
#' If provided alongside with pairwise specification, the model |
|
14 |
#' `y ~ ARM + univariate(SEX, AGE, RACE)` lead to the study and comparison of the models |
|
15 |
#' + `y ~ ARM` |
|
16 |
#' + `y ~ ARM + SEX` |
|
17 |
#' + `y ~ ARM + AGE` |
|
18 |
#' + `y ~ ARM + RACE` |
|
19 |
#' |
|
20 |
#' @export |
|
21 |
univariate <- function(x) { |
|
22 | 1x |
structure(x, varname = deparse(substitute(x))) |
23 |
} |
|
24 | ||
25 |
# Get the right-hand-term of a formula |
|
26 |
rht <- function(x) { |
|
27 | 4x |
checkmate::assert_formula(x) |
28 | 4x |
y <- as.character(rev(x)[[1]]) |
29 | 4x |
return(y) |
30 |
} |
|
31 | ||
32 |
#' Hazard ratio estimation in interactions |
|
33 |
#' |
|
34 |
#' This function estimates the hazard ratios between arms when an interaction variable is given with |
|
35 |
#' specific values. |
|
36 |
#' |
|
37 |
#' @param variable,given (`character(2)`)\cr names of the two variables in the interaction. We seek the estimation of |
|
38 |
#' the levels of `variable` given the levels of `given`. |
|
39 |
#' @param lvl_var,lvl_given (`character`)\cr corresponding levels given by [levels()]. |
|
40 |
#' @param mmat (named `numeric`) a vector filled with `0`s used as a template to obtain the design matrix. |
|
41 |
#' @param coef (`numeric`)\cr vector of estimated coefficients. |
|
42 |
#' @param vcov (`matrix`)\cr variance-covariance matrix of underlying model. |
|
43 |
#' @param conf_level (`proportion`)\cr confidence level of estimate intervals. |
|
44 |
#' |
|
45 |
#' @details Given the cox regression investigating the effect of Arm (A, B, C; reference A) |
|
46 |
#' and Sex (F, M; reference Female). The model is abbreviated: y ~ Arm + Sex + Arm x Sex. |
|
47 |
#' The cox regression estimates the coefficients along with a variance-covariance matrix for: |
|
48 |
#' |
|
49 |
#' - b1 (arm b), b2 (arm c) |
|
50 |
#' - b3 (sex m) |
|
51 |
#' - b4 (arm b: sex m), b5 (arm c: sex m) |
|
52 |
#' |
|
53 |
#' Given that I want an estimation of the Hazard Ratio for arm C/sex M, the estimation |
|
54 |
#' will be given in reference to arm A/Sex M by exp(b2 + b3 + b5)/ exp(b3) = exp(b2 + b5), |
|
55 |
#' therefore the interaction coefficient is given by b2 + b5 while the standard error is obtained |
|
56 |
#' as $1.96 * sqrt(Var b2 + Var b5 + 2 * covariance (b2,b5))$ for a confidence level of 0.95. |
|
57 |
#' |
|
58 |
#' @return A list of matrices (one per level of variable) with rows corresponding to the combinations of |
|
59 |
#' `variable` and `given`, with columns: |
|
60 |
#' * `coef_hat`: Estimation of the coefficient. |
|
61 |
#' * `coef_se`: Standard error of the estimation. |
|
62 |
#' * `hr`: Hazard ratio. |
|
63 |
#' * `lcl, ucl`: Lower/upper confidence limit of the hazard ratio. |
|
64 |
#' |
|
65 |
#' @seealso [s_cox_multivariate()]. |
|
66 |
#' |
|
67 |
#' @examples |
|
68 |
#' library(dplyr) |
|
69 |
#' library(survival) |
|
70 |
#' |
|
71 |
#' ADSL <- tern_ex_adsl %>% |
|
72 |
#' filter(SEX %in% c("F", "M")) |
|
73 |
#' |
|
74 |
#' adtte <- tern_ex_adtte %>% filter(PARAMCD == "PFS") |
|
75 |
#' adtte$ARMCD <- droplevels(adtte$ARMCD) |
|
76 |
#' adtte$SEX <- droplevels(adtte$SEX) |
|
77 |
#' |
|
78 |
#' mod <- coxph( |
|
79 |
#' formula = Surv(time = AVAL, event = 1 - CNSR) ~ (SEX + ARMCD)^2, |
|
80 |
#' data = adtte |
|
81 |
#' ) |
|
82 |
#' |
|
83 |
#' mmat <- stats::model.matrix(mod)[1, ] |
|
84 |
#' mmat[!mmat == 0] <- 0 |
|
85 |
#' |
|
86 |
#' @keywords internal |
|
87 |
estimate_coef <- function(variable, given, |
|
88 |
lvl_var, lvl_given, |
|
89 |
coef, |
|
90 |
mmat, |
|
91 |
vcov, |
|
92 |
conf_level = 0.95) { |
|
93 | 8x |
var_lvl <- paste0(variable, lvl_var[-1]) # [-1]: reference level |
94 | 8x |
giv_lvl <- paste0(given, lvl_given) |
95 | ||
96 | 8x |
design_mat <- expand.grid(variable = var_lvl, given = giv_lvl) |
97 | 8x |
design_mat <- design_mat[order(design_mat$variable, design_mat$given), ] |
98 | 8x |
design_mat <- within( |
99 | 8x |
data = design_mat, |
100 | 8x |
expr = { |
101 | 8x |
inter <- paste0(variable, ":", given) |
102 | 8x |
rev_inter <- paste0(given, ":", variable) |
103 |
} |
|
104 |
) |
|
105 | ||
106 | 8x |
split_by_variable <- design_mat$variable |
107 | 8x |
interaction_names <- paste(design_mat$variable, design_mat$given, sep = "/") |
108 | ||
109 | 8x |
design_mat <- apply( |
110 | 8x |
X = design_mat, MARGIN = 1, FUN = function(x) { |
111 | 27x |
mmat[names(mmat) %in% x[-which(names(x) == "given")]] <- 1 |
112 | 27x |
return(mmat) |
113 |
} |
|
114 |
) |
|
115 | 8x |
colnames(design_mat) <- interaction_names |
116 | ||
117 | 8x |
betas <- as.matrix(coef) |
118 | ||
119 | 8x |
coef_hat <- t(design_mat) %*% betas |
120 | 8x |
dimnames(coef_hat)[2] <- "coef" |
121 | ||
122 | 8x |
coef_se <- apply(design_mat, 2, function(x) { |
123 | 27x |
vcov_el <- as.logical(x) |
124 | 27x |
y <- vcov[vcov_el, vcov_el] |
125 | 27x |
y <- sum(y) |
126 | 27x |
y <- sqrt(y) |
127 | 27x |
return(y) |
128 |
}) |
|
129 | ||
130 | 8x |
q_norm <- stats::qnorm((1 + conf_level) / 2) |
131 | 8x |
y <- cbind(coef_hat, `se(coef)` = coef_se) |
132 | ||
133 | 8x |
y <- apply(y, 1, function(x) { |
134 | 27x |
x["hr"] <- exp(x["coef"]) |
135 | 27x |
x["lcl"] <- exp(x["coef"] - q_norm * x["se(coef)"]) |
136 | 27x |
x["ucl"] <- exp(x["coef"] + q_norm * x["se(coef)"]) |
137 | ||
138 | 27x |
return(x) |
139 |
}) |
|
140 | ||
141 | 8x |
y <- t(y) |
142 | 8x |
y <- by(y, split_by_variable, identity) |
143 | 8x |
y <- lapply(y, as.matrix) |
144 | ||
145 | 8x |
attr(y, "details") <- paste0( |
146 | 8x |
"Estimations of ", variable, |
147 | 8x |
" hazard ratio given the level of ", given, " compared to ", |
148 | 8x |
variable, " level ", lvl_var[1], "." |
149 |
) |
|
150 | 8x |
return(y) |
151 |
} |
|
152 | ||
153 |
#' `tryCatch` around `car::Anova` |
|
154 |
#' |
|
155 |
#' Captures warnings when executing [car::Anova]. |
|
156 |
#' |
|
157 |
#' @inheritParams car::Anova |
|
158 |
#' |
|
159 |
#' @return A list with item `aov` for the result of the model and `error_text` for the captured warnings. |
|
160 |
#' |
|
161 |
#' @examples |
|
162 |
#' # `car::Anova` on cox regression model including strata and expected |
|
163 |
#' # a likelihood ratio test triggers a warning as only Wald method is |
|
164 |
#' # accepted. |
|
165 |
#' |
|
166 |
#' library(survival) |
|
167 |
#' |
|
168 |
#' mod <- coxph( |
|
169 |
#' formula = Surv(time = futime, event = fustat) ~ factor(rx) + strata(ecog.ps), |
|
170 |
#' data = ovarian |
|
171 |
#' ) |
|
172 |
#' |
|
173 |
#' @keywords internal |
|
174 |
try_car_anova <- function(mod, |
|
175 |
test.statistic) { # nolint |
|
176 | 2x |
y <- tryCatch( |
177 | 2x |
withCallingHandlers( |
178 | 2x |
expr = { |
179 | 2x |
warn_text <- c() |
180 | 2x |
list( |
181 | 2x |
aov = car::Anova( |
182 | 2x |
mod, |
183 | 2x |
test.statistic = test.statistic, |
184 | 2x |
type = "III" |
185 |
), |
|
186 | 2x |
warn_text = warn_text |
187 |
) |
|
188 |
}, |
|
189 | 2x |
warning = function(w) { |
190 |
# If a warning is detected it is handled as "w". |
|
191 | ! |
warn_text <<- trimws(paste0("Warning in `try_car_anova`: ", w)) |
192 | ||
193 |
# A warning is sometimes expected, then, we want to restart |
|
194 |
# the execution while ignoring the warning. |
|
195 | ! |
invokeRestart("muffleWarning") |
196 |
} |
|
197 |
), |
|
198 | 2x |
finally = { |
199 |
} |
|
200 |
) |
|
201 | ||
202 | 2x |
return(y) |
203 |
} |
|
204 | ||
205 |
#' Fit a Cox regression model and ANOVA |
|
206 |
#' |
|
207 |
#' The functions derives the effect p-values using [car::Anova()] from [survival::coxph()] results. |
|
208 |
#' |
|
209 |
#' @inheritParams t_coxreg |
|
210 |
#' |
|
211 |
#' @return A list with items `mod` (results of [survival::coxph()]), `msum` (result of `summary`) and |
|
212 |
#' `aov` (result of [car::Anova()]). |
|
213 |
#' |
|
214 |
#' @noRd |
|
215 |
fit_n_aov <- function(formula, |
|
216 |
data = data, |
|
217 |
conf_level = conf_level, |
|
218 |
pval_method = c("wald", "likelihood"), |
|
219 |
...) { |
|
220 | 1x |
pval_method <- match.arg(pval_method) |
221 | ||
222 | 1x |
environment(formula) <- environment() |
223 | 1x |
suppressWarnings({ |
224 |
# We expect some warnings due to coxph which fails strict programming. |
|
225 | 1x |
mod <- survival::coxph(formula, data = data, ...) |
226 | 1x |
msum <- summary(mod, conf.int = conf_level) |
227 |
}) |
|
228 | ||
229 | 1x |
aov <- try_car_anova( |
230 | 1x |
mod, |
231 | 1x |
test.statistic = switch(pval_method, |
232 | 1x |
"wald" = "Wald", |
233 | 1x |
"likelihood" = "LR" |
234 |
) |
|
235 |
) |
|
236 | ||
237 | 1x |
warn_attr <- aov$warn_text |
238 | ! |
if (!is.null(aov$warn_text)) message(warn_attr) |
239 | ||
240 | 1x |
aov <- aov$aov |
241 | 1x |
y <- list(mod = mod, msum = msum, aov = aov) |
242 | 1x |
attr(y, "message") <- warn_attr |
243 | ||
244 | 1x |
return(y) |
245 |
} |
|
246 | ||
247 |
# argument_checks |
|
248 |
check_formula <- function(formula) { |
|
249 | 1x |
if (!(inherits(formula, "formula"))) { |
250 | 1x |
stop("Check `formula`. A formula should resemble `Surv(time = AVAL, event = 1 - CNSR) ~ study_arm(ARMCD)`.") |
251 |
} |
|
252 | ||
253 | ! |
invisible() |
254 |
} |
|
255 | ||
256 |
check_covariate_formulas <- function(covariates) { |
|
257 | 1x |
if (!all(vapply(X = covariates, FUN = inherits, what = "formula", FUN.VALUE = TRUE)) || is.null(covariates)) { |
258 | 1x |
stop("Check `covariates`, it should be a list of right-hand-term formulas, e.g. list(Age = ~AGE).") |
259 |
} |
|
260 | ||
261 | ! |
invisible() |
262 |
} |
|
263 | ||
264 |
name_covariate_names <- function(covariates) { |
|
265 | 1x |
miss_names <- names(covariates) == "" |
266 | 1x |
no_names <- is.null(names(covariates)) |
267 | ! |
if (any(miss_names)) names(covariates)[miss_names] <- vapply(covariates[miss_names], FUN = rht, FUN.VALUE = "name") |
268 | ! |
if (no_names) names(covariates) <- vapply(covariates, FUN = rht, FUN.VALUE = "name") |
269 | 1x |
return(covariates) |
270 |
} |
|
271 | ||
272 |
check_increments <- function(increments, covariates) { |
|
273 | 1x |
if (!is.null(increments)) { |
274 | 1x |
covariates <- vapply(covariates, FUN = rht, FUN.VALUE = "name") |
275 | 1x |
lapply( |
276 | 1x |
X = names(increments), FUN = function(x) { |
277 | 3x |
if (!x %in% covariates) { |
278 | 1x |
warning( |
279 | 1x |
paste( |
280 | 1x |
"Check `increments`, the `increment` for ", x, |
281 | 1x |
"doesn't match any names in investigated covariate(s)." |
282 |
) |
|
283 |
) |
|
284 |
} |
|
285 |
} |
|
286 |
) |
|
287 |
} |
|
288 | ||
289 | 1x |
invisible() |
290 |
} |
|
291 | ||
292 |
#' Multivariate Cox model - summarized results |
|
293 |
#' |
|
294 |
#' Analyses based on multivariate Cox model are usually not performed for the Controlled Substance Reporting or |
|
295 |
#' regulatory documents but serve exploratory purposes only (e.g., for publication). In practice, the model usually |
|
296 |
#' includes only the main effects (without interaction terms). It produces the hazard ratio estimates for each of the |
|
297 |
#' covariates included in the model. |
|
298 |
#' The analysis follows the same principles (e.g., stratified vs. unstratified analysis and tie handling) as the |
|
299 |
#' usual Cox model analysis. Since there is usually no pre-specified hypothesis testing for such analysis, |
|
300 |
#' the p.values need to be interpreted with caution. (**Statistical Analysis of Clinical Trials Data with R**, |
|
301 |
#' `NEST's bookdown`) |
|
302 |
#' |
|
303 |
#' @param formula (`formula`)\cr a formula corresponding to the investigated [survival::Surv()] survival model |
|
304 |
#' including covariates. |
|
305 |
#' @param data (`data.frame`)\cr a data frame which includes the variable in formula and covariates. |
|
306 |
#' @param conf_level (`proportion`)\cr the confidence level for the hazard ratio interval estimations. Default is 0.95. |
|
307 |
#' @param pval_method (`string`)\cr the method used for the estimation of p-values, should be one of |
|
308 |
#' `"wald"` (default) or `"likelihood"`. |
|
309 |
#' @param ... optional parameters passed to [survival::coxph()]. Can include `ties`, a character string specifying the |
|
310 |
#' method for tie handling, one of `exact` (default), `efron`, `breslow`. |
|
311 |
#' |
|
312 |
#' @return A `list` with elements `mod`, `msum`, `aov`, and `coef_inter`. |
|
313 |
#' |
|
314 |
#' @details The output is limited to single effect terms. Work in ongoing for estimation of interaction terms |
|
315 |
#' but is out of scope as defined by the Global Data Standards Repository |
|
316 |
#' (**`GDS_Standard_TLG_Specs_Tables_2.doc`**). |
|
317 |
#' |
|
318 |
#' @seealso [estimate_coef()]. |
|
319 |
#' |
|
320 |
#' @examples |
|
321 |
#' library(dplyr) |
|
322 |
#' |
|
323 |
#' adtte <- tern_ex_adtte |
|
324 |
#' adtte_f <- subset(adtte, PARAMCD == "OS") # _f: filtered |
|
325 |
#' adtte_f <- filter( |
|
326 |
#' adtte_f, |
|
327 |
#' PARAMCD == "OS" & |
|
328 |
#' SEX %in% c("F", "M") & |
|
329 |
#' RACE %in% c("ASIAN", "BLACK OR AFRICAN AMERICAN", "WHITE") |
|
330 |
#' ) |
|
331 |
#' adtte_f$SEX <- droplevels(adtte_f$SEX) |
|
332 |
#' adtte_f$RACE <- droplevels(adtte_f$RACE) |
|
333 |
#' |
|
334 |
#' @keywords internal |
|
335 |
s_cox_multivariate <- function(formula, data, |
|
336 |
conf_level = 0.95, |
|
337 |
pval_method = c("wald", "likelihood"), |
|
338 |
...) { |
|
339 | 1x |
tf <- stats::terms(formula, specials = c("strata")) |
340 | 1x |
covariates <- rownames(attr(tf, "factors"))[-c(1, unlist(attr(tf, "specials")))] |
341 | 1x |
lapply( |
342 | 1x |
X = covariates, |
343 | 1x |
FUN = function(x) { |
344 | 3x |
if (is.character(data[[x]])) { |
345 | 1x |
data[[x]] <<- as.factor(data[[x]]) |
346 |
} |
|
347 | 3x |
invisible() |
348 |
} |
|
349 |
) |
|
350 | 1x |
pval_method <- match.arg(pval_method) |
351 | ||
352 |
# Results directly exported from environment(fit_n_aov) to environment(s_function_draft) |
|
353 | 1x |
y <- fit_n_aov( |
354 | 1x |
formula = formula, |
355 | 1x |
data = data, |
356 | 1x |
conf_level = conf_level, |
357 | 1x |
pval_method = pval_method, |
358 |
... |
|
359 |
) |
|
360 | 1x |
mod <- y$mod |
361 | 1x |
aov <- y$aov |
362 | 1x |
msum <- y$msum |
363 | 1x |
list2env(as.list(y), environment()) |
364 | ||
365 | 1x |
all_term_labs <- attr(mod$terms, "term.labels") |
366 | 1x |
term_labs <- all_term_labs[which(attr(mod$terms, "order") == 1)] |
367 | 1x |
names(term_labs) <- term_labs |
368 | ||
369 | 1x |
coef_inter <- NULL |
370 | 1x |
if (any(attr(mod$terms, "order") > 1)) { |
371 | 1x |
for_inter <- all_term_labs[attr(mod$terms, "order") > 1] |
372 | 1x |
names(for_inter) <- for_inter |
373 | 1x |
mmat <- stats::model.matrix(mod)[1, ] |
374 | 1x |
mmat[!mmat == 0] <- 0 |
375 | 1x |
mcoef <- stats::coef(mod) |
376 | 1x |
mvcov <- stats::vcov(mod) |
377 | ||
378 | 1x |
estimate_coef_local <- function(variable, given) { |
379 | 6x |
estimate_coef( |
380 | 6x |
variable, given, |
381 | 6x |
coef = mcoef, mmat = mmat, vcov = mvcov, conf_level = conf_level, |
382 | 6x |
lvl_var = levels(data[[variable]]), lvl_given = levels(data[[given]]) |
383 |
) |
|
384 |
} |
|
385 | ||
386 | 1x |
coef_inter <- lapply( |
387 | 1x |
for_inter, function(x) { |
388 | 3x |
y <- attr(mod$terms, "factors")[, x] |
389 | 3x |
y <- names(y[y > 0]) |
390 | 3x |
Map(estimate_coef_local, variable = y, given = rev(y)) |
391 |
} |
|
392 |
) |
|
393 |
} |
|
394 | ||
395 | 1x |
list(mod = mod, msum = msum, aov = aov, coef_inter = coef_inter) |
396 |
} |
1 |
#' Count patients with marked laboratory abnormalities |
|
2 |
#' |
|
3 |
#' @description `r lifecycle::badge("stable")` |
|
4 |
#' |
|
5 |
#' The analyze function [count_abnormal_by_marked()] creates a layout element to count patients with marked laboratory |
|
6 |
#' abnormalities for each direction of abnormality, categorized by parameter value. |
|
7 |
#' |
|
8 |
#' This function analyzes primary analysis variable `var` which indicates whether a single, replicated, |
|
9 |
#' or last marked laboratory abnormality was observed. Levels of `var` to include for each marked lab |
|
10 |
#' abnormality (`single` and `last_replicated`) can be supplied via the `category` parameter. Additional |
|
11 |
#' analysis variables that can be supplied as a list via the `variables` parameter are `id` (defaults |
|
12 |
#' to `USUBJID`), a variable to indicate unique subject identifiers, `param` (defaults to `PARAM`), a |
|
13 |
#' variable to indicate parameter values, and `direction` (defaults to `abn_dir`), a variable to indicate |
|
14 |
#' abnormality directions. |
|
15 |
#' |
|
16 |
#' For each combination of `param` and `direction` levels, marked lab abnormality counts are calculated |
|
17 |
#' as follows: |
|
18 |
#' * `Single, not last` & `Last or replicated`: The number of patients with `Single, not last` |
|
19 |
#' and `Last or replicated` values, respectively. |
|
20 |
#' * `Any`: The number of patients with either single or replicated marked abnormalities. |
|
21 |
#' |
|
22 |
#' Fractions are calculated by dividing the above counts by the number of patients with at least one |
|
23 |
#' valid measurement recorded during the analysis. |
|
24 |
#' |
|
25 |
#' Prior to using this function in your table layout you must use [rtables::split_rows_by()] to create two |
|
26 |
#' row splits, one on variable `param` and one on variable `direction`. |
|
27 |
#' |
|
28 |
#' @inheritParams argument_convention |
|
29 |
#' @param category (`list`)\cr a list with different marked category names for single |
|
30 |
#' and last or replicated. |
|
31 |
#' @param .stats (`character`)\cr statistics to select for the table. Run `get_stats("abnormal_by_marked")` |
|
32 |
#' to see available statistics for this function. |
|
33 |
#' |
|
34 |
#' @note `Single, not last` and `Last or replicated` levels are mutually exclusive. If a patient has |
|
35 |
#' abnormalities that meet both the `Single, not last` and `Last or replicated` criteria, then the |
|
36 |
#' patient will be counted only under the `Last or replicated` category. |
|
37 |
#' |
|
38 |
#' @name abnormal_by_marked |
|
39 |
#' @order 1 |
|
40 |
NULL |
|
41 | ||
42 |
#' @describeIn abnormal_by_marked Statistics function for patients with marked lab abnormalities. |
|
43 |
#' |
|
44 |
#' @return |
|
45 |
#' * `s_count_abnormal_by_marked()` returns statistic `count_fraction` with `Single, not last`, |
|
46 |
#' `Last or replicated`, and `Any` results. |
|
47 |
#' |
|
48 |
#' @keywords internal |
|
49 |
s_count_abnormal_by_marked <- function(df, |
|
50 |
.var = "AVALCAT1", |
|
51 |
.spl_context, |
|
52 |
category = list(single = "SINGLE", last_replicated = c("LAST", "REPLICATED")), |
|
53 |
variables = list(id = "USUBJID", param = "PARAM", direction = "abn_dir")) { |
|
54 | 3x |
checkmate::assert_string(.var) |
55 | 3x |
checkmate::assert_list(variables) |
56 | 3x |
checkmate::assert_list(category) |
57 | 3x |
checkmate::assert_subset(names(category), c("single", "last_replicated")) |
58 | 3x |
checkmate::assert_subset(names(variables), c("id", "param", "direction")) |
59 | 3x |
checkmate::assert_vector(unique(df[[variables$direction]]), max.len = 1) |
60 | ||
61 | 2x |
assert_df_with_variables(df, c(aval = .var, variables)) |
62 | 2x |
checkmate::assert_multi_class(df[[.var]], classes = c("factor", "character")) |
63 | 2x |
checkmate::assert_multi_class(df[[variables$id]], classes = c("factor", "character")) |
64 | ||
65 | ||
66 | 2x |
first_row <- .spl_context[.spl_context$split == variables[["param"]], ] |
67 |
# Patients in the denominator have at least one post-baseline visit. |
|
68 | 2x |
subj <- first_row$full_parent_df[[1]][[variables[["id"]]]] |
69 | 2x |
subj_cur_col <- subj[first_row$cur_col_subset[[1]]] |
70 |
# Some subjects may have a record for high and low directions but |
|
71 |
# should be counted only once. |
|
72 | 2x |
denom <- length(unique(subj_cur_col)) |
73 | ||
74 | 2x |
if (denom != 0) { |
75 | 2x |
subjects_last_replicated <- unique( |
76 | 2x |
df[df[[.var]] %in% category[["last_replicated"]], variables$id, drop = TRUE] |
77 |
) |
|
78 | 2x |
subjects_single <- unique( |
79 | 2x |
df[df[[.var]] %in% category[["single"]], variables$id, drop = TRUE] |
80 |
) |
|
81 |
# Subjects who have both single and last/replicated abnormalities are counted in only the last/replicated group. |
|
82 | 2x |
subjects_single <- setdiff(subjects_single, subjects_last_replicated) |
83 | 2x |
n_single <- length(subjects_single) |
84 | 2x |
n_last_replicated <- length(subjects_last_replicated) |
85 | 2x |
n_any <- n_single + n_last_replicated |
86 | 2x |
result <- list(count_fraction = list( |
87 | 2x |
"Single, not last" = c(n_single, n_single / denom), |
88 | 2x |
"Last or replicated" = c(n_last_replicated, n_last_replicated / denom), |
89 | 2x |
"Any Abnormality" = c(n_any, n_any / denom) |
90 |
)) |
|
91 |
} else { |
|
92 | ! |
result <- list(count_fraction = list( |
93 | ! |
"Single, not last" = c(0, 0), |
94 | ! |
"Last or replicated" = c(0, 0), |
95 | ! |
"Any Abnormality" = c(0, 0) |
96 |
)) |
|
97 |
} |
|
98 | ||
99 | 2x |
result |
100 |
} |
|
101 | ||
102 |
#' @describeIn abnormal_by_marked Formatted analysis function which is used as `afun` |
|
103 |
#' in `count_abnormal_by_marked()`. |
|
104 |
#' |
|
105 |
#' @return |
|
106 |
#' * `a_count_abnormal_by_marked()` returns the corresponding list with formatted [rtables::CellValue()]. |
|
107 |
#' |
|
108 |
#' @keywords internal |
|
109 |
a_count_abnormal_by_marked <- make_afun( |
|
110 |
s_count_abnormal_by_marked, |
|
111 |
.formats = c(count_fraction = format_count_fraction) |
|
112 |
) |
|
113 | ||
114 |
#' @describeIn abnormal_by_marked Layout-creating function which can take statistics function arguments |
|
115 |
#' and additional format arguments. This function is a wrapper for [rtables::analyze()]. |
|
116 |
#' |
|
117 |
#' @return |
|
118 |
#' * `count_abnormal_by_marked()` returns a layout object suitable for passing to further layouting functions, |
|
119 |
#' or to [rtables::build_table()]. Adding this function to an `rtable` layout will add formatted rows containing |
|
120 |
#' the statistics from `s_count_abnormal_by_marked()` to the table layout. |
|
121 |
#' |
|
122 |
#' @examples |
|
123 |
#' library(dplyr) |
|
124 |
#' |
|
125 |
#' df <- data.frame( |
|
126 |
#' USUBJID = as.character(c(rep(1, 5), rep(2, 5), rep(1, 5), rep(2, 5))), |
|
127 |
#' ARMCD = factor(c(rep("ARM A", 5), rep("ARM B", 5), rep("ARM A", 5), rep("ARM B", 5))), |
|
128 |
#' ANRIND = factor(c( |
|
129 |
#' "NORMAL", "HIGH", "HIGH", "HIGH HIGH", "HIGH", |
|
130 |
#' "HIGH", "HIGH", "HIGH HIGH", "NORMAL", "HIGH HIGH", "NORMAL", "LOW", "LOW", "LOW LOW", "LOW", |
|
131 |
#' "LOW", "LOW", "LOW LOW", "NORMAL", "LOW LOW" |
|
132 |
#' )), |
|
133 |
#' ONTRTFL = rep(c("", "Y", "Y", "Y", "Y", "Y", "Y", "Y", "Y", "Y"), 2), |
|
134 |
#' PARAMCD = factor(c(rep("CRP", 10), rep("ALT", 10))), |
|
135 |
#' AVALCAT1 = factor(rep(c("", "", "", "SINGLE", "REPLICATED", "", "", "LAST", "", "SINGLE"), 2)), |
|
136 |
#' stringsAsFactors = FALSE |
|
137 |
#' ) |
|
138 |
#' |
|
139 |
#' df <- df %>% |
|
140 |