Related
say I have a function f as
f = function(x = 1, y, z, t) { x + y + z}
and a list l such
l = list(Y = 2, t = "test")
I can evaluate f in l like
eval(quote(f(y = Y, z = 3)), envir = l)
6
My question is that I'd like to get all the values of the arguments that ends up being used by the function f ie. a function magic that would take a call object and an environment and would return the values of all the arguments that would be used in evaluating the expression.
For instance:
call_obj = quote(f(y = Y, z = 3))
magic(call_obj, envir = l)
# I get a named list which value is list(1,2,3,"test")
# For that matter I do not even need the default arguments values (x)
EDIT: Adding a bounty for a base-r answer (while #Artem Sokolov provided a purrr-rlang one, extracting a couple relevant functions would still be fine though)
tidyverse solution
# Identify the variables in l that can be used to specify arguments of f
args1 <- l[ intersect( names(formals(f)), names(l) ) ]
# Augment the call with these variables
call_obj2 <- rlang::call_modify( call_obj, !!!args1 )
# f(y = Y, z = 3, t = "test")
# Evaluate the arguments of the call in the context of l and combine with defaults
purrr::list_modify( formals(f),
!!!purrr::map(rlang::call_args(call_obj2), eval, l) )
base R solution
# As above
args1 <- l[ intersect( names(formals(f)), names(l) ) ]
# Augment the call with variables in args1
l1 <- modifyList( as.list(call_obj), args1 )[-1]
# Evaluate the arguments in the context of l and combine with defaults
modifyList(formals(f), lapply(l1, eval, l))
Output for both solutions
# $x
# [1] 1
#
# $y
# [1] 2
#
# $z
# [1] 3
#
# $t
# [1] "test"
How about this one:
magic <- function(call_obj, envir) {
call_fun <- as.list(as.call(call_obj))[[1]]
call_obj <- match.call(match.fun(call_fun), as.call(call_obj))
## arguments supplied in call
call_args <- as.list(call_obj)[-1]
## arguments from function definition
fun_args <- formals(match.fun(call_fun))
## match arguments from call with list
new_vals_call <- lapply(call_args, function(x) eval(x, envir = envir))
## match arguments from function definition with list
## because everything (including NULL) can be a valid function argument we cannot directly use mget()
in_list <- sapply(names(fun_args), function(x, env) exists(x, envir = env), as.environment(envir))
new_vals_formals <- mget(names(fun_args), envir = as.environment(envir), ifnotfound = "")[in_list]
## values in the call take precedence over values from the list (can easily be reversed if needed)
new_vals_complete <- modifyList(fun_args, new_vals_formals, keep.null = TRUE)
new_vals_complete <- modifyList(new_vals_complete, new_vals_call, keep.null = TRUE)
## Construct a call object (if you want only the list of arguments return new_vals_complete)
as.call(c(call_fun, new_vals_complete))
}
# -------------------------------------------------------------------------
f <- function(x = 1, y, z, t) { x + y + z}
## Tests
## basic test
magic(quote(f(y = Y, z = 3)), list(Y = 2, t = "test"))
#> f(x = 1, y = 2, z = 3, t = "test")
## precedence (t defined twice)
magic(quote(f(y = Y, z = 3, t=99)), list(Y = 2, t = "test"))
#> f(x = 1, y = 2, z = 3, t = 99)
## missing values (z is missing)
magic(quote(f(y = Y)), list(Y = 2, t = "test"))
#> f(x = 1, y = 2, z = , t = "test")
## NULL values in call
magic(quote(f(y = Y, z = NULL)), list(Y = 2, t = "test"))
#> f(x = 1, y = 2, z = NULL, t = "test")
## NULL values in list
magic(quote(f(y = Y, z = 3)), list(Y = 2, t = NULL))
#> f(x = 1, y = 2, z = 3, t = NULL)
## NULL values and precendece
magic(quote(f(y = Y, z = 3, t= NULL)), list(Y = 2, t = "test"))
#> f(x = 1, y = 2, z = 3, t = NULL)
magic(quote(f(y = Y, z = 3, t=99)), list(Y = 2, t = NULL))
#> f(x = 1, y = 2, z = 3, t = 99)
## call with subcalls
magic(quote(f(y = sin(pi), z = 3)), list(Y = 2, t = "test"))
#> f(x = 1, y = 1.22460635382238e-16, z = 3, t = "test")
magic(quote(f(y = Y, z = 3)), list(Y = sin(pi), t = "test"))
#> f(x = 1, y = 1.22460635382238e-16, z = 3, t = "test")
## call with additional vars (g is not an argument of f) -> error: unused arguments
magic(quote(f(g = Y, z = 3)), list(Y = 2, t = "test"))
## list with with additional vars (g is not an argument of f) -> vars are ignored
magic(quote(f(y = Y, z = 3)), list(Y = 2, t = "test", g=99))
#> f(x = 1, y = 2, z = 3, t = "test")
## unnamed arguments
magic(quote(f(99, y = Y, z = 3)), list(Y = 2, t = "test"))
#> f(x = 99, y = 2, z = 3, t = "test")
magic(quote(f(99, y = Y, 77)), list(Y = 2, t = "test"))
#> f(x = 99, y = 2, z = 77, t = "test")
Strictly Base R... Also supports unnamed arguments in call_obj.
Function definition
magic <- function(call_obj, envir) {
#browser()
# Get all formal args
Formals <- formals(as.character(call_obj))
# fix names of call_obj to allow unnamed args
unnamed <- which(names(call_obj)[-1] == "")
# ignore extra arguments names if too many args (issue a warning?)
unnamed <- unnamed[unnamed <= length(Formals)]
# check for names conflicts
named <- which(names(call_obj)[-1] != "")
if (any(unnamed > named))
stop("Unnamed arguments cannot follow named arguments in call_obj")
if (any(names(Formals)[unnamed] %in% names(call_obj)))
stop("argument names conflicting in call_obj; ",
"avoid unnamed arguments if possible")
names(call_obj)[unnamed + 1] <- names(Formals)[unnamed]
# Replace defaults by call_obj values
for (nn in intersect(names(call_obj), names(Formals))) {
Formals[nn] <- call_obj[nn]
}
# Check for other values in envir
for (mm in names(which(sapply(Formals, class) == "name"))) {
if (mm %in% names(envir))
Formals[mm] <- envir[mm]
else if (Formals[mm] %in% names(envir))
Formals[mm] <- envir[which(names(envir) == Formals[[mm]])]
}
print(as.call(c(as.list(as.call(call_obj))[[1]], Formals)))
return(invisible(Formals))
}
Example
f = function(x = 1, y, z, t) { x + y + z}
l = list(Y = 2, t = "test")
call_obj = quote(f(y = Y, z = 3))
magic(call_obj, envir = l)
Results (printed)
f(x = 1, y = 2, z = 3, t = "test")
Returned object (invisibly, for assignment)
$x
[1] 1
$y
[1] 2
$z
[1] 3
$t
[1] "test"
Although we got there through different ways, all the results from AEF's tests concur with mine.
I have adapted the codes below which I referred from https://statsandr.com/blog/how-to-do-a-t-test-or-anova-for-many-variables-at-once-in-r-and-communicate-the-results-in-a-better-way/#to-go-even-further into my dataset:
Day<-c(1,1,1,1,1,1,1,1,1,2,2,2,2,2,2,2,2,2,2)
Group<-c("A","A","A","B","B","B","C","C","C","A","A","A","A","B","B","B","C","C","C")
Rain<-c(4,4,6,5,3,4,5,5,3,6,6,6,5,3,3,3,2,5,2)
UV<-c(6,6,7,8,5,6,5,6,6,6,7,7,8,8,5,6,8,5,7)
dat<-data.frame(Day,Group,Rain,UV)
x <- which(names(dat) == "Group")
y <- which(names(dat) == "Rain"
| names(dat) == "UV")
method1 <- "kruskal.test"
method2 <- "wilcox.test"
my_comparisons <- list(c("A", "B"), c("A", "C"), c("B", "C")) # comparisons for post-hoc test
library(ggpubr)
for (i in y) {
for (j in x) {
p <- ggboxplot(dat,
x = colnames(dat[j]), y = colnames(dat[i]),
color = colnames(dat[j]),
legend = "none",
palette = "npg",
add = "jitter"
)
print(
p + stat_compare_means(aes(label = paste0(..method.., ", p-value = ", ..p.format..)),
method = method1, label.y = max(dat[, i], na.rm = TRUE)
)
+ stat_compare_means(comparisons = my_comparisons, method = method2, label = "p.format")
)
}
}
How do I further repeat this function through different "Day"? Thanks.
I think you want to see the results for each day, right? You can add a third loop like this:
for (h in unique(dat$Day)) {
for (i in y) {
for (j in x) {
dat_tmp <- dat[dat$Day == h,] # create a subset of the data for each day
p <- ggboxplot(dat_tmp,
x = colnames(dat_tmp[j]), y = colnames(dat_tmp[i]),
color = colnames(dat_tmp[j]),
legend = "none",
palette = "npg",
add = "jitter"
)
print(
p + stat_compare_means(aes(label = paste0(..method.., ", p-value = ", ..p.format..)),
method = method1, label.y = max(dat_tmp[, i], na.rm = TRUE)
)
+ stat_compare_means(comparisons = my_comparisons, method = method2, label = "p.format")
)
}
}
}
I added a third loop to your code and created dat_tmp inside the loop, which becomes the dataset that you use for the analyses of each day.
I developed the stability R package which can be installed from CRAN.
install.packages("stability")
However, I have difficulty in making it to take custom column names as function arguments. Here is an example of add_anova function
library(stability)
data(ge_data)
YieldANOVA <-
add_anova(
.data = ge_data
, .y = Yield
, .rep = Rep
, .gen = Gen
, .env = Env
)
YieldANOVA
The above code works fine. However, when I change the column names of the data.frame, it doesn't work as below:
df1 <- ge_data
names(df1) <- c("G", "Institute", "R", "Block", "E", "Y")
fm1 <-
add_anova(
.data = df1
, .y = Y
, .rep = Rep
, .gen = G
, .env = E
)
Error in model.frame.default(formula = terms(.data$Y ~ .data$E + .data$Rep:.data$E + :
invalid type (NULL) for variable '.data$Rep'
Similarly another function stab_reg
fm1Reg <-
stab_reg(
.data = df1
, .y = Y
, .gen = G
, .env = E
)
Error in eval(predvars, data, env) : object 'Gen' not found
The codes of these functions can be accessed by
getAnywhere(add_anova.default)
function (.data, .y, .rep, .gen, .env)
{
Y <- enquo(.y)
Rep <- enquo(.rep)
G <- enquo(.gen)
E <- enquo(.env)
fm1 <- lm(formula = terms(.data$Y ~ .data$E + .data$Rep:.data$E +
.data$G + .data$G:.data$E, keep.order = TRUE), data = .data)
fm1ANOVA <- anova(fm1)
rownames(fm1ANOVA) <- c("Env", "Rep(Env)", "Gen", "Gen:Env",
"Residuals")
fm1ANOVA[1, 4] <- fm1ANOVA[1, 3]/fm1ANOVA[2, 3]
fm1ANOVA[2, 4] <- NA
fm1ANOVA[1, 5] <- 1 - pf(as.numeric(fm1ANOVA[1, 4]), fm1ANOVA[1,
1], fm1ANOVA[2, 1])
fm1ANOVA[2, 5] <- 1 - pf(as.numeric(fm1ANOVA[2, 4]), fm1ANOVA[2,
1], fm1ANOVA[5, 1])
class(fm1ANOVA) <- c("anova", "data.frame")
return(list(anova = fm1ANOVA))
}
<bytecode: 0xc327c28>
<environment: namespace:stability>
and
getAnywhere(stab_reg.default)
function (.data, .y, .rep, .gen, .env)
{
Y <- enquo(.y)
Rep <- enquo(.rep)
G <- enquo(.gen)
E <- enquo(.env)
g <- length(levels(.data$G))
e <- length(levels(.data$E))
r <- length(levels(.data$Rep))
g_means <- .data %>% dplyr::group_by(!!G) %>% dplyr::summarize(Mean = mean(!!Y))
names(g_means) <- c("G", "Mean")
DataNew <- .data %>% dplyr::group_by(!!G, !!E) %>% dplyr::summarize(GEMean = mean(!!Y)) %>%
dplyr::group_by(!!E) %>% dplyr::mutate(EnvMean = mean(GEMean))
IndvReg <- lme4::lmList(GEMean ~ EnvMean | Gen, data = DataNew)
IndvRegFit <- summary(IndvReg)
StabIndvReg <- tibble::as_tibble(data.frame(g_means, Slope = coef(IndvRegFit)[,
, 2][, 1], LCI = confint(IndvReg)[, , 2][, 1], UCI = confint(IndvReg)[,
, 2][, 2], R.Sqr = IndvRegFit$r.squared, RMSE = IndvRegFit$sigma,
SSE = IndvRegFit$sigma^2 * IndvRegFit$df[, 2], Delta = IndvRegFit$sigma^2 *
IndvRegFit$df[, 2]/r))
MeanSlopePlot <- ggplot(data = StabIndvReg, mapping = aes(x = Slope,
y = Mean)) + geom_point() + geom_text(aes(label = G),
size = 2.5, vjust = 1.25, colour = "black") + geom_vline(xintercept = 1,
linetype = "dotdash") + geom_hline(yintercept = mean(StabIndvReg$Mean),
linetype = "dotdash") + labs(x = "Slope", y = "Mean") +
scale_x_continuous(sec.axis = dup_axis(), labels = scales::comma) +
scale_y_continuous(sec.axis = dup_axis(), labels = scales::comma) +
theme_bw()
return(list(StabIndvReg = StabIndvReg, MeanSlopePlot = MeanSlopePlot))
}
<bytecode: 0xe431010>
<environment: namespace:stability>
One of the problems in the data 'df1' is the column name is 'R' instead of "Rep" which was passed into the function. Second, the terms passed into the formula are quosures. we could change it to string with quo_names and then construct formula with paste
add_anova1 <- function (.data, .y, .rep, .gen, .env) {
y1 <- quo_name(enquo(.y))
r1 <- quo_name(enquo(.rep))
g1 <- quo_name(enquo(.gen))
e1 <- quo_name(enquo(.env))
fm <- formula(paste0(y1, "~", paste(e1, paste(r1, e1, sep=":"),
g1, paste(g1, e1, sep=":"), sep="+")))
fm1 <- lm(terms(fm, keep.order = TRUE), data = .data)
fm1ANOVA <- anova(fm1)
rownames(fm1ANOVA) <- c("Env", "Rep(Env)", "Gen", "Gen:Env",
"Residuals")
fm1ANOVA[1, 4] <- fm1ANOVA[1, 3]/fm1ANOVA[2, 3]
fm1ANOVA[2, 4] <- NA
fm1ANOVA[1, 5] <- 1 - pf(as.numeric(fm1ANOVA[1, 4]), fm1ANOVA[1,
1], fm1ANOVA[2, 1])
fm1ANOVA[2, 5] <- 1 - pf(as.numeric(fm1ANOVA[2, 4]), fm1ANOVA[2,
1], fm1ANOVA[5, 1])
class(fm1ANOVA) <- c("anova", "data.frame")
return(list(anova = fm1ANOVA))
}
YieldANOVA2 <- add_anova1(
.data = df1
, .y = Y
, .rep = R
, .gen = G
, .env = E
)
-checking with the output generated using 'ge_data' without changing the column names
all.equal(YieldANOVA, YieldANOVA2, check.attributes = FALSE)
#[1] TRUE
Similarly stab_reg could be changed
This question already has answers here:
How to use R's ellipsis feature when writing your own function?
(5 answers)
Split up `...` arguments and distribute to multiple functions
(4 answers)
Closed 6 years ago.
Is there a way to pass arbitrary arguments to more than one command inside a function? The following function clearly does not work but I hope it explains what I am trying to achieve.
test = function(x = rnorm(20), y = rnorm(20), ..., ---){
plot(x, y, type = "p", ...)
lines(x, y, ---)
}
The goal is to be able to write a function that creates plot with say lines and points and polygon and can take arbitrary arguments for each command and pass them to the respective commands without me having to explicitly specify arguments for each command.
Here is a hackish approach:
.. <- "/////" #or anything which won't be used as a valid parameter
f <- function(...){
arguments <- list(...)
if(.. %in% arguments){
i <- which(arguments == ..)
terms <- unlist(arguments[1:(i-1)])
factors <- unlist(arguments[(i+1):length(arguments)])
c(sum(terms),prod(factors))
}
}
Then, for example,
> f(2,3,4,..,7,8,10)
[1] 9 560
You could obviously extend the idea to multiple ... fields, each delimited with ..
OPTION 1
Function
test = function(x = rnorm(20), y = rnorm(20), plot_options = NA, ...){
if (is.na(plot_options) == FALSE){
eval(parse(text = paste0("plot(x, y, ", plot_options, ")")))
} else {
plot(x, y, type = "n")
}
lines(x, y, ...)
}
USAGE
test()
set.seed(42)
m = rnorm(20)
n = rnorm(20)
test(x = m, y = n,
plot_options = "type = 'p', col = 'red', pch = 19, xlab = 'Test Plot', ylab = 'Y-axis'")
OPTION 2 (#Gregor's Solution)
Function
test2 = function(x = rnorm(20), y = rnorm(20), ..., line_options){
plot(x, y, ...)
if (missing(line_options)) {
lines(x, y)
} else {
do.call(lines, c(list(x = x, y = y), line_options))
}
}
USAGE
par(mfrow = c(2, 2), mar = c(2, 2, 1, 1))
test2(main = 'default')
test2(line_options = list(lty = 2), main = 'line')
test2(col = 'red', main = 'plot')
test2(col = 'red', line_options = list(lty = 2, col = 'blue'), main = 'line and plot')
I have a problem with structure.
Here is an exemple of what I'm doing.
x <- c(211.50, 200.50, 148.60, 144.20, 132.20, 159.80, 107.70, 91.40, 63.10, 62.10, 55.70, 74.60, 224.90, 208.001, 45.80, 133.50, 122.70, 161.70, 160.00, 136.80, 92.20, 91.20, 79.20, 109.90, 244.60, 212.20, 147.20, 129.30, 118.50, 165.80, 120.60, 97.90, 69.30, 65.50, 59.10, 81.90, 94.15, 114.20, 131.03, 133.89, 132.25, 153.51)
y <- x
Ref <- c(rep("ref1",36), rep("ref2",6))
ID <- c(rep("id1",6), rep("id2",6),rep("id3",6),rep("id4",6),rep("id5",6),rep("id6",6),rep("id7",6))
data.split <- data.frame(Ref,ID,x,y)
l.ref <- ddply(data.split, .(Ref), "nrow")
vec1 <- c(rep(1,l.ref$nrow[1]))
for (i in 2:length(l.ref$Ref)) {
vec2 <- c(rep(i,l.ref$nrow[i]))
vec3 <- append(vec1,vec2, after =length(vec1))
vec1 <- vec3
}
vec_ref <- vec3
l.id <- ddply(data.split, .(ID), "nrow")
vec1 <- c(rep(1,l.id$nrow[1]))
for (i in 2:length(l.id$ID)) {
vec2 <- c(rep(i,l.id$nrow[i]))
vec3 <- append(vec1,vec2, after =length(vec1))
vec1 <- vec3
}
vec_id <- vec3
df <- structure(list(Ref = structure(vec_ref, .Label = l.ref$Ref, class = "factor"),
Id = structure(vec_id, .Label = l.id$ID, class = "factor"),
x = data.split$x, y = data.split$y),
.Names = c("Ref", "Id", "x", "y"),
row.names = c(NA, -length(data.split$x)), class = "data.frame")
ggplot(data = df, aes(x = x, y = y, colour = df$Ref)) +
geom_point(aes(shape = df$Id)) + scale_shape_manual(value=1:length(l.id$ID))
Warning messages:
1: In [<-.factor(*tmp*, is.na(values), value = "NA") :
invalid factor level, NAs generated
2: Removed 42 rows containing missing values (geom_point).
I have empty graphic, I don't understand what is the problem?
What I'm doing wrong?
You've messed up your factors in your data frame. Whats all that business with 'structure'? Lose it, and use this:
df = data.frame(Ref = factor(vec_ref,labels=l.ref$Ref),
Id = factor(vec_id,labels=l.id$ID),
x=data.split$x,y=data.split$y)
and then the ggplot works.