I'm trying to write a function to accept a data.frame (x
) and a column
from it. The function performs some calculations on x and later returns another data.frame. I'm stuck on the best-practices method to pass the column name to the function.
The two minimal examples fun1
and fun2
below produce the desired result, being able to perform operations on x$column
, using max()
as an example. However, both rely on the seemingly (at least to me) inelegant
- call to
substitute()
and possiblyeval()
- the need to pass the column name as a character vector.
fun1 <- function(x, column){
do.call("max", list(substitute(x[a], list(a = column))))
}
fun2 <- function(x, column){
max(eval((substitute(x[a], list(a = column)))))
}
df <- data.frame(B = rnorm(10))
fun1(df, "B")
fun2(df, "B")
I would like to be able to call the function as fun(df, B)
, for example. Other options I have considered but have not tried:
- Pass
column
as an integer of the column number. I think this would avoidsubstitute()
. Ideally, the function could accept either. with(x, get(column))
, but, even if it works, I think this would still requiresubstitute
- Make use of
formula()
andmatch.call()
, neither of which I have much experience with.
Subquestion: Is do.call()
preferred over eval()
?
You can just use the column name directly:
There's no need to use substitute, eval, etc.
You can even pass the desired function as a parameter:
Alternatively, using
[[
also works for selecting a single column at a time:Personally I think that passing the column as a string is pretty ugly. I like to do something like:
which will yield:
Notice how the specification of a data.frame is optional. you can even work with functions of your columns:
This answer will cover many of the same elements as existing answers, but this issue (passing column names to functions) comes up often enough that I wanted there to be an answer that covered things a little more comprehensively.
Suppose we have a very simple data frame:
and we'd like to write a function that creates a new column
z
that is the sum of columnsx
andy
.A very common stumbling block here is that a natural (but incorrect) attempt often looks like this:
The problem here is that
df$col1
doesn't evaluate the expressioncol1
. It simply looks for a column indf
literally calledcol1
. This behavior is described in?Extract
under the section "Recursive (list-like) Objects".The simplest, and most often recommended solution is simply switch from
$
to[[
and pass the function arguments as strings:This is often considered "best practice" since it is the method that is hardest to screw up. Passing the column names as strings is about as unambiguous as you can get.
The following two options are more advanced. Many popular packages make use of these kinds of techniques, but using them well requires more care and skill, as they can introduce subtle complexities and unanticipated points of failure. This section of Hadley's Advanced R book is an excellent reference for some of these issues.
If you really want to save the user from typing all those quotes, one option might be to convert bare, unquoted column names to strings using
deparse(substitute())
:This is, frankly, a bit silly probably, since we're really doing the same thing as in
new_column1
, just with a bunch of extra work to convert bare names to strings.Finally, if we want to get really fancy, we might decide that rather than passing in the names of two columns to add, we'd like to be more flexible and allow for other combinations of two variables. In that case we'd likely resort to using
eval()
on an expression involving the two columns:Just for fun, I'm still using
deparse(substitute())
for the name of the new column. Here, all of the following will work:So the short answer is basically: pass data.frame column names as strings and use
[[
to select single columns. Only start delving intoeval
,substitute
, etc. if you really know what you're doing.