Skip to content

Commit 0554942

Browse files
edelaruaddsjoberg
andauthored
Enhance formula processing in ard_survival_survfit() (#226)
**What changes are proposed in this pull request?** * Implemented `summary(extend=TRUE)` in `ard_survival_survfit()` to return results for time points out of bounds. * Added a `data.frame` method to `ard_survival_survfit()`. * Added a warning for incorrect formula type to `ard_survival_survfit()`. Closes #223 and #224 -------------------------------------------------------------------------------- Pre-review Checklist (if item does not apply, mark is as complete) - [ ] **All** GitHub Action workflows pass with a ✅ - [ ] PR branch has pulled the most recent updates from master branch: `usethis::pr_merge_main()` - [ ] If a bug was fixed, a unit test was added. - [ ] If a new `ard_*()` function was added, it passes the ARD structural checks from `cards::check_ard_structure()`. - [ ] If a new `ard_*()` function was added, `set_cli_abort_call()` has been set. - [ ] If a new `ard_*()` function was added and it depends on another package (such as, `broom`), `is_pkg_installed("broom")` has been set in the function call and the following added to the roxygen comments: `@examplesIf do.call(asNamespace("cardx")$is_pkg_installed, list(pkg = "broom""))` - [ ] Code coverage is suitable for any new functions/features (generally, 100% coverage for new code): `devtools::test_coverage()` Reviewer Checklist (if item does not apply, mark is as complete) - [ ] If a bug was fixed, a unit test was added. - [ ] Code coverage is suitable for any new functions/features: `devtools::test_coverage()` When the branch is ready to be merged: - [ ] Update `NEWS.md` with the changes from this pull request under the heading "`# cardx (development version)`". If there is an issue associated with the pull request, reference it in parentheses at the end update (see `NEWS.md` for examples). - [ ] **All** GitHub Action workflows pass with a ✅ - [ ] Approve Pull Request - [ ] Merge the PR. Please use "Squash and merge" or "Rebase and merge". --------- Co-authored-by: Daniel Sjoberg <danield.sjoberg@gmail.com>
1 parent 609b99e commit 0554942

File tree

8 files changed

+301
-15
lines changed

8 files changed

+301
-15
lines changed

NAMESPACE

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ S3method(ard_missing,survey.design)
1212
S3method(ard_regression,default)
1313
S3method(ard_stats_anova,anova)
1414
S3method(ard_stats_anova,data.frame)
15+
S3method(ard_survival_survfit,data.frame)
16+
S3method(ard_survival_survfit,survfit)
1517
S3method(ard_total_n,survey.design)
1618
S3method(construct_model,data.frame)
1719
S3method(construct_model,survey.design)

NEWS.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# cardx 0.2.1.9008
22

3+
* Implemented `summary(extend=TRUE)` in `ard_survival_survfit()` to return results for time points out of bounds. (#224)
4+
5+
* Added a `data.frame` method to `ard_survival_survfit()`.
6+
7+
* Added a warning for incorrect formula type to `ard_survival_survfit()`. (#223)
8+
39
# cardx 0.2.1
410

511
## New Features and Updates

R/ard_survival_survfit.R

Lines changed: 81 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44
#' Analysis results data for survival quantiles and x-year survival estimates, extracted
55
#' from a [survival::survfit()] model.
66
#'
7-
#' @param x ([survival::survfit()])\cr
8-
#' a [survival::survfit()] object. See below for details.
7+
#' @param x (`survfit` or `data.frame`)\cr
8+
#' an object of class `survfit` created with [survival::survfit()] or a data frame. See below for details.
99
#' @param times (`numeric`)\cr
1010
#' a vector of times for which to return survival probabilities.
1111
#' @param probs (`numeric`)\cr
@@ -23,6 +23,34 @@
2323
#' ) %>%
2424
#' knitr::kable()
2525
#' ```
26+
#' @param y (`Surv` or `string`)\cr
27+
#' an object of class `Surv` created using [survival::Surv()]. This object will be passed as the left-hand side of
28+
#' the formula constructed and passed to [survival::survfit()]. This object can also be passed as a string.
29+
#' @param variables (`character`)\cr
30+
#' stratification variables to be passed as the right-hand side of the formula constructed and passed to
31+
#' [survival::survfit()].
32+
#' @param method.args (named `list`)\cr
33+
#' named list of arguments that will be passed to [survival::survfit()].
34+
#' @inheritParams rlang::args_dots_empty
35+
#'
36+
#' @section Formula Specification:
37+
#' When passing a [`survival::survfit()`] object to `ard_survival_survfit()`,
38+
#' the `survfit()` call must use an evaluated formula and not a stored formula.
39+
#' Including a proper formula in the call allows the function to accurately
40+
#' identify all variables included in the estimation. See below for examples:
41+
#'
42+
#' ```r
43+
#' library(cardx)
44+
#' library(survival)
45+
#'
46+
#' # include formula in `survfit()` call
47+
#' survfit(Surv(time, status) ~ sex, lung) |> ard_survival_survfit(time = 500)
48+
#'
49+
#' # you can also pass a data frame to `ard_survival_survfit()` as well.
50+
#' lung |>
51+
#' ard_survival_survfit(y = Surv(time, status), variables = "sex", time = 500)
52+
#' ```
53+
#' You **cannot**, however, pass a stored formula, e.g. `survfit(my_formula, lung)`
2654
#'
2755
#' @return an ARD data frame of class 'card'
2856
#' @name ard_survival_survfit
@@ -42,6 +70,9 @@
4270
#' survfit(Surv_CNSR(AVAL, CNSR) ~ TRTA, data = cards::ADTTE, conf.int = 0.90) |>
4371
#' ard_survival_survfit(probs = c(0.25, 0.5, 0.75))
4472
#'
73+
#' cards::ADTTE |>
74+
#' ard_survival_survfit(y = Surv_CNSR(AVAL, CNSR), variables = c("TRTA", "SEX"), times = 90)
75+
#'
4576
#' # Competing Risks Example ---------------------------
4677
#' set.seed(1)
4778
#' ADTTE_MS <- cards::ADTTE %>%
@@ -59,15 +90,31 @@ NULL
5990

6091
#' @rdname ard_survival_survfit
6192
#' @export
62-
ard_survival_survfit <- function(x, times = NULL, probs = NULL, type = NULL) {
93+
ard_survival_survfit <- function(x, ...) {
94+
set_cli_abort_call()
95+
96+
check_not_missing(x)
97+
UseMethod("ard_survival_survfit")
98+
}
99+
100+
#' @rdname ard_survival_survfit
101+
#' @export
102+
ard_survival_survfit.survfit <- function(x, times = NULL, probs = NULL, type = NULL, ...) {
63103
set_cli_abort_call()
64104

65105
# check installed packages ---------------------------------------------------
66106
check_pkg_installed(c("survival", "broom"))
67107

68108
# check/process inputs -------------------------------------------------------
69-
check_not_missing(x)
70-
check_class(x, cls = "survfit")
109+
if (is.name(x$call$formula)) {
110+
cli::cli_abort(
111+
message = paste(
112+
"The call in the survfit object {.arg x} must be an evaluated formula.",
113+
"Please see the function documentation for details on properly specifying formulas."
114+
),
115+
call = get_cli_abort_call()
116+
)
117+
}
71118
if (inherits(x, "survfitcox")) {
72119
cli::cli_abort("Argument {.arg x} cannot be class {.cls survfitcox}.",
73120
call = get_cli_abort_call()
@@ -107,6 +154,34 @@ ard_survival_survfit <- function(x, times = NULL, probs = NULL, type = NULL) {
107154
.format_survfit_results(tidy_survfit)
108155
}
109156

157+
#' @rdname ard_survival_survfit
158+
#' @export
159+
ard_survival_survfit.data.frame <- function(x, y, variables,
160+
times = NULL, probs = NULL, type = NULL,
161+
method.args = list(conf.int = 0.95), ...) {
162+
set_cli_abort_call()
163+
164+
# check/process inputs -------------------------------------------------------
165+
check_class(variables, "character")
166+
167+
# process outcome as string --------------------------------------------------
168+
y <- enquo(y)
169+
# if a character was passed, return it as is
170+
if (tryCatch(is.character(eval_tidy(y)), error = \(e) FALSE)) y <- eval_tidy(y) # styler: off
171+
# otherwise, convert expr to string
172+
else y <- expr_deparse(quo_get_expr(y)) # styler: off
173+
174+
# build model ----------------------------------------------------------------
175+
construct_model(
176+
data = x,
177+
formula = stats::reformulate(termlabels = bt(variables), response = y),
178+
method = "survfit",
179+
package = "survival",
180+
method.args = {{ method.args }}
181+
) |>
182+
ard_survival_survfit(times = times, probs = probs, type = type)
183+
}
184+
110185
#' Process Survival Fit For Time Estimates
111186
#'
112187
#' @inheritParams cards::tidy_as_ard
@@ -134,7 +209,7 @@ ard_survival_survfit <- function(x, times = NULL, probs = NULL, type = NULL) {
134209
start.time <- 0
135210
}
136211
x <- survival::survfit0(x, start.time) %>%
137-
summary(times)
212+
summary(times, extend = TRUE)
138213

139214
# process competing risks/multi-state models
140215
multi_state <- inherits(x, "summary.survfitms")

man/ard_survival_survfit.Rd

Lines changed: 55 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/dot-process_survfit_probs.Rd

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/dot-process_survfit_time.Rd

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/testthat/_snaps/ard_survival_survfit.md

Lines changed: 87 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -265,13 +265,38 @@
265265
Message
266266
i 4 more variables: context, fmt_fn, warning, error
267267

268+
---
269+
270+
Code
271+
survival::survfit(survival::Surv(AVAL, CNSR) ~ TRTA, data = ADTTE_MS) %>%
272+
ard_survival_survfit(times = c(60, 180), type = "risk")
273+
Condition
274+
Error in `ard_survival_survfit()`:
275+
! Cannot use `type` argument with `survfit` models with class <survfitms/survfitcoxms>.
276+
268277
# ard_survival_survfit() errors are properly handled
269278

270279
Code
271-
ard_survival_survfit("not_survfit")
280+
ard_survival_survfit(x, times = 25)
272281
Condition
273282
Error in `ard_survival_survfit()`:
274-
! The `x` argument must be class <survfit>, not a string.
283+
! The call in the survfit object `x` must be an evaluated formula. Please see the function documentation for details on properly specifying formulas.
284+
285+
---
286+
287+
Code
288+
ard_survival_survfit(times = 25)
289+
Condition
290+
Error in `ard_survival_survfit()`:
291+
! The `x` argument cannot be missing.
292+
293+
---
294+
295+
Code
296+
ard_survival_survfit("not_survfit")
297+
Condition
298+
Error in `UseMethod()`:
299+
! no applicable method for 'ard_survival_survfit' applied to an object of class "character"
275300

276301
---
277302

@@ -282,6 +307,15 @@
282307
Error in `ard_survival_survfit()`:
283308
! `type` must be one of "survival", "risk", or "cumhaz", not "notatype".
284309

310+
---
311+
312+
Code
313+
ard_survival_survfit(survival::survfit(survival::Surv(AVAL, CNSR) ~ TRTA,
314+
cards::ADTTE), probs = c(0.25, 0.75), type = "risk")
315+
Condition
316+
Error in `ard_survival_survfit()`:
317+
! Cannot use `type` argument when `probs` argument specifed.
318+
285319
---
286320

287321
Code
@@ -300,3 +334,54 @@
300334
Error in `ard_survival_survfit()`:
301335
! Argument `x` cannot be class <survfitcox>.
302336

337+
# ard_survival_survfit() extends to times outside range
338+
339+
Code
340+
print(ard_survival_survfit(survival::survfit(survival::Surv(AVAL, CNSR) ~ TRTA,
341+
cards::ADTTE), times = 200), n = Inf)
342+
Message
343+
{cards} data frame: 15 x 11
344+
Output
345+
group1 group1_level variable variable_level stat_name stat_label stat
346+
1 TRTA Placebo time 200 n.risk Number o… 0
347+
2 TRTA Placebo time 200 estimate Survival… 0
348+
3 TRTA Placebo time 200 std.error Standard… NaN
349+
4 TRTA Placebo time 200 conf.high CI Upper… NA
350+
5 TRTA Placebo time 200 conf.low CI Lower… NA
351+
6 TRTA Xanomeli… time 200 n.risk Number o… 0
352+
7 TRTA Xanomeli… time 200 estimate Survival… 0
353+
8 TRTA Xanomeli… time 200 std.error Standard… NaN
354+
9 TRTA Xanomeli… time 200 conf.high CI Upper… NA
355+
10 TRTA Xanomeli… time 200 conf.low CI Lower… NA
356+
11 TRTA Xanomeli… time 200 n.risk Number o… 0
357+
12 TRTA Xanomeli… time 200 estimate Survival… 0
358+
13 TRTA Xanomeli… time 200 std.error Standard… NaN
359+
14 TRTA Xanomeli… time 200 conf.high CI Upper… NA
360+
15 TRTA Xanomeli… time 200 conf.low CI Lower… NA
361+
Message
362+
i 4 more variables: context, fmt_fn, warning, error
363+
364+
# ard_survival_survfit.data.frame() works as expected
365+
366+
Code
367+
res_quo <- print(dplyr::mutate(ard_survival_survfit.data.frame(x = mtcars, y = "survival::Surv(mpg, am)",
368+
variables = "vs", times = 20, method.args = list(start.time = 0, id = cyl)),
369+
stat = lapply(stat, function(x) ifelse(is.numeric(x), cards::round5(x, 3), x))),
370+
n = Inf)
371+
Message
372+
{cards} data frame: 10 x 11
373+
Output
374+
group1 group1_level variable variable_level stat_name stat_label stat
375+
1 vs 0 time 20 n.risk Number o… 3
376+
2 vs 0 time 20 estimate Survival… 0.615
377+
3 vs 0 time 20 std.error Standard… 0.082
378+
4 vs 0 time 20 conf.high CI Upper… 0.8
379+
5 vs 0 time 20 conf.low CI Lower… 0.474
380+
6 vs 1 time 20 n.risk Number o… 11
381+
7 vs 1 time 20 estimate Survival… 1
382+
8 vs 1 time 20 std.error Standard… 0
383+
9 vs 1 time 20 conf.high CI Upper… 1
384+
10 vs 1 time 20 conf.low CI Lower… 1
385+
Message
386+
i 4 more variables: context, fmt_fn, warning, error
387+

0 commit comments

Comments
 (0)