Convert variables in data.table to formula

Viewed 308

I have a sample data.table data as below:

   VarName Formulae
1:       A      1+1
2:       B      A+3
3:       C     B*10
4:       D      A+C
5:       E      D/2

I want to convert the Formulae column into formulas, so that the output can become like:

  VarName Result
1:       A      2
2:       B      5
3:       C      50
4:       D      52
5:       E      26

Basically the VarName column is the variable name and the Formulae column is the corresponding formula.

A = 1+1
B = A+3
C = B*10
D = A+C
E = D/2

I have tried using the eval and parse functions like data$VarName = eval(parse(text = "data$Formulae")), however I could not get the desired output.

4 Answers

Loop through VarName replace them with Formulae within brackets, then evaluate:

res <- setNames(x$Formulae, x$VarName)

while(any(grepl(paste0(names(res), collapse = "|"), res))) {
  for(i in names(res)){
    res <- gsub(i, paste0("(", res[ i ], ")"), res, fixed = TRUE)
  }
}

#res, after replacements:
#                          A                          B 
#                      "1+1"                  "(1+1)+3" 
#                          C                          D 
#             "((1+1)+3)*10"     "(1+1)+(((1+1)+3)*10)" 
#                          E 
# "((1+1)+(((1+1)+3)*10))/2" 

# evaluate
sapply(res, function(i) eval(parse(text = i)))
#A  B  C  D  E 
#2  5 50 52 26 

One way to do this is to convert Formulae to actual one-sided formulae and then functions which are in turn evaluated inside of lst() which allows for the sequential building of objects. This relies on the metaprogramming functionality of the tidyverse framework rather than data.table.

library(dplyr)
library(purrr)

df <- data.frame(VarName = LETTERS[1:5],
                 Formulae = c("1+1", "A+3", "B*10", "A+C", "D/2"))

lst(!!!map(set_names(df$Formulae, df$VarName),
           ~ quo(
             as_mapper(reformulate(.x))()
           )))
$A
[1] 2

$B
[1] 5

$C
[1] 50

$D
[1] 52

$E
[1] 26

Or alternatively:

lst(!!!set_names(df$Formulae, df$VarName) %>% map(str2lang))

As noted in the comments below these require that the formulae are in a sequential order.

It's interesting to see another function for this task that can be useful in more complex (where the order of evaluation is not specified) cases -- delayedAssign assings a value to a name and only evaluates it when requested. This way, each object is evaluated sequentially until its value is reached. For example, consider the following "data.frame":

d = structure(list(v = c("a", "b", "A", "B", "C", "D", "E"), 
                   f = c("C+b", "A+B/D", "1+1", "A+3", "B*10", "A+C", "D/2")), 
              class = "data.frame", row.names = c(NA, -7L))

Then we setup a new environment (to avoid cluttering .GlobalEnv) and assign our variables:

e = new.env()
forms = parse(text = d$f)
for(i in 1:nrow(d)) do.call(delayedAssign, list(d$v[i], forms[[i]], e, e))

And evaluate:

unlist(mget(ls(e), e)) #or
unlist(eapply(e, eval))
#        A         B         C         D         a         E         b 
# 2.000000  5.000000 50.000000 52.000000 52.096154 26.000000  2.096154 

Using apply :

df <- data.frame("VarName"=c("X","Y"),"Formulae"=c("1+1","X+1"))
df$formulas <- apply(df,1,function(x)eval(parse(text = paste0(x["VarName"]," ~ ",x["Formulae"]))))

Using eval(parse(...)) structure was correct, but this should work properly. However, maybe someone will answer a cleaner proposition.

Take note that column "formulas" can not be a vector, so it is a list.

str(df)
'data.frame':   2 obs. of  3 variables:
 $ VarName : chr  "X" "Y"
 $ Formulae: chr  "1+1" "X+1"
 $ formulas:List of 2
  ..$ :Class 'formula'  language X ~ 1 + 1
  .. .. ..- attr(*, ".Environment")=<environment: 0x000002933f8904a8> 
  ..$ :Class 'formula'  language Y ~ X + 1
  .. .. ..- attr(*, ".Environment")=<environment: 0x000002933fb6f3b8> 

This can cause some headaches in dataframes usage. I suggest using mapping tools like purrr instead of concatening everything into a dataframe in this case.

Related