How to refer to a column in a function

Viewed 86

I was trying to write a function that contains two actions, and the first one includes subsetting a dataframe.

Let's say I have these two dataframes.

      ID   Var1
   1   5     3
   2   6     1



      ID   Var2
   1   5     9
   2   6     2

And my function is like this

mu_fuc = function(df, condition) {
    workingdf = subset(df, condition < 3)

###pass working df to the other action
  } 

I am aware that for this first action, I can use conditional slicing as a work around. However, as I try to work on the other action, I realized I still have to refer to a column name in an dataframe for another existing function.

I tried as.name(condition), but it did not work.

Thank you for your time. Any suggestion is highly appreciated.

---------Update on 12/26-----------

The method using eval worked well for the function once, as below.

meta = function(sub_data, y) {
  y <- eval(as.list(match.call())$y, sub_data)
  workingdf <- subset(sub_data, y != 999) ##this successfully grab the column named y in the dataframe## 
  
  meta1 <- metacor(y,  ##this successfully grab the column named y in the dataframe## 
                   n, 
                   data = workingdf,
                   studlab = workingdf$Author_year,
                   sm = "ZCOR",
                   method.tau = "SJ", 
                   comb.fixed = F)
  return(meta1)

But somehow the same approach did not work in the following code.

mod_analysis = function(meta, moderator){
  workingdf <- meta$data
  moderator <- eval(as.list(match.call())$moderator, workingdf) 
  
  output = metareg(meta, moderator)
  return(output)
}  

Then it was this error message:

Error in eval(predvars, data, env) : object 'moderator' not found 

I don't know why it worked for the first function but not the second.

3 Answers

One can convert the condition from a simple column reference to an expression, enabling the function argument to include the right hand side of an expression instead of hard coding it into the function. This can be accomplished with a couple of functions from the rlang package, enquo() and eval_tidy().

We'll illustrate this with a subsetting function and the mtcars data frame.

aSubsetFunction <- function(df,condition){
   require(rlang)
   condition <- enquo(condition)
   rows_value <- eval_tidy(condition, df)
   stopifnot(is.logical(rows_value))
   df[rows_value, ,drop = FALSE]
}

The condition <- enquo(condition) line quotes the condition expression. The eval_tidy() function evaluates the quoted expression, using df as a data mask. The output from eval_tidy(), rows_value, is a vector of logical values (TRUE / FALSE), which we use on the row dimension of the input data frame with the [ form of the extract operator. We use stopifnot() to generate an error if rows_value is not a vector of logical values.

We call the function twice to illustrate that it works with multiple columns in the data frame.

aSubsetFunction(mtcars,mpg > 25)
aSubsetFunction(mtcars,carb > 4)

...and the output:

> aSubsetFunction(mtcars,mpg > 25)
Loading required package: rlang
                mpg cyl  disp  hp drat    wt  qsec vs am gear carb
Fiat 128       32.4   4  78.7  66 4.08 2.200 19.47  1  1    4    1
Honda Civic    30.4   4  75.7  52 4.93 1.615 18.52  1  1    4    2
Toyota Corolla 33.9   4  71.1  65 4.22 1.835 19.90  1  1    4    1
Fiat X1-9      27.3   4  79.0  66 4.08 1.935 18.90  1  1    4    1
Porsche 914-2  26.0   4 120.3  91 4.43 2.140 16.70  0  1    5    2
Lotus Europa   30.4   4  95.1 113 3.77 1.513 16.90  1  1    5    2
> aSubsetFunction(mtcars,carb > 4)
               mpg cyl disp  hp drat   wt qsec vs am gear carb
Ferrari Dino  19.7   6  145 175 3.62 2.77 15.5  0  1    5    6
Maserati Bora 15.0   8  301 335 3.54 3.57 14.6  0  1    5    8

Using data from the original post, the solution works as follows.

df1 <- read.csv(text="ID,Val1
                5,3
                6,1")
df2 <- read.csv(text="ID,Val2
                5,9
                6,2")
aSubsetFunction(df1,Val1 < 3)
aSubsetFunction(df2,Val2 < 3)

...and the output:

> aSubsetFunction(df1,Val1 < 3)
  ID Val1
2  6    1
> aSubsetFunction(df2,Val2 < 3)
  ID Val2
2  6    2

Having illustrated the approach, we can use the order of object evaluation in R to simplify the function down to a single line of R code:

aSubsetFunction <- function(df,condition){
   require(rlang)
   df[eval_tidy(enquo(condition), df), ,drop = FALSE]
}

...which produces the same output as listed above.

> aSubsetFunction(mtcars,mpg > 25)
Loading required package: rlang
                mpg cyl  disp  hp drat    wt  qsec vs am gear carb
Fiat 128       32.4   4  78.7  66 4.08 2.200 19.47  1  1    4    1
Honda Civic    30.4   4  75.7  52 4.93 1.615 18.52  1  1    4    2
Toyota Corolla 33.9   4  71.1  65 4.22 1.835 19.90  1  1    4    1
Fiat X1-9      27.3   4  79.0  66 4.08 1.935 18.90  1  1    4    1
Porsche 914-2  26.0   4 120.3  91 4.43 2.140 16.70  0  1    5    2
Lotus Europa   30.4   4  95.1 113 3.77 1.513 16.90  1  1    5    2
> aSubsetFunction(mtcars,carb > 4)
               mpg cyl disp  hp drat   wt qsec vs am gear carb
Ferrari Dino  19.7   6  145 175 3.62 2.77 15.5  0  1    5    6
Maserati Bora 15.0   8  301 335 3.54 3.57 14.6  0  1    5    8
> 

Epilogue: why can't we use variable substitution with subset()?

From an initial look at the question, one might expect that we could resolve the question with the following code.

subset2 <- function(df,condition){
   subset(df,df[[condition]] > 4)
}

subset2(mtcars,carb)

However, this fails with an object not found error:

 Error in (function(x, i, exact) if (is.matrix(i)) as.matrix(x)[[i]] else .subset2(x,  : 
  object 'carb' not found 

Once again Advanced R provides an explanation, directly quoting from the documentation for subset().

This is a convenience function intended for use interactively. For programming it is better to use the standard subsetting functions like [, and in particular the non-standard evaluation of argument subset can have unanticipated consequences.

Bottom line: it's important to understand Base R non-standard evaluation when writing functions to automate one's analysis because the assumptions coded into various R functions can produce unexpected results. This is especially true of modeling functions like lm() that rely on formula(), as Wickham describes in Advanced R: wrapping modeling functions.

References: Advanced R, Chapter 20 section 4, Chapter 20 section 6

I guess you can try match.call with eval

mu_fuc <- function(df, condition) {
  condition <- eval(as.list(match.call())$condition, df)
  workingdf <- subset(df, condition < 3)
  workingdf
}

which enables

> mu_fuc(df1, Var1)
  ID Var1
2  6    1

> mu_fuc(df2, Var2)
  ID Var2
2  6    2

Try this with indexing:

#Funtion
mu_fuc = function(df, condition) {
  workingdf <- df[df[[condition]]<3,]
  return(workingdf)
} 
#Apply
mu_fuc(df1,'Var1')

Output:

mu_fuc(df1,'Var1')
  ID Var1
2  6    1

Some data used:

#Data
df1 <- structure(list(ID = 5:6, Var1 = c(3L, 1L)), class = "data.frame", row.names = c("1", 
"2"))
Related