Skip to content

Commit 0c5fd0b

Browse files
ddsjobergMelkiades
andauthored
Adding model construction helpers (#121)
**What changes are proposed in this pull request?** * Adding functions to assist in constructing models `construct_model()`, `reformulate2()` (similar to `stats::reformulate()` but adds backticks around variable names with spaces in them), `bt()` (adds backtics to variable as needed), and `bt_strip()` (removes backticks from character variable names). I also updated `ard_survey_svychisq()` to use `reformulate2()` instead of `stats::reformulate()` which allows variable names with spaces in them. Other instances of `stats::reformulate()` should also be updated in the future. The reason we're adding the constructors is because the full ARS (Analysis Result Standard) requires that we begin with a data frame, and these help us take a data frame, a response variable, and covariates to construct complex models. **Reference GitHub issue associated with pull request.** _e.g., 'closes #<issue number>'_ closes #108 -------------------------------------------------------------------------------- Pre-review Checklist (if item does not apply, mark is as complete) - [x] **All** GitHub Action workflows pass with a ✅ - [x] PR branch has pulled the most recent updates from master branch: `usethis::pr_merge_main()` - [x] If a bug was fixed, a unit test was added. - [x] If a new `ard_*()` function was added, it passes the ARD structural checks from `cards::check_ard_structure()`. - [x] 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: Davide Garolini <dgarolini@gmail.com>
1 parent f2fe696 commit 0c5fd0b

12 files changed

+390
-80
lines changed

NAMESPACE

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
S3method(ard_regression,default)
44
S3method(ard_stats_anova,anova)
55
S3method(ard_stats_anova,data.frame)
6+
S3method(construct_model,data.frame)
67
export("%>%")
78
export(all_of)
89
export(any_of)
@@ -36,6 +37,9 @@ export(ard_survey_svyranktest)
3637
export(ard_survey_svyttest)
3738
export(ard_survival_survdiff)
3839
export(ard_survival_survfit)
40+
export(bt)
41+
export(bt_strip)
42+
export(construct_model)
3943
export(contains)
4044
export(ends_with)
4145
export(everything)
@@ -49,6 +53,7 @@ export(proportion_ci_jeffreys)
4953
export(proportion_ci_strat_wilson)
5054
export(proportion_ci_wald)
5155
export(proportion_ci_wilson)
56+
export(reformulate2)
5257
export(starts_with)
5358
export(where)
5459
import(rlang)

R/ard_stats_anova.R

Lines changed: 32 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -10,21 +10,13 @@
1010
#' a data frame
1111
#' @param formulas (`list`)\cr
1212
#' a list of formulas
13-
#' @param fn (`string`)\cr
14-
#' string naming the function to be called, e.g. `"glm"`.
15-
#' If function belongs to a library that is not attached, the package name
16-
#' must be specified in the `package` argument.
17-
#' @param fn.args (named `list`)\cr
18-
#' named list of arguments that will be passed to `fn`.
19-
#' @param package (`string`)\cr
20-
#' string of package name that will be temporarily loaded when function
21-
#' specified in `method` is executed.
22-
#' @param method (`string`)\cr
13+
#' @param method_text (`string`)\cr
2314
#' string of the method used. Default is `"ANOVA results from `stats::anova()`"`.
2415
#' We provide the option to change this as `stats::anova()` can produce
2516
#' results from many types of models that may warrant a more precise
2617
#' description.
2718
#' @inheritParams rlang::args_dots_empty
19+
#' @inheritParams construction_helpers
2820
#'
2921
#' @details
3022
#' When a list of formulas is supplied to `ard_stats_anova()`, these formulas
@@ -34,12 +26,12 @@
3426
#' The models are constructed using `rlang::exec()`, which is similar to `do.call()`.
3527
#'
3628
#' ```r
37-
#' rlang::exec(.fn = fn, formula = formula, data = data, !!!fn.args)
29+
#' rlang::exec(.fn = method, formula = formula, data = data, !!!method.args)
3830
#' ```
3931
#'
4032
#' The above function is executed in `withr::with_namespace(package)`, which
41-
#' allows for the use of `ard_stats_anova(fn)` from packages,
42-
#' e.g. `package = 'lme4'` must be specified when `fn = 'glmer'`.
33+
#' allows for the use of `ard_stats_anova(method)` from packages,
34+
#' e.g. `package = 'lme4'` must be specified when `method = 'glmer'`.
4335
#' See example below.
4436
#'
4537
#' @return ARD data frame
@@ -55,15 +47,15 @@
5547
#' ard_stats_anova(
5648
#' x = mtcars,
5749
#' formulas = list(am ~ mpg, am ~ mpg + hp),
58-
#' fn = "glm",
59-
#' fn.args = list(family = binomial)
50+
#' method = "glm",
51+
#' method.args = list(family = binomial)
6052
#' )
6153
#'
6254
#' ard_stats_anova(
6355
#' x = mtcars,
6456
#' formulas = list(am ~ 1 + (1 | vs), am ~ mpg + (1 | vs)),
65-
#' fn = "glmer",
66-
#' fn.args = list(family = binomial),
57+
#' method = "glmer",
58+
#' method.args = list(family = binomial),
6759
#' package = "lme4"
6860
#' )
6961
NULL
@@ -76,60 +68,52 @@ ard_stats_anova <- function(x, ...) {
7668

7769
#' @rdname ard_stats_anova
7870
#' @export
79-
ard_stats_anova.anova <- function(x, method = "ANOVA results from `stats::anova()`", ...) {
71+
ard_stats_anova.anova <- function(x, method_text = "ANOVA results from `stats::anova()`", ...) {
8072
set_cli_abort_call()
8173

8274
# check inputs ---------------------------------------------------------------
8375
check_dots_empty()
8476
check_pkg_installed("broom", reference_pkg = "cardx")
85-
check_string(method, message = "Argument {.arg method} must be a string of a function name.")
77+
check_string(method_text)
8678

8779
# return df in cards formats -------------------------------------------------
8880
lst_results <-
8981
cards::eval_capture_conditions(
90-
.anova_tidy_and_reshape(x, method = method)
82+
.anova_tidy_and_reshape(x, method_text = method_text)
9183
)
9284

9385
# final tidying up of cards data frame ---------------------------------------
94-
.anova_final_ard_prep(lst_results, method = method)
86+
.anova_final_ard_prep(lst_results, method_text = method_text)
9587
}
9688

9789

9890
#' @rdname ard_stats_anova
9991
#' @export
10092
ard_stats_anova.data.frame <- function(x,
10193
formulas,
102-
fn,
103-
fn.args = list(),
94+
method,
95+
method.args = list(),
10496
package = "base",
105-
method = "ANOVA results from `stats::anova()`",
97+
method_text = "ANOVA results from `stats::anova()`",
10698
...) {
10799
set_cli_abort_call()
108100

109101
# check inputs ---------------------------------------------------------------
110102
check_dots_empty()
111-
check_string(package)
112103
check_pkg_installed(c("broom", "withr", package), reference_pkg = "cardx")
113104
check_not_missing(formulas)
114-
check_not_missing(x)
115-
check_not_missing(fn)
116-
check_string(method, message = "Argument {.arg method} must be a string of a function name.")
117-
check_data_frame(x)
118-
check_string(fn)
119-
if (str_detect(fn, "::")) {
120-
cli::cli_abort(
121-
c(
122-
"Argument {.arg fn} cannot be namespaced.",
123-
i = "Put the package name in the {.arg package} argument."
124-
),
125-
call = get_cli_abort_call()
105+
check_class(formulas, cls = "list")
106+
walk(
107+
formulas,
108+
~ check_class(
109+
.x,
110+
cls = "formula",
111+
arg_name = "formulas",
112+
message = "Each element of {.arg formulas} must be class {.cls formula}"
126113
)
127-
}
114+
)
128115

129116
# calculate results and return df in cards formats ---------------------------
130-
# process fn.args argument
131-
fn.args <- rlang::call_args(rlang::enexpr(fn.args))
132-
133117
# create models
134118
lst_results <-
135119
cards::eval_capture_conditions({
@@ -138,24 +122,20 @@ ard_stats_anova.data.frame <- function(x,
138122
lapply(
139123
formulas,
140124
function(formula) {
141-
withr::with_namespace(
142-
package = package,
143-
call2(.fn = fn, formula = formula, data = x, !!!fn.args) |>
144-
eval_tidy()
145-
)
125+
construct_model(x = x, formula = formula, method = method, method.args = {{ method.args }}, package = package)
146126
}
147127
)
148128

149129
# now calculate `stats::anova()` and reshape results
150130
rlang::inject(stats::anova(!!!models)) |>
151-
.anova_tidy_and_reshape(method = method)
131+
.anova_tidy_and_reshape(method_text = method_text)
152132
})
153133

154134
# final tidying up of cards data frame ---------------------------------------
155-
.anova_final_ard_prep(lst_results, method = method)
135+
.anova_final_ard_prep(lst_results, method_text = method_text)
156136
}
157137

158-
.anova_tidy_and_reshape <- function(x, method) {
138+
.anova_tidy_and_reshape <- function(x, method_text) {
159139
broom::tidy(x) |>
160140
dplyr::mutate(
161141
across(everything(), as.list),
@@ -174,13 +154,13 @@ ard_stats_anova.data.frame <- function(x,
174154
dplyr::filter(., dplyr::n() == dplyr::row_number()) |>
175155
dplyr::mutate(
176156
stat_name = "method",
177-
stat = list(.env$method)
157+
stat = list(.env$method_text)
178158
)
179159
)
180160
}
181161
}
182162

183-
.anova_final_ard_prep <- function(lst_results, method) {
163+
.anova_final_ard_prep <- function(lst_results, method_text) {
184164
# saving the results in data frame -------------------------------------------
185165
df_card <-
186166
if (!is.null(lst_results[["result"]])) {
@@ -189,7 +169,7 @@ ard_stats_anova.data.frame <- function(x,
189169
dplyr::tibble(
190170
variable = "model_1",
191171
stat_name = c("p.value", "method"),
192-
stat = list(NULL, method)
172+
stat = list(NULL, method_text)
193173
)
194174
}
195175

R/ard_survey_svychisq.R

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ ard_survey_svychisq <- function(data, by, variables, statistic = "F", ...) {
5050
cards::tidy_as_ard(
5151
lst_tidy =
5252
cards::eval_capture_conditions(
53-
survey::svychisq(stats::reformulate(termlabels = paste(variable, by, sep = "+"), response = NULL), design = data, statistic = statistic, ...) |>
53+
survey::svychisq(reformulate2(termlabels = c(variable, by)), design = data, statistic = statistic, ...) |>
5454
broom::tidy()
5555
),
5656
tidy_result_names = c("statistic", "p.value", "ndf", "ddf", "method"),

R/construction_helpers.R

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
#' Construction Helpers
2+
#'
3+
#' These functions help construct calls to various types of models.
4+
#'
5+
#' - `construct_model()`: Builds models of the form `method(data = data, formula = formula, method.args!!!)`.
6+
#' If the `package` argument is specified, that package is temporarily attached
7+
#' when the model is evaluated.
8+
#'
9+
#' - `reformulate2()`: This is a copy of `reformulate()` except that variable
10+
#' names that contain a space are wrapped in backticks.
11+
#'
12+
#' - `bt()`: Adds backticks to a character vector.
13+
#'
14+
#' - `bt_strip()`: Removes backticks from a string if it begins and ends with a backtick.
15+
#'
16+
#' @param x (`data.frame`)\cr
17+
#' a data frame
18+
#' @param formula (`formula`)\cr
19+
#' a formula
20+
#' @param method (`string`)\cr
21+
#' string naming the function to be called, e.g. `"glm"`.
22+
#' If function belongs to a library that is not attached, the package name
23+
#' must be specified in the `package` argument.
24+
#' @param method.args (named `list`)\cr
25+
#' named list of arguments that will be passed to `fn`.
26+
#' @param package (`string`)\cr
27+
#' string of package name that will be temporarily loaded when function
28+
#' specified in `method` is executed.
29+
#' @param x (`character`)\cr
30+
#' character vector, typically of variable names
31+
#' @param pattern (`string`)\cr
32+
#' regular expression string. If the regex matches, backticks are added
33+
#' to the string. When `NULL`, backticks are not added.
34+
#' @param pattern_term,pattern_response passed to `bt(pattern)` for arguments
35+
#' `stats::reformulate(termlabels, response)`.
36+
#' @inheritParams rlang::eval_tidy
37+
#' @inheritParams stats::reformulate
38+
#' @inheritParams rlang::args_dots_empty
39+
#'
40+
#' @return depends on the calling function
41+
#' @name construction_helpers
42+
#'
43+
#' @examplesIf do.call(asNamespace("cardx")$is_pkg_installed, list(pkg = c("withr", "lme4"), reference_pkg = "cardx"))
44+
#' construct_model(
45+
#' x = mtcars,
46+
#' formula = am ~ mpg + (1 | vs),
47+
#' method = "glmer",
48+
#' method.args = list(family = binomial),
49+
#' package = "lme4"
50+
#' )
51+
#'
52+
#' construct_model(
53+
#' x = mtcars |> dplyr::rename(`M P G` = mpg),
54+
#' formula = reformulate2(c("M P G", "cyl"), response = "hp"),
55+
#' method = "lm"
56+
#' ) |>
57+
#' ard_regression() |>
58+
#' dplyr::filter(stat_name %in% c("term", "estimate", "p.value"))
59+
NULL
60+
61+
#' @rdname construction_helpers
62+
#' @export
63+
construct_model <- function(x, ...) {
64+
UseMethod("construct_model")
65+
}
66+
67+
#' @rdname construction_helpers
68+
#' @export
69+
construct_model.data.frame <- function(x, formula, method, method.args = list(), package = "base", env = caller_env(), ...) {
70+
set_cli_abort_call()
71+
# check pkg installations ----------------------------------------------------
72+
check_dots_empty()
73+
check_pkg_installed(c("withr", package), reference_pkg = "cardx")
74+
75+
check_not_missing(formula)
76+
check_class(formula, cls = "formula")
77+
78+
check_not_missing(method)
79+
check_string(method)
80+
check_not_namespaced(method)
81+
82+
# convert method.args to list of expressions (to account for NSE inputs) -----
83+
method.args <- call_args(enexpr(method.args))
84+
85+
# build model ----------------------------------------------------------------
86+
withr::with_namespace(
87+
package = package,
88+
call2(.fn = method, formula = formula, data = x, !!!method.args) |>
89+
eval_tidy(env = env)
90+
)
91+
}
92+
93+
#' @rdname construction_helpers
94+
#' @export
95+
reformulate2 <- function(termlabels, response = NULL, intercept = TRUE,
96+
pattern_term = "[ \n\r]", pattern_response = "[ \n\r]",
97+
env = parent.frame()) {
98+
stats::reformulate(
99+
termlabels = bt(termlabels, pattern_term),
100+
response = bt(response, pattern_response),
101+
intercept = intercept,
102+
env = env
103+
)
104+
}
105+
106+
#' @rdname construction_helpers
107+
#' @export
108+
bt <- function(x, pattern = "[ \n\r]") {
109+
if (is_empty(x)) {
110+
return(x)
111+
}
112+
if (is_empty(pattern)) {
113+
return(x)
114+
}
115+
ifelse(
116+
str_detect(x, pattern = pattern),
117+
paste0("`", x, "`"),
118+
x
119+
)
120+
}
121+
122+
#' @rdname construction_helpers
123+
#' @export
124+
bt_strip <- function(x) {
125+
ifelse(
126+
str_detect(x, "^`.*`$"),
127+
substr(x, 2, nchar(x) - 1),
128+
x
129+
)
130+
}
131+
132+
check_not_namespaced <- function(x,
133+
arg_name = rlang::caller_arg(x),
134+
class = "check_not_namespaced",
135+
call = get_cli_abort_call()) {
136+
check_string(x, arg_name = arg_name, call = call, class = "check_not_namespaced")
137+
138+
if (str_detect(x, "::")) {
139+
c("Argument {.arg {arg_name}} cannot be namespaced.",
140+
i = "Put the package name in the {.arg package} argument."
141+
) |>
142+
cli::cli_abort(call = call, class = class)
143+
}
144+
145+
invisible(x)
146+
}

_pkgdown.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,3 +57,4 @@ reference:
5757
- title: "Helpers"
5858
- contents:
5959
- proportion_ci
60+
- construction_helpers

inst/WORDLIST

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ XG
1818
Xin
1919
agresti
2020
anova
21+
backtick
2122
cli
2223
clopper
2324
coull
@@ -27,6 +28,7 @@ funder
2728
jeffreys
2829
pearson
2930
pre
31+
quosures
3032
sd
3133
strat
3234
vif

0 commit comments

Comments
 (0)