<p><div><div><div><div data-width="100" data-show-faces="false" data-send="true" data-layout="button_count" data-height="21" data-href="http://www.r-bloggers.com/one-function-to-run-them-all-or-just-eval/"></div></div><div><a data-url="http://www.r-bloggers.com/one-function-to-run-them-all-or-just-eval/" data-counturl="http://www.r-bloggers.com/one-function-to-run-them-all-or-just-eval/" data-count="horizontal" data-text="One function to run them all… Or just eval" class="twitter-share-button" data-via="rbloggers" href="https://twitter.com/share"></a></div><div></div></div><div></div></div>
<div>
(This article was first published on <strong><a href="http://www.vesnam.com/Rblog/one-function-to-run-them-all/"> Fun with R</a></strong>, and kindly contributed to <a rel="nofollow" href="http://www.r-bloggers.com/">R-bloggers)</a> </div>
<p>When I was writing a <a target="_blank" href="http://www.vesnam.com/Rblog/modeling-in-systems-biology/">sysBio</a> package, I needed a function that can be customized based on the user specified model. Such function would include user specified variables (parameters, rates, etc.) with assigned values, functions/expression that use those variables, and the ordinary differential equations that describe the model. After doing some reading, the <em>eval</em> function seemed like a logical choice. It was pretty easy to figure out how to use it and to implement the custom function I needed (if interested, you can see it in <a target="_blank" href="https://github.com/Vessy/sysBio/blob/master/R/makeODEs.R">makeODEs.R</a> and <a target="_blank" href="https://github.com/Vessy/sysBio/blob/master/R/simulateModel.R">simulateModel.R</a>). </p>
<p>This made me think about how easy it would be to use <em>eval</em> to create a general purpose function. Before I go any further, let me first say that I am aware of issues of using <em>eval</em> (security, harder readability and debugging, extra complexity, etc). If the hypothetical situation described below is a real-life problem I am working on, I would weight pros and cons before calling the presented <em>eval</em> approach the chosen approach. But this is just me being curious and we can pretend it is ok. </p>
<p>So, let’s say that we are working with people (users) whose goals are to create reports from different real-life data. Let’s also assume that these data are represented in tabular format and that reports represent summaries based on mathematical functions applied on the data presented in the tables. Additionally, let’s assume that our users have phobia from writing or learning any type of code (I have to add this, because my significant other had a very strong stand against <em>eval</em> and advocated for teaching the users to learn or even copy/paste the code in instead). </p>
<p>Clearly, using R, we could address users’ needs pretty easy – we could load and store users’ data in data frames, (check the data for consistency), use some type of <em>ply</em> function to summarize the data, and print the reports in the output files. Ignoring the read/check/print parts, we could create a summary in a single line command. </p>
<p>Now, let’s assume that users often want to change their summaries, for example every few months they remove one of the old summary functions and add one new . In order to keep users’ demands current, we would have to go and change our summary commands quite often. </p>
<p>This is where <em>eval</em> approach comes to play. Can we create a single, general function that will be able to handle different, specific users’ data summary requests?</p>
<p>The answer is yes.<br>
However, note that the presented approach does not do any data evaluation and checking, nor it performs security check on user passed strings (which, in real-life, would be pretty big security concern).</p>
<p>We will assume that user’s data is already in the form of data frame (with <em>stringsAsFactors</em> set to <em>FALSE</em>). Furthermore, we will assume that users provide a list of variables (that correspond to table column names) over which they want to split and summarize the data (character vector), as well as a two-column table (data frame) which will contain the user specified name of the function used for summary (first column) and the function used for summary (second column). </p>
<p>As data example, we can use <em>mtcars</em> data set from the <a target="_blank" href="https://stat.ethz.ch/R-manual/R-devel/library/datasets/html/00Index.html">datasets</a> library. We will assume that the user is interested in information about gears and for each gear wants to calculate some custom value (defined as <em>mpg+2*carb</em>, total hp (defined as <em>sum(hp)</em>), and average mpg (defined as <em>mean(mpg)</em>):</p>
<p><code>ex1_df &lt;- datasets::mtcars</code><br>
<code>ex1_vars &lt;- &quot;gear&quot;</code><br>
<code>ex1_formulas &lt;- data.frame(labs=c(&quot;cust_val&quot;, &quot;total_hp&quot;, &quot;avg_mpg&quot;), ress=c(&quot;mpg+2*carb&quot;, &quot;sum(hp)&quot;, &quot;mean(mpg)&quot;), stringsAsFactors = FALSE)</code></p>
<p>If we were coding the user's request, we could use the ddply function from the <a target="_blank" href="https://cran.r-project.org/web/packages/plyr/index.html">plyr</a> package as follow:<br>
<code><br>
ex1_trueResult &lt;- plyr::ddply(ex1_df, .variable=c(&quot;gear&quot;), function(x) 
  data.frame(cust_val = x$mpg+2*x$carb, 
             total_hp = sum(x$hp),
             avg_mpg = mean(x$mpg)))
</code></p>
<p>The first thing I tried to do was to create a function that will recreate the <em>ddply</em> call:</p>
<p><div class="feedlyNoScript">
<pre><code>oneForMany_first &lt;- function(df_to_use = NA, cols_fix = NA, cols_ops = NA){
  
  if ((is.null(df_to_use)) | (is.null(cols_fix)) | (is.null(cols_ops)))
    stop(&quot;You did not specify all arguments!&quot;)
  
  if ((!is.data.frame(df_to_use)) | (!is.character(cols_fix)) | (!is.data.frame(cols_ops)))
    stop(&quot;Wrong argument type(s)! First and third arguments need to be a data frame; second argument needs to be a vector of characters!&quot;)
  
  if (ncol(cols_ops)>2)
    stop(&quot;Data frame defined by third argument should have only two columns!&quot;)
  
  # Transform assignments into appropriate format
  col_names.sorted &lt;- sort(colnames(df_to_use))
  col_names.length &lt;- length(col_names.sorted)
  
  # DF needs to be in stringsAsFactors=FALSE mode
  for (j in 1:nrow(cols_ops)){
    
    for (i in col_names.length:1){
      cols_ops[j,2] &lt;- stringr::str_replace_all(as.character(cols_ops[j,2]), col_names.sorted[i], paste(&quot;hlp[&quot;,as.character(i), &quot;]&quot;, sep=&quot;&quot;))
    }
    
    for (i in 1:col_names.length){
      cols_ops[j,2] &lt;- stringr::str_replace_all(as.character(cols_ops[j,2]),  paste(&quot;hlp\[&quot;,as.character(i), &quot;\]&quot;, sep=&quot;&quot;), paste(&quot;x$&quot;, col_names.sorted[i], sep=&quot;&quot;))
    }
  }
  
  # Create a vector of functions
  func_list &lt;- parse(text = paste(as.character(cols_ops[,1]), &quot; = &quot;, cols_ops[,2], sep=&quot;&quot;))
  
  # Run plyer
  plyr::ddply(df_to_use, .variable=cols_fix, function(x) data.frame(eval(func_list)))
}</code></pre>
<p></p>
<p>Running this function will create a summary, but only for avg_mpg:<br>
<code><br>
ex1_res01 &lt;- oneForMany_first(ex1_df, ex1_vars, ex1_formulas)
</code></p>
<p>This is because when an expression vector is passed to eval, the result of evaluating is the result of evaluating the last element. To get around this issue, we can use a <em>sapply</em> command:</p>
<p><div class="feedlyNoScript">
<pre><code>oneForMany_second &lt;- function(df_to_use = NA, cols_fix = NA, cols_ops = NA){
  
  if ((is.null(df_to_use)) | (is.null(cols_fix)) | (is.null(cols_ops)))
    stop(&quot;You did not specify all arguments!&quot;)
  
  if ((!is.data.frame(df_to_use)) | (!is.character(cols_fix)) | (!is.data.frame(cols_ops)))
    stop(&quot;Wrong argument type(s)! First and third arguments need to be a data frame; second argument needs to be a vector of characters!&quot;)
  
  if (ncol(cols_ops)>2)
    stop(&quot;Data frame defined by third argument should have only two columns!&quot;)
  
  # Transform assignments into appropriate format
  col_names.sorted &lt;- sort(colnames(df_to_use))
  col_names.length &lt;- length(col_names.sorted)
  
  # DF needs to be in stringsAsFactors=FALSE mode
  for (j in 1:nrow(cols_ops)){
    
    for (i in col_names.length:1){
      cols_ops[j,2] &lt;- stringr::str_replace_all(as.character(cols_ops[j,2]), col_names.sorted[i], paste(&quot;hlp[&quot;,as.character(i), &quot;]&quot;, sep=&quot;&quot;))
    }
    
    for (i in 1:col_names.length){
      cols_ops[j,2] &lt;- stringr::str_replace_all(as.character(cols_ops[j,2]),  paste(&quot;hlp\[&quot;,as.character(i), &quot;\]&quot;, sep=&quot;&quot;), paste(&quot;x$&quot;, col_names.sorted[i], sep=&quot;&quot;))
    }
  }
  
  # Create a vector of functions
  func_list &lt;- parse(text = paste(as.character(cols_ops[,1]), &quot; = &quot;, cols_ops[,2], sep=&quot;&quot;))
  
  # Run plyer
  plyr::ddply(df_to_use, .variable=cols_fix, function(x) data.frame(sapply(func_list, function(y) eval(y))))
}
</code></pre>
<p></p>
<p>This time, all expressions have been evaluated and all calculations have been performed. However, due to the column names mismatches (the function did not assigned the proper names to the columns), the data is not in desired format and one would need to parse it in order to use it (which can be done, but it is not what we want).<br>
<code><br>
ex1_res02 &lt;- oneForMany_second(ex1_df, ex1_vars, ex1_formulas)
</code></p>
<p>Similarly, if we use a list instead of a vector, all expressions are evaluated and all values are be calculated.<br>
<div class="feedlyNoScript">
<pre><code>oneForMany_third &lt;- function(df_to_use = NA, cols_fix = NA, cols_ops = NA){
  
  if ((is.null(df_to_use)) | (is.null(cols_fix)) | (is.null(cols_ops)))
    stop(&quot;You did not specify all arguments!&quot;)
  
  if ((!is.data.frame(df_to_use)) | (!is.character(cols_fix)) | (!is.data.frame(cols_ops)))
    stop(&quot;Wrong argument type(s)! First and third arguments need to be a data frame; second argument needs to be a vector of characters!&quot;)
  
  if (ncol(cols_ops)>2)
    stop(&quot;Data frame defined by third argument should have only two columns!&quot;)
  
  # Transform assignments into appropriate format
  col_names.sorted &lt;- sort(colnames(df_to_use))
  col_names.length &lt;- length(col_names.sorted)
  
  # DF needs to be in stringsAsFactors=FALSE mode
  for (j in 1:nrow(cols_ops)){
    
    for (i in col_names.length:1){
      cols_ops[j,2] &lt;- stringr::str_replace_all(as.character(cols_ops[j,2]), col_names.sorted[i], paste(&quot;hlp[&quot;,as.character(i), &quot;]&quot;, sep=&quot;&quot;))
    }
    
    for (i in 1:col_names.length){
      cols_ops[j,2] &lt;- stringr::str_replace_all(as.character(cols_ops[j,2]),  paste(&quot;hlp\[&quot;,as.character(i), &quot;\]&quot;, sep=&quot;&quot;), paste(&quot;x$&quot;, col_names.sorted[i], sep=&quot;&quot;))
    }
  }
  
  # Create a vector of functions
  func_list &lt;- parse(text = paste(&quot;as.list(c(&quot;, paste(as.character(cols_ops[,1]), &quot; = &quot;, cols_ops[,2], sep=&quot;&quot;, collapse=&quot;, &quot;), &quot;))&quot;, sep=&quot;&quot;))
  
  # Run plyer
  plyr::ddply(df_to_use, .variable=cols_fix, function(x) data.frame(eval(func_list)))
}</code></pre>
<p></p>
<p>But the resulting data is also not in desired format and should be parsed.<br>
<code><br>
ex1_res03 &lt;- oneForMany_third(ex1_df, ex1_vars, ex1_formulas)
</code></p>
<p>Now, let's try something slightly different. Let's use <em>eval</em> to create a function that returns a data frame with desired columns (user specified functions):<br>
<div class="feedlyNoScript">
<pre><code>oneForMany_fourth &lt;- function(df_to_use = NA, cols_fix = NA, cols_ops = NA){
  
  if ((is.null(df_to_use)) | (is.null(cols_fix)) | (is.null(cols_ops)))
    stop(&quot;You did not specify all arguments!&quot;)
  
  if ((!is.data.frame(df_to_use)) | (!is.character(cols_fix)) | (!is.data.frame(cols_ops)))
    stop(&quot;Wrong argument type(s)! First and third arguments need to be a data frame; second argument needs to be a vector of characters!&quot;)
  
  if (ncol(cols_ops)>2)
    stop(&quot;Data frame defined by third argument should have only two columns!&quot;)
  
  # Transform assignments into appropriate format
  col_names.sorted &lt;- sort(colnames(df_to_use))
  col_names.length &lt;- length(col_names.sorted)
  
  # DF needs to be in stringsAsFactors=FALSE mode
  for (j in 1:nrow(cols_ops)){
    
    for (i in col_names.length:1){
      cols_ops[j,2] &lt;- stringr::str_replace_all(as.character(cols_ops[j,2]), col_names.sorted[i], paste(&quot;hlp[&quot;,as.character(i), &quot;]&quot;, sep=&quot;&quot;))
    }
    
    for (i in 1:col_names.length){
      cols_ops[j,2] &lt;- stringr::str_replace_all(as.character(cols_ops[j,2]),  paste(&quot;hlp\[&quot;,as.character(i), &quot;\]&quot;, sep=&quot;&quot;), paste(&quot;x$&quot;, col_names.sorted[i], sep=&quot;&quot;))
    }
  }
  
  func_list  &lt;- function(x){
    eval(parse(text = paste(as.character(cols_ops[,1]), &quot; &lt;- &quot;, cols_ops[,2], sep=&quot;&quot;)))
    eval(parse(text = paste(&quot;data.frame(&quot;, paste(as.character(cols_ops[,1]), sep=&quot;&quot;, collapse=&quot;, &quot;), &quot;)&quot;, sep=&quot;&quot;)))
  }
  
  
  plyr::ddply(df_to_use, .variable=cols_fix,function(x) func_list(x))
}
</code></pre>
<p><br>
<code>ex1_res04 &lt;- oneForMany_fourth(ex1_df, ex1_vars, ex1_formulas)</code></p>
<p>This seems to work. We can test to confirm that the obtain results are the same as the hard coded ones:<br>
<code>testthat::expect_that(ex1_trueResult, testthat::equals(ex1_res04))</code></p>
<p>Alternatively, instead of trying to replicate the form of the hard-coded function, we could use the <em>do.call</em> function. For do.call, we need to specify the function we use and provide a list of arguments.<br>
<div class="feedlyNoScript">
<pre><code>oneForMany_fifth &lt;- function(df_to_use = NA, cols_fix = NA, cols_ops = NA){
  
  if ((is.null(df_to_use)) | (is.null(cols_fix)) | (is.null(cols_ops)))
    stop(&quot;You did not specify all arguments!&quot;)
  
  if ((!is.data.frame(df_to_use)) | (!is.character(cols_fix)) | (!is.data.frame(cols_ops)))
    stop(&quot;Wrong argument type(s)! First and third arguments need to be a data frame; second argument needs to be a vector of characters!&quot;)
  
  if (ncol(cols_ops)>2)
    stop(&quot;Data frame defined by third argument should have only two columns!&quot;)
  
  func_list &lt;- eval(parse(text=
                            paste(&quot;as.list(c(&quot;,
                                  paste(&quot;parse(text=&quot;&quot;, as.character(cols_ops[,2]), sep=&quot;&quot;, collapse = &quot;&quot;), &quot;),
                                  &quot;&quot;)))&quot;, sep=&quot;&quot;)))
  
  func_list &lt;- setNames(func_list, as.character(cols_ops[,1]))
  
  result_mutate &lt;- do.call(plyr::ddply, c(list(.data = df_to_use, .variables = cols_fix,  .fun = plyr::mutate), func_list))

  result_mutate[, c(cols_fix, as.character(cols_ops[,1]))]
}</code></pre>
<p></p>
<p>Note that we used the <em>mutate</em> instead of <em>summarise</em> function even thought we want data summary and this choice required additional filtering step. While <em>summarise</em> function perform group-wise summaries and creates a new data frame based on specified variables and functions, it fails when functions return data of different length (in our case, <em>sum(hp)</em> and <em>mean(mpg)</em> return single values, while <em>mpg+2*carb</em> result in multiple values).The <em>mutate</em> function adds columns to the existing data frame, so unequal lengths are not the issue.Another important thing to mention is that the arguments list has to be a named list.</p>
<p>We can run it and test the results<br>
<code>ex1_res05 &lt;- oneForMany_fifth(ex1_df, ex1_vars, ex1_formulas)
testthat::expect_that(ex1_trueResult, testthat::equals(ex1_res05))
</code></p>
<p>This and another example are also available at:</p>
<p><div class="feedlyNoScript">
<pre><code># Using eval to create a function that will be able to produce a data summary (using the plyr::ddply function)
# based on the user specified data frame, variables (that correspond to table column names) over which they 
# want to split and summarize the data (character vector), and a two-column table (data frame) which contains 
# the user specified name of the function used for summary (first column) and the function used for summary (second column)
#
# For more info, see http://www.vesnam.com/Rblog/one-function-to-run-them-all/
#

# Data, variable, and formulas used for data summary
ex1_df &lt;- datasets::mtcars
ex1_vars &lt;- &quot;gear&quot;
ex1_formulas &lt;- data.frame(labs=c(&quot;cust_val&quot;, &quot;total_hp&quot;, &quot;avg_mpg&quot;), ress=c(&quot;mpg+2*carb&quot;, &quot;sum(hp)&quot;, &quot;mean(mpg)&quot;), stringsAsFactors = FALSE)

# This is the expected result
ex1_trueResult &lt;- plyr::ddply(ex1_df, .variable=c(&quot;gear&quot;), function(x) 
  data.frame(cust_val = x$mpg+2*x$carb, 
             total_hp = sum(x$hp),
             avg_mpg = mean(x$mpg)))

# Testing version #!
# It passes an expression vector to eval and only the last element is evaluated.
ex1_res01 &lt;- oneForMany_first(ex1_df, ex1_vars, ex1_formulas)

# Testing version #2
# Using sapply to overcome the eval expression vector issue
# Everything is calculated, but data is not in desired format (due to column names mismatches)
# and one would need to parse it
ex1_res02 &lt;- oneForMany_secpmd(ex1_df, ex1_vars, ex1_formulas)

# Testing version #3
# Using a list instead of vector; everything is calculated, but data is not in desired format
# and one would need to parse it
ex1_res03 &lt;- oneForMany_third(ex1_df, ex1_vars, ex1_formulas)

# Testing version #4
# Using eval to create a function that returns a data frame with desired columns (user specified functions)
ex1_res04 &lt;- oneForMany_fourth(ex1_df, ex1_vars, ex1_formulas)
# We can test to confirm that the results are OK
testthat::expect_that(ex1_trueResult, testthat::equals(ex1_res04))

# Testing version #5
# Using do.call to call plyer
ex1_res05 &lt;- oneForMany_fifth(ex1_df, ex1_vars, ex1_formulas)
# We can test to confirm that the results are OK
testthat::expect_that(ex1_trueResult, testthat::equals(ex1_res05))

# Different data set
ex2_df &lt;- datasets::quakes
ex2_vars &lt;- c(&quot;lat&quot;, &quot;long&quot;)
ex2_formulas &lt;-  data.frame(what=c(&quot;l1&quot;, &quot;l2&quot;, &quot;l3&quot;, &quot;l4&quot;), 
                      how=c(&quot;lat+2*long&quot;, &quot;depth^2/log(mag+1)&quot;, &quot;(lat+long)/(stations*0.5)&quot;, &quot;mean(mag)&quot;), stringsAsFactors = FALSE)

ex2_trueResult &lt;- plyr::ddply(ex2_df, .variable=c(&quot;lat&quot;, &quot;long&quot;), function(x) 
  data.frame(l1 = x$lat+2*x$long, 
             l2 = x$depth^2/log(x$mag+1), 
             l3 = (x$lat+x$long)/(x$stations*0.5),
             l4 = mean(x$mag)))

ex2_res04 &lt;- oneForMany_fourth(ex2_df, ex2_vars, ex2_formulas)
testthat::expect_that(ex2_trueResult, testthat::equals(ex2_res04))

ex2_res05 &lt;- oneForMany_fifth(ex2_df, ex2_vars, ex2_formulas)
testthat::expect_that(ex2_trueResult, testthat::equals(ex2_res05))</code></pre>
<p></p>
<p><div><div><div><div data-width="55" data-show-faces="false" data-send="true" data-layout="box_count" data-height="62" data-href="http://www.r-bloggers.com/one-function-to-run-them-all-or-just-eval/"></div></div><div><a data-url="http://www.r-bloggers.com/one-function-to-run-them-all-or-just-eval/" data-counturl="http://www.r-bloggers.com/one-function-to-run-them-all-or-just-eval/" data-count="vertical" data-text="One function to run them all… Or just eval" class="twitter-share-button" data-via="rbloggers" href="https://twitter.com/share"></a></div><div></div></div><div></div></div>
<div>
<div>To <strong>leave a comment</strong> for the author, please follow the link and comment on their blog: <strong><a href="http://www.vesnam.com/Rblog/one-function-to-run-them-all/"> Fun with R</a></strong>.</div>
<hr>
<a rel="nofollow" href="http://www.r-bloggers.com/">R-bloggers.com</a> offers <strong><a rel="nofollow" href="http://feedburner.google.com/fb/a/mailverify?uri=RBloggers">daily e-mail updates</a></strong> about <a title="The R Project for Statistical Computing" rel="nofollow" href="http://www.r-project.org/">R</a> news and <a title="R tutorials" rel="nofollow" href="http://www.r-bloggers.com/?s=tutorial">tutorials</a> on topics such as: <a title="Data science" rel="nofollow" href="http://www.r-bloggers.com/?s=data%20science">Data science</a>, <a title="Big Data" rel="nofollow" href="http://www.r-bloggers.com/?s=Big%20Data">Big Data, <a title="R jobs" rel="nofollow" href="http://www.r-users.com/">R jobs</a>, visualization (<a title="ggplot and ggplot2 tutorials" rel="nofollow" href="http://www.r-bloggers.com/?s=ggplot2">ggplot2</a>, <a title="Boxplots using lattice and ggplot2 tutorials" rel="nofollow" href="http://www.r-bloggers.com/?s=boxplot">Boxplots</a>, <a title="Maps and gis" rel="nofollow" href="http://www.r-bloggers.com/?s=map">maps</a>, <a title="Animation in R" rel="nofollow" href="http://www.r-bloggers.com/?s=animation">animation</a>), programming (<a title="RStudio IDE for R" rel="nofollow" href="http://www.r-bloggers.com/?s=RStudio">RStudio</a>, <a title="Sweave and literate programming" rel="nofollow" href="http://www.r-bloggers.com/?s=sweave">Sweave</a>, <a title="LaTeX in R" rel="nofollow" href="http://www.r-bloggers.com/?s=LaTeX">LaTeX</a>, <a title="SQL and databases" rel="nofollow" href="http://www.r-bloggers.com/?s=SQL">SQL</a>, <a title="Eclipse IDE for R" rel="nofollow" href="http://www.r-bloggers.com/?s=eclipse">Eclipse</a>, <a title="git and github, Version Control System" rel="nofollow" href="http://www.r-bloggers.com/?s=git">git</a>, <a title="Large data in R using Hadoop" rel="nofollow" href="http://www.r-bloggers.com/?s=hadoop">hadoop</a>, <a title="Web Scraping of google, facebook, yahoo, twitter and more using R" rel="nofollow" href="http://www.r-bloggers.com/?s=Web+Scraping">Web Scraping</a>) statistics (<a title="Regressions and ANOVA analysis tutorials" rel="nofollow" href="http://www.r-bloggers.com/?s=regression">regression</a>, <a title="principal component analysis tutorial" rel="nofollow" href="http://www.r-bloggers.com/?s=PCA">PCA</a>, <a title="Time series" rel="nofollow" href="http://www.r-bloggers.com/?s=time+series">time series</a>, <a title="finance trading" rel="nofollow" href="http://www.r-bloggers.com/?s=trading">trading</a>) and more...
<img height="1" alt="" width="1" src="http://feeds.feedburner.com/~r/RBloggers/~4/yE_Oqy6yuxU"></a></div></div></div></div></div></div></div>