A common scenario in my day-to-day data analysis is that I have many objects, typically data frames, and for each of them I want to create a new section in my Quarto/R Markdown document with its summary statistics. The operations are usually the same; only the data is different. The manual way is to copy and paste the same chunk of code for generating the summaries and change only the variable name. For instance:
## Section for Data AData A has `r nrow(data_a)` rows and `r ncol(data_a)` columns.```{r}summary_fn(data_a)```## Section for Data BData B has `r nrow(data_b)` rows and `r ncol(data_b)` columns.```{r}summary_fn(data_b)```## Section for Data C...
This becomes a pain when there are many objects and I have to change the
names one by one. This post shows how to automate this task with
knit_child from the knitr rendering engine.
Using the knitr Engine
The knitr engine has built-in support for dynamic, parameterized
documents. For R Markdown, you can pass data to the document using the
params argument in rmarkdown::render() (learn more at
https://bookdown.org/yihui/rmarkdown/parameterized-reports.html). This
is intended for rendering one big parameterized .Rmd document.
To insert sub-contents into either .qmd or .Rmd files,
knitr::knit_child is the function you need. Similar to params in
rmarkdown::render, you can pass an environment object to
knitr::knit_child using the envir argument. knit_child returns a
character string that can be embedded directly into a document, so it’s
used together with the chunk option output = 'asis' (Quarto) or
results = 'asis' (R Markdown).
knit_child accepts either an inline string text or a file path
file. Below we use the inline string to generate a summary of the
iris data frame.
```{r}# chunk option output = 'asis'res <- knitr::knit_child(text = c("```{r}","summary(data)","```"), envir = rlang::env(data = iris), quiet = TRUE)cat(res, sep = "\n")```
summary(data)
… 6 lines#> Sepal.Length Sepal.Width Petal.Length Petal.Width Species#> Min. :4.30 Min. :2.00 Min. :1.00 Min. :0.1 setosa :50#> 1st Qu.:5.10 1st Qu.:2.80 1st Qu.:1.60 1st Qu.:0.3 versicolor:50#> Median :5.80 Median :3.00 Median :4.35 Median :1.3 virginica :50#> Mean :5.84 Mean :3.06 Mean :3.76 Mean :1.2#> 3rd Qu.:6.40 3rd Qu.:3.30 3rd Qu.:5.10 3rd Qu.:1.8#> Max. :7.90 Max. :4.40 Max. :6.90 Max. :2.5
Putting it all together, we can divide the job into a main document
main.Rmd and a template document _template.Rmd. In the main
document, we collect all the data, loop through them and insert them
into the template document using knitr::knit_child. Then we can use
the asis output option to include the template document in the main
document.
This will also work for Quarto documents as long as we are using the knitr engine.
The example main and template documents are shown below:
In main.Rmd
---title: "Dynamic contents in R Markdown and Quarto with `knit_child()`"toc: true---Generate random dataset```{r}#| echo: falserandom_data <- function(n) {data.frame(x = rnorm(n),y = rnorm(n))}``````{r}all_data <- purrr::map(1:5, ~ random_data(round(runif(1, 10, 100))))``````{r}render_child <- function(data, i) {res <- knitr::knit_child(text = xfun::read_utf8("_template.Rmd"),envir = rlang::env(data = data, i = i),quiet = TRUE)cat(res, sep = "\n")cat("\n")}```Here is a list of reports```{r}#| results: "asis"#| echo: falsepurrr::iwalk(all_data, render_child)```
In _template.Rmd
## Dataset `r i`Dataset `r i` has `r nrow(data)` rows.### Summary```{r}#| echo: falsesummary(data)```### Plot```{r}#| echo: false#| fig-align: centerplot(data)```
I’ve defined a wrapper function render_child() that uses
knit_child() under the hood. The key is that you can pass arbitrary
values using the envir argument: pass whatever the template document
needs as a named list and convert it into an environment object with
rlang::env().
When you run the main document, it generates a list of random data frames, generates a child document for each using the template document, and inserts the results back into the main document. The result is shown below:
A Note on knitr::knit_expand
R Markdown
Cookbook
also introduces knitr::knit_expand(), which inserts contents into a
template. For example:
knitr::knit_expand(text = "The value of `a` is {{a}}",a = 1)#> [1] "The value of `a` is 1"
Since you can also pass a file argument to knit_expand, I was
tempted to use this function with knit_child when I started writing
this post. For example:
res <- knitr::knit_expand(file = "template.Rmd",data = iris[1:5, ])
Then you can access iris in the template document like so:
---title: template document---```{r}{{ data }}```
But this will not work. knitr::knit_expand simply finds all the
placeholders marked by { }, interpolates them and then passes the text
to the knitting functions. This works fine if you only need to insert
simple primitives, such as strings and numbers, but not if you want to
pass data.frames or other complex objects. The knitting functions will
then see a code chunk like this:
```{r}c(5.1, 4.9, 4.7, 4.6, 5)c(3.5, 3, 3.2, 3.1, 3.6)c(1.4, 1.4, 1.3, 1.5, 1.4)c(0.2, 0.2, 0.2, 0.2, 0.2)c(1, 1, 1, 1, 1)```
which is not valid R syntax.
Without knitr
If you are using Quarto with an engine other than knitr, I found no
construct similar to knitr::knit_child. Although Quarto offers a
native variables
mechanism for reading values from configuration files, it’s intended for
static data that should be shared across multiple documents. It also has
the same problem as knitr::knit_expand: you are limited to the data
structures YAML supports.