Use NSE para construir uma fórmula
Estou tentando construir uma fórmula usando NSE para que eu possa facilmente canalizar colunas. O seguinte é o meu caso de uso desejado:
df %>% make_formula(col1, col2, col3)
[1] "col1 ~ col2 + col3"
Eu fiz primeiro esta função:
varstring <- function(...) {
as.character(match.call()[-1])
}
Isso funciona muito bem com objetos únicos ou vários objetos:
varstring(col)
[1] "col"
varstring(col1, col2, col3)
[1] "col1" "col2" "col3"
Eu crio minha função para criar a fórmula a seguir:
formula <- function(df, col, ...) {
group <- varstring(col)
vars <- varstring(...)
paste(group,"~", paste(vars, collapse = " + "), sep = " ")
}
No entanto, a chamada de função formula(df, col, col1, col2, col3)produz [1] "group ~ ..1 + ..2 + ..3".
Eu entendo que a fórmula está literalmente avaliando varstring(group)e varstring(...)não substituindo os objetos fornecidos pelo usuário para avaliação como eu gostaria também. Mas não consigo descobrir como fazer isso funcionar como pretendido.
Respostas
Você pode juntar um número arbitrário de argumentos com uma função binária usandoreduce()
make_formula <- function(lhs, ..., op = "+") {
lhs <- ensym(lhs)
args <- ensyms(...)
n <- length(args)
if (n == 0) {
rhs <- 1
} else if (n == 1) {
rhs <- args[[1]]
} else {
rhs <- purrr::reduce(args, function(out, new) call(op, out, new))
}
# Don't forget to forward the caller environment
new_formula(lhs, rhs, env = caller_env())
}
make_formula(disp)
#> disp ~ 1
make_formula(disp, cyl)
#> disp ~ cyl
make_formula(disp, cyl, am, drat)
#> disp ~ cyl + am + drat
make_formula(disp, cyl, am, drat, op = "*")
#> disp ~ cyl * am * drat
Uma grande vantagem de trabalhar com expressões é que ele é robusto para pequenas mesas bobby (https://xkcd.com/327/):
# User inputs are always interpreted as symbols (variable name)
make_formula(disp, `I(file.remove('~'))`)
#> disp ~ `I(file.remove('~'))`
# With `paste()` + `parse()` user inputs are interpreted as arbitrary code
reformulate(c("foo", "I(file.remove('~'))"))
#> ~foo + I(file.remove("~"))
Sugiro usar rlang::enquo(s) e rlang::as_namepara conseguir isso:
library(rlang)
formula <- function(df, col, ...) {
group <- enquo(col)
vars <- enquos(...)
group_str <- rlang::as_name(group)
vars_str <- lapply(vars, rlang::as_name)
paste(group_str,"~", paste(vars_str, collapse = " + "), sep = " ")
}
formula(mtcars, col, col1, col2, col3)
#> [1] "col ~ col1 + col2 + col3"
Nós poderíamos usarreformulate
formula_fn <- function(dat, col, ...) {
deparse(reformulate(purrr::map_chr(ensyms(...), rlang::as_string),
response = rlang::as_string(ensym(col) )))
}
formula_fn(mtcars, col, col1, col2, col3)
#[1] "col ~ col1 + col2 + col3"
Eu segui o conselho de @LionelHenry acima e criei a seguinte função com algumas funcionalidades adicionais não solicitadas na minha pergunta inicial.
#' Create a formula
#'
#' Creates a new formula object to be used anywhere formulas are used (i.e, `glm`).
#'
#' @param ... any number of arguments to compose the formula
#' @param lhs a boolean indicating if the formula has a left hand side of the argument
#' @param op the operand acting upon the arguments of the right side of the formula.
#' @param group an argument to use as a grouping variable to facet by
#'
#' @return a formula
#'
#' @details If `lhs` is `TRUE`, the first argument provided is used as the left hand side of the formula.
#' The `group` paramenter will add `| group` to the end of the formula. This is useful for packages that support faceting by grouping variables for the purposes of tables or graphs.
#'
#' @export
#'
#' @examples
#' make_formula(var1, var2, var3)
#' make_formula(var1, var2, var3, lhs = FALSE)
#' make_formula(var1, var2, var3, lhs = FALSE, group = var4)
#'
make_formula <- function(..., lhs = TRUE, op = "+", group = NULL) {
args <- rlang::ensyms(...)
n <- length(args)
group <- rlang::enexpr(group)
if(lhs) {
left <- args[[1]]
if (n == 1) {
right <- 1
} else if (n == 2) {
right <- args[[2]]
} else {
right <- purrr::reduce(args[-1], function(out, new) call(op, out, new))
}
} else {
left <- NULL
if (n == 1) {
right <- args[[1]]
} else {
right <- purrr::reduce(args, function(out, new) call(op, out, new))
}
}
if(!is.null(group)) {
group <- rlang::ensym(group)
right <- purrr::reduce(c(right, group), function(out, new) call("|", out, new))
}
rlang::new_formula(left, right, env = rlang::caller_env()) # Forward to the caller environment
}