<p><div><div><div><div data-show-faces="false" data-height="21" data-width="100" data-layout="button_count" data-send="true" data-href="https://www.r-bloggers.com/a-recipe-for-recipes/"></div></div><div></div></div><div></div></div>
<div>
(This article was first published on <strong><a href="https://edwinth.github.io/blog/recipes_blog/"> That’s so Random</a></strong>, and kindly contributed to <a rel="nofollow" href="https://www.r-bloggers.com/">R-bloggers)</a> </div>
<p>If you build statistical or machine learning models, the <code>recipes</code> package can be useful for data preparation. A <code>recipe</code> object is a container that holds all the steps that should be performed to go from the raw data set to the set that is fed into model a algorithm. Once your recipe is ready it can be executed on a data set at once, to perform all the steps. Not only on the train set on which the recipe was created, but also on new data, such as test sets and data that should be scored by the model. This assures that new data gets the exact same preparation as the train set, and thus can be validly scored by the learned model. The author of <code>recipes</code>, Max Kuhn, has provided abundant material to familiarize yourself with the richness of the package, see <a rel="nofollow" href="https://cran.r-project.org/web/packages/recipes/vignettes/Simple_Example.html" target="_blank">here</a>, <a rel="nofollow" href="https://www.rstudio.com/resources/webinars/creating-and-preprocessing-a-design-matrix-with-recipes/" target="_blank">here</a>, and <a rel="nofollow" href="https://cran.r-project.org/web/packages/recipes/index.html" target="_blank">here</a>. I will not dwell on how to use the package. Rather, I’d like to share what in my opinion is a good way to create new <code>steps</code> and <code>checks</code> to the package<sup><a rel="nofollow" href="https://edwinth.github.io/blog/recipes_blog/#fn:1" target="_blank">1</a></sup>. Use of the package is probably intuitive. Developing new <code>steps</code> and <code>checks</code>, however, does require a little more understanding of the package inner workings. With this procedure, or recipe if you like, I hope you will find adding (and maybe even contributing) your own <code>steps</code> and <code>checks</code> becomes easier and more organized.</p>
<h2>S3 classes in <code>recipes</code></h2>
<p>Lets build a very simple recipe:</p>
<div>
<div>
<pre><code><span>library</span><span>(</span><span>tidyverse</span><span>)</span><span>
</span><span>library</span><span>(</span><span>recipes</span><span>)</span><span>
</span><span>rec</span><span> </span><span>&lt;-</span><span> </span><span>recipe</span><span>(</span><span>mtcars</span><span>)</span><span> </span><span>%>%</span><span>
</span><span>step_center</span><span>(</span><span>everything</span><span>())</span><span> </span><span>%>%</span><span>
</span><span>step_scale</span><span>(</span><span>everything</span><span>())</span><span> </span><span>%>%</span><span>
</span><span>check_missing</span><span>(</span><span>everything</span><span>())</span><span>
</span><span>rec</span><span> </span><span>%>%</span><span> </span><span>class</span><span>()</span><span>
</span></code></pre>
</div>
</div>
<div>
<div>
<pre><code>## [1] &quot;recipe&quot;
</code></pre>
</div>
</div>
<p>As mentioned a recipe is a container for the steps and checks. It is a list of class <code>recipe</code> on which the <code>prep</code>, <code>bake</code>, <code>print</code> and <code>tidy</code> methods do the work as described in their respective documentation files. The <code>steps</code> and <code>checks</code> added to the recipe are stored inside this list. As you can see below, each of them are of their own subclass, as well as of the generic <code>step</code> or <code>check</code> classes.</p>
<div>
<div>
<pre><code><span>rec</span><span>$</span><span>steps</span><span> </span><span>%>%</span><span> </span><span>map</span><span>(</span><span>class</span><span>)</span><span>
</span></code></pre>
</div>
</div>
<div>
<div>
<pre><code>## [[1]]
## [1] &quot;step_center&quot; &quot;step&quot;       
## 
## [[2]]
## [1] &quot;step_scale&quot; &quot;step&quot;      
## 
## [[3]]
## [1] &quot;check_missing&quot; &quot;check&quot;
</code></pre>
</div>
</div>
<p>Each subclass has the same four methods defined as the <code>recipe</code> class. As one of the methods is called on the recipe, it will call the same method on all the steps and checks that are added to the recipe. (Exception is <code>prep.recipe</code>, which does not only call<code>prep</code> on all the steps and checks, but also <code>bake</code>). This means that when implementing a new <code>step</code> or <code>check</code>, you should provide these four methods. Additionally, we’ll need the function that is called by the user to add it to the recipe object and a constructor (if you are not sure what this is, see <a rel="nofollow" href="https://adv-r.hadley.nz/s3.html" target="_blank">15.3.1 of Advanced R by Hadley Wickham</a>).</p>
<h2>The recipes for recipes</h2>
<p>When writing a new <code>step</code> or <code>check</code> you will probably be inclined to copy-paste an existing step and start tweaking it from the top. Thus, first writing the function, then the constructor, and then the methods one by one. I think this is suboptimal and can get messy quickly. My preferred way is to start by not bothering about <code>recipes</code> at all, but write a general function that does the preparation work on a single vector. Once you are happy with this function you sit and think about which arguments to this function should be provided with upfront. These should be added as arguments to the function that is called by the user. Next you think about which function arguments are statistics that should be learned on the train set in the <code>prep</code> part of the <code>recipe</code>. You’ll then go on and do the constructor that is called by both the main function and the <code>prep</code> method. Only then you’ll write the function that is called by the user. You’ll custom <code>step</code> or <code>check</code> is completed by writing the four methods, since the functionality is already created, these are mostly bookkeeping.</p>
<p>For both checks and steps I made a skeleton available, in which all the “overhead” that should be in a new step or check is present. This is more convenient than copy-paste and existing example and try to figure out what is step specific and should be erased. You’ll find the skeletons <a rel="nofollow" href="https://github.com/EdwinTh/recipe_for_recipes" target="_blank">on github</a>. Note that for creating your own steps and checks you should clone the <a rel="nofollow" href="https://github.com/topepo/recipes" target="_blank">package source code</a>, since the helper functions that are used are not exported by the package.</p>
<h2>Putting it to practice</h2>
<p>We are going to do two examples in which the <em>recipe for recipes</em> is applied.</p>
<h3>Example 1: A signed log</h3>
<p>First up is a signed-log (inspired by <a rel="nofollow" href="https://www.amazon.com/Practical-Data-Science-Nina-Zumel/dp/1617291560" target="_blank">Practical Data Science with R</a>), which is taking the log over the absolute value of a variable, multiplied by its original sign. Thus, enabling us to take logs over negative values. If a variable is between -1 and 1, we’ll set it to 0, otherwise things get messy.</p>
<h4>1) preparing the function</h4>
<p>This is what the function on a vector could look like, if we did not bother about <code>recipes</code>:</p>
<div>
<div>
<pre><code><span>signed_log</span><span> </span><span>&lt;-</span><span> </span><span>function</span><span>(</span><span>x</span><span>,</span><span> </span><span>base</span><span> </span><span>=</span><span> </span><span>exp</span><span>(</span><span>1</span><span>))</span><span> </span><span>{</span><span>
</span><span>ifelse</span><span>(</span><span>abs</span><span>(</span><span>x</span><span>)</span><span> </span><span></span><span> </span><span>1</span><span>,</span><span> </span><span>0</span><span>,</span><span> </span><span>sign</span><span>(</span><span>x</span><span>)</span><span> </span><span>*</span><span> </span><span>log</span><span>(</span><span>abs</span><span>(</span><span>x</span><span>),</span><span> </span><span>base</span><span> </span><span>=</span><span> </span><span>base</span><span>))</span><span>
</span><span>}</span><span>
</span></code></pre>
</div>
</div>
<h4>2) think about the arguments</h4>
<p>The only argument of the function is <code>base</code>, that should be provided upfront when adding the step to a recipe object. There are no statistics to be learned in the <code>prep</code> step.</p>
<h4>3) the constructor</h4>
<p>Now we are going to write the first of the recipes functions. This is the constructor that produces new instances of the object of class <code>step_signed_log</code>. The first four arguments are present in each step or check, they are therefore part of the skeletons. The <code>terms</code> argument will hold the information on which columns the step should be performed. For <code>role</code>, <code>train</code> and <code>skip</code>, please see the documentation in one of the skeletons. <code>base</code> is <code>step_signed_log</code> specific, as used in 1). In <code>prep.step_signed_log</code> the tidyselect arguments will be converted to a character vector holding the actual column names. <code>columns</code> will store these names in the <code>step_signed_log</code> object. This container argument will not be necessary if the columns names are also present in another way. For instance, <code>step_center</code> has the argument <code>means</code>, that will be populated by the means of the variables of the train set by its <code>prep</code> method. In the <code>bake</code> method the names of the columns to be prepared are already provided by the names of the means and there is need to use the <code>columns</code> argument.</p>
<div>
<div>
<pre><code><span>step_signed_log_new</span><span> </span><span>&lt;-</span><span>
</span><span>function</span><span>(</span><span>terms</span><span> </span><span>=</span><span> </span><span>NULL</span><span>,</span><span>
</span><span>role</span><span> </span><span>=</span><span> </span><span>NA</span><span>,</span><span>
</span><span>skip</span><span> </span><span>=</span><span> </span><span>FALSE</span><span>,</span><span>
</span><span>trained</span><span> </span><span>=</span><span> </span><span>FALSE</span><span>,</span><span>
</span><span>base</span><span> </span><span>=</span><span> </span><span>NULL</span><span>,</span><span>
</span><span>columns</span><span> </span><span>=</span><span> </span><span>NULL</span><span>)</span><span> </span><span>{</span><span>
</span><span>step</span><span>(</span><span>
</span><span>subclass</span><span> </span><span>=</span><span> </span><span>&quot;signed_log&quot;</span><span>,</span><span>
</span><span>terms</span><span> </span><span>=</span><span> </span><span>terms</span><span>,</span><span>
</span><span>role</span><span> </span><span>=</span><span> </span><span>role</span><span>,</span><span>
</span><span>skip</span><span> </span><span>=</span><span> </span><span>skip</span><span>,</span><span>
</span><span>trained</span><span> </span><span>=</span><span> </span><span>trained</span><span>,</span><span>
</span><span>base</span><span> </span><span>=</span><span> </span><span>base</span><span>,</span><span>
</span><span>columns</span><span> </span><span>=</span><span> </span><span>columns</span><span>
</span><span>)</span><span>
</span><span>}</span><span>
</span></code></pre>
</div>
</div>
<h4>4) the function to add it to the recipe</h4>
<p>Next up is the function that will be called by the user to add the step to its recipe. You’ll see the internal helper function <code>add_step</code> is called. It will expand the recipe with the <code>step_signed_log</code> object that is produced by the constructor we just created.</p>
<div>
<div>
<pre><code><span>step_signed_log</span><span> </span><span>&lt;-</span><span>
</span><span>function</span><span>(</span><span>recipe</span><span>,</span><span>
</span><span>...</span><span>,</span><span>
</span><span>role</span><span> </span><span>=</span><span> </span><span>NA</span><span>,</span><span>
</span><span>skip</span><span> </span><span>=</span><span> </span><span>FALSE</span><span>,</span><span>
</span><span>trained</span><span> </span><span>=</span><span> </span><span>FALSE</span><span>,</span><span>
</span><span>base</span><span> </span><span>=</span><span> </span><span>exp</span><span>(</span><span>1</span><span>),</span><span>
</span><span>columns</span><span> </span><span>=</span><span> </span><span>NULL</span><span>)</span><span> </span><span>{</span><span>
</span><span>add_step</span><span>(</span><span>
</span><span>recipe</span><span>,</span><span>
</span><span>step_signed_log_new</span><span>(</span><span>
</span><span>terms</span><span> </span><span>=</span><span> </span><span>ellipse_check</span><span>(</span><span>...</span><span>),</span><span>
</span><span>role</span><span> </span><span>=</span><span> </span><span>role</span><span>,</span><span>
</span><span>skip</span><span> </span><span>=</span><span> </span><span>skip</span><span>,</span><span>
</span><span>trained</span><span> </span><span>=</span><span> </span><span>trained</span><span>,</span><span>
</span><span>base</span><span> </span><span>=</span><span> </span><span>base</span><span>,</span><span>
</span><span>columns</span><span> </span><span>=</span><span> </span><span>columns</span><span>
</span><span>)</span><span>
</span><span>)</span><span>
</span><span>}</span><span>
</span></code></pre>
</div>
</div>
<h4>5) the <code>prep</code> method</h4>
<p>As recognized in 2) we don’t have to do much in the <code>prep</code> method of this particular step, since the preparation of new sets does not depend on statistics learned on the train set. The only thing we do here is applying the internal function <code>terms_select</code> function to replace the <code>tidyselect</code> selections, by the actual names of the columns on which <code>step_signed_log</code> should be applied. We call the constructor again, indicating that the step is trained and we supplying the column names at the <code>columns</code> argument.</p>
<div>
<div>
<pre><code><span>prep.step_signed_log</span><span> </span><span>&lt;-</span><span> </span><span>function</span><span>(</span><span>x</span><span>,</span><span>
</span><span>training</span><span>,</span><span>
</span><span>info</span><span> </span><span>=</span><span> </span><span>NULL</span><span>,</span><span> </span><span>...</span><span>)</span><span> </span><span>{</span><span>
</span><span>col_names</span><span> </span><span>&lt;-</span><span> </span><span>terms_select</span><span>(</span><span>x</span><span>$</span><span>terms</span><span>,</span><span> </span><span>info</span><span> </span><span>=</span><span> </span><span>info</span><span>)</span><span>
</span><span>step_signed_log_new</span><span>(</span><span>
</span><span>terms</span><span> </span><span>=</span><span> </span><span>x</span><span>$</span><span>terms</span><span>,</span><span>
</span><span>role</span><span> </span><span>=</span><span> </span><span>x</span><span>$</span><span>role</span><span>,</span><span>
</span><span>skip</span><span> </span><span>=</span><span> </span><span>x</span><span>$</span><span>skip</span><span>,</span><span>
</span><span>trained</span><span> </span><span>=</span><span> </span><span>TRUE</span><span>,</span><span>
</span><span>base</span><span> </span><span>=</span><span> </span><span>x</span><span>$</span><span>base</span><span>,</span><span>
</span><span>columns</span><span> </span><span>=</span><span> </span><span>col_names</span><span>
</span><span>)</span><span>
</span><span>}</span><span>
</span></code></pre>
</div>
</div>
<h4>6) the <code>bake</code> method</h4>
<p>We are now ready to apply the baking function, designed at 1), inside the recipes framework. We loop through the variables, apply the function and return the updated data frame.</p>
<div>
<div>
<pre><code><span>bake.step_signed_log</span><span> </span><span>&lt;-</span><span> </span><span>function</span><span>(</span><span>object</span><span>,</span><span>
</span><span>newdata</span><span>,</span><span>
</span><span>...</span><span>)</span><span> </span><span>{</span><span>
</span><span>col_names</span><span> </span><span>&lt;-</span><span> </span><span>object</span><span>$</span><span>columns</span><span>
</span><span>for</span><span> </span><span>(</span><span>i</span><span> </span><span>in</span><span> </span><span>seq_along</span><span>(</span><span>col_names</span><span>))</span><span> </span><span>{</span><span>
</span><span>col</span><span> </span><span>&lt;-</span><span> </span><span>newdata</span><span>[[</span><span> </span><span>col_names</span><span>[</span><span>i</span><span>]</span><span> </span><span>]]</span><span>
</span><span>newdata</span><span>[,</span><span> </span><span>col_names</span><span>[</span><span>i</span><span>]]</span><span> </span><span>&lt;-</span><span>
</span><span>ifelse</span><span>(</span><span>abs</span><span>(</span><span>col</span><span>)</span><span> </span><span></span><span> </span><span>1</span><span>,</span><span> </span><span>0</span><span>,</span><span> </span><span>sign</span><span>(</span><span>col</span><span>)</span><span> </span><span>*</span><span> </span><span>log</span><span>(</span><span>abs</span><span>(</span><span>col</span><span>),</span><span> </span><span>base</span><span> </span><span>=</span><span> </span><span>object</span><span>$</span><span>base</span><span>))</span><span>
</span><span>}</span><span>
</span><span>as_tibble</span><span>(</span><span>newdata</span><span>)</span><span>
</span><span>}</span><span>
</span></code></pre>
</div>
</div>
<h4>7) the <code>print</code> method</h4>
<p>This assures pretty printing of the <code>recipe</code> object to which <code>step_signed_log</code> is added. You use the internal <code>printer</code> function with a message specific for the step.</p>
<div>
<div>
<pre><code><span>print.step_signed_log</span><span> </span><span>&lt;-</span><span>
</span><span>function</span><span>(</span><span>x</span><span>,</span><span> </span><span>width</span><span> </span><span>=</span><span> </span><span>max</span><span>(</span><span>20</span><span>,</span><span> </span><span>options</span><span>()</span><span>$</span><span>width</span><span> </span><span>-</span><span> </span><span>30</span><span>),</span><span> </span><span>...</span><span>)</span><span> </span><span>{</span><span>
</span><span>cat</span><span>(</span><span>&quot;Taking the signed log for &quot;</span><span>,</span><span> </span><span>sep</span><span> </span><span>=</span><span> </span><span>&quot;&quot;</span><span>)</span><span>
</span><span>printer</span><span>(</span><span>x</span><span>$</span><span>columns</span><span>,</span><span> </span><span>x</span><span>$</span><span>terms</span><span>,</span><span> </span><span>x</span><span>$</span><span>trained</span><span>,</span><span> </span><span>width</span><span> </span><span>=</span><span> </span><span>width</span><span>)</span><span>
</span><span>invisible</span><span>(</span><span>x</span><span>)</span><span>
</span><span>}</span><span>
</span></code></pre>
</div>
</div>
<h4>8) the <code>tidy</code> method</h4>
<p>Finally, <code>tidy</code> will add a line for this step to the data frame when the <code>tidy</code> method is called on a recipe.</p>
<div>
<div>
<pre><code><span>tidy.step_signed_log</span><span> </span><span>&lt;-</span><span> </span><span>function</span><span>(</span><span>x</span><span>,</span><span> </span><span>...</span><span>)</span><span> </span><span>{</span><span>
</span><span>if</span><span> </span><span>(</span><span>is_trained</span><span>(</span><span>x</span><span>))</span><span> </span><span>{</span><span>
</span><span>res</span><span> </span><span>&lt;-</span><span> </span><span>tibble</span><span>(</span><span>terms</span><span> </span><span>=</span><span> </span><span>x</span><span>$</span><span>columns</span><span>)</span><span>
</span><span>}</span><span> </span><span>else</span><span> </span><span>{</span><span>
</span><span>res</span><span> </span><span>&lt;-</span><span> </span><span>tibble</span><span>(</span><span>terms</span><span> </span><span>=</span><span> </span><span>sel2char</span><span>(</span><span>x</span><span>$</span><span>terms</span><span>))</span><span>
</span><span>}</span><span>
</span><span>res</span><span>
</span><span>}</span><span>
</span></code></pre>
</div>
</div>
<p>Lets do a quick check to see if it works as expected</p>
<div>
<div>
<pre><code><span>recipe</span><span>(</span><span>data_frame</span><span>(</span><span>x</span><span> </span><span>=</span><span> </span><span>1</span><span>))</span><span> </span><span>%>%</span><span> </span><span>step_signed_log</span><span>(</span><span>x</span><span>)</span><span> </span><span>%>%</span><span> </span><span>prep</span><span>()</span><span> </span><span>%>%</span><span> </span><span>bake</span><span>(</span><span>data_frame</span><span>(</span><span>x</span><span> </span><span>=</span><span> </span><span>-3</span><span>:</span><span>3</span><span>))</span><span>
</span></code></pre>
</div>
</div>
<div>
<div>
<pre><code>## # A tibble: 7 x 1
##            x
##        </code></pre></div></div>