Can eval be called within a data frame with variables defined in that data frame?

Viewed 346

Consider:

quotes<-alist(x, x+1, x+2)
data.frame(x=c(5, 10, 15), lapply(quotes, eval))

This does not work because eval cannot find x. Is there any way to get eval to recognize the variables that were defined in the data frame? I know that I could assign the data frame to some variables and then build it up with functions like transform, but I'm looking to do all of this within my call to data.frame.

For a possible version of the intended outputs, we can use this:

quotes<-alist(x, x+1, x+2)
x=c(5, 10, 15)
data.frame(x, lapply(quotes, eval))
5 Answers

Have you noticed that the simpler call data.frame(x, y = x) wouldn't work either ?

data.frame() uses standard evaluation so in your case they'll be evaluated in the global environment.

If you name your quotes elements you'll be able to do tibble(x, !!!quotes) though, because tibble works differently.

Technically the following might be acceptable to you, we cheat by creating a temp value in the global environment, which we then remove on exit.

(I use evalq only to be able to use on.exit)

quotes<-alist(x, x+1, x+2)

df <- data.frame(
  x = c(5, 10, 15) ->> .t.e.m.p., 
  evalq({
    on.exit(rm(.t.e.m.p., envir = .GlobalEnv))
    lapply(quotes, eval, list(x= .t.e.m.p.))
  }))

df
#>    x c.5..10..15. c.6..11..16. c.7..12..17.
#> 1  5            5            6            7
#> 2 10           10           11           12
#> 3 15           15           16           17

ls(all.names = TRUE)
#> [1] "df"     "quotes"

Created on 2021-05-11 by the reprex package (v0.3.0)

This looks of course horrible, and using transform, within or tibble is probably a wiser choice.

In case you don't mind when x is created in .GlobalEnv, and potentially overwriting it, you can use <- inside data.frame.

quotes<-alist(x, x+1, x+2)

data.frame(x=x <- c(5, 10, 15), lapply(quotes, eval))
#   x c.5..10..15. c.6..11..16. c.7..12..17.
#1  5            5            6            7
#2 10           10           11           12
#3 15           15           16           17

rm(x)

You can call it inside local but then x is not created in the .GlobalEnv and eval needs to be told to look in current environment.

local(data.frame(x=x <- c(5, 10, 15), lapply(quotes, eval, environment())))
#   x c.5..10..15. c.6..11..16. c.7..12..17.
#1  5            5            6            7
#2 10           10           11           12
#3 15           15           16           17

It is also possible to assign the values to a different name but here eval needs a list with the name x. Note that in this case TMP would be first created or overwritten and if needed, afterwards removed with rm from the .GlobalEnv.

data.frame(x=TMP <- c(5, 10, 15), lapply(quotes, eval, list(x=TMP)))
#   x c.5..10..15. c.6..11..16. c.7..12..17.
#1  5            5            6            7
#2 10           10           11           12
#3 15           15           16           17

rm(TMP)

Other (but not asked/wanted) possibilities might be using transform:

transform(data.frame(x=c(5, 10, 15)), y=lapply(quotes, eval, environment()))
#   x y.c.5..10..15. y.c.6..11..16. y.c.7..12..17.
#1  5              5              6              7
#2 10             10             11             12
#3 15             15             16             17

within:

within(data.frame(x=c(5, 10, 15)), y <- lapply(quotes, eval, environment()))
#   x         y
#1  5 5, 10, 15
#2 10 6, 11, 16
#3 15 7, 12, 17

or tibble:

tibble::tibble(x=c(5, 10, 15), y=sapply(quotes, eval, environment()))
## A tibble: 3 x 2
#      x y[,1]  [,2]  [,3]
#  <dbl> <dbl> <dbl> <dbl>
#1     5     5     6     7
#2    10    10    11    12
#3    15    15    16    17

And finally (for reference) the intended output from the question:

x <- c(5, 10, 15)
data.frame(x, lapply(quotes, eval))
#   x c.5..10..15. c.6..11..16. c.7..12..17.
#1  5            5            6            7
#2 10           10           11           12
#3 15           15           16           17

rm(x)

Similar to previous answers using tibble, but adding a data.table solution for completeness:

data.table(x=c(5, 10, 15))[, lapply(quotes, eval, envir=environment())]
#    V1 V2 V3
# 1:  5  6  7
# 2: 10 11 12
# 3: 15 16 17

Debugging data.frame(x=c(5, 10, 15), lapply(quotes, eval)) leads to:

x <- list(...)
#Error in FUN(X[[i]], ...) : object 'x' not found

This is due to evaluation of second argument:

lapply(quotes, eval)
#Error in FUN(X[[i]], ...) : object 'x' not found

A workaround seems here difficult to find without previously defining x in the environment.

tibble allows to use previous variables during definition of further variables, as it relies on non standard evaluation with quos(...) instead of list(...) to process arguments :

function (..., .rows = NULL, .name_repair = c("check_unique", 
  "unique", "universal", "minimal")) 
{
  xs <- quos(...)
  is.null <- map_lgl(xs, quo_is_null)
  tibble_quos(xs[!is.null], .rows, .name_repair)
}

With tibble:

tibble::tibble(x=c(1,2,3), result= sapply(quotes,function(quo) eval(quo)))

# A tibble: 3 x 2
      x result[,1]  [,2]  [,3]
  <dbl>      <dbl> <dbl> <dbl>
1     1          1     2     3
2     2          2     3     4
3     3          3     4     5

This is however not yet the expected result because quotes need to be processed row wise with x, for example with mapply:

tibble::tibble(x=c(1,2,3), result = mapply(function(quo,x) eval(quo),quotes,x))

# A tibble: 3 x 2
      x result
  <dbl>  <dbl>
1     1      1
2     2      3
3     3      5

As others have mentioned, I think the best solution is to use tibble() instead of data.frame(). However, if you want to avoid the tibble dependency, the best solution would be, I think, to create a custom function my_frame() which implements the functionality you need. This can be fairly simple if you're only ever going to be 'defining' values in the first column:

my_frame <- function(x, quotes) {
  data.frame(x, lapply(quotes, eval, environment()))
}

my_frame(c(5, 10, 15), quotes)
#>    x c.5..10..15. c.6..11..16. c.7..12..17.
#> 1  5            5            6            7
#> 2 10           10           11           12
#> 3 15           15           16           17
Related