Author: Roman Tsegelskyi, Gergely Daróczi
Date: 2021-06-11

# Using pander with knitr

#### Roman Tsegelskyi, Gergely Daróczi

#### 2021-06-11

While the report generation functionality of `pander` and `knitr` do overlap, we feel that the most powerful way to use `R/knitr/pander` for report generation is to utilize them together. This short vignette aims to explain how to embed `pander` output in reports generated by `knitr`. If you are not aware of `knitr`, be sure to check out the project’s homepage for extensive documentation and examples.

One of `knitr`’s most useful features is the ability to convert tables to output format on the fly. For example:

    head(iris)
    #>   Sepal.Length Sepal.Width Petal.Length Petal.Width Species
    #> 1          5.1         3.5          1.4         0.2  setosa
    #> 2          4.9         3.0          1.4         0.2  setosa
    #> 3          4.7         3.2          1.3         0.2  setosa
    #> 4          4.6         3.1          1.5         0.2  setosa
    #> 5          5.0         3.6          1.4         0.2  setosa
    #> 6          5.4         3.9          1.7         0.4  setosa
    knitr::kable(head(iris))

Sepal.Length | Sepal.Width | Petal.Length | Petal.Width | Species
--- | --- | --- | --- | ---
5.1 | 3.5 | 1.4 | 0.2 | setosa
4.9 | 3.0 | 1.4 | 0.2 | setosa
4.7 | 3.2 | 1.3 | 0.2 | setosa
4.6 | 3.1 | 1.5 | 0.2 | setosa
5.0 | 3.6 | 1.4 | 0.2 | setosa
5.4 | 3.9 | 1.7 | 0.4 | setosa

However, `kable` table generator is simple by design, and does not capture all the variety of classes that `R` has to offer. For example, `CrossTable` and `tabular` are not supported:

    library(descr, quietly = TRUE)
    ct <- CrossTable(mtcars$gear, mtcars$cyl)
    #> Warning in chisq.test(tab, correct = FALSE, ...): Chi-squared approximation may
    #> be incorrect
    knitr::kable(ct)
    #> Error in as.data.frame.default(x): cannot coerce class '"CrossTable"' to a data.frame
    library(tables, quietly = TRUE)
    tab <- tabular( (Species + 1) ~ (n=1) + Format(digits=2)*
             (Sepal.Length + Sepal.Width)*(mean + sd), data=iris )
    knitr::kable(tab)

term | term | term | term | term
--- | --- | --- | --- | ---
50 | 5.006 | 0.352489687213451 | 3.428 | 0.379064369096289
50 | 5.936 | 0.516171147063863 | 2.77 | 0.313798323378411
50 | 6.588 | 0.635879593274432 | 2.974 | 0.322496638172637
150 | 5.84333333333333 | 0.828066127977863 | 3.05733333333333 | 0.435866284936698

This is where `pander` comes in handy, as `pander` supports rendering for many popular classes:

    methods(pander)
    #>  [1] pander.anova*           pander.aov*             pander.aovlist*
    #>  [4] pander.Arima*           pander.call*            pander.cast_df*
    #>  [7] pander.character*       pander.clogit*          pander.coxph*
    #> [10] pander.cph*             pander.CrossTable*      pander.data.frame*
    #> [13] pander.data.table*      pander.Date*            pander.default*
    #> [16] pander.density*         pander.describe*        pander.ets*
    #> [19] pander.evals*           pander.factor*          pander.formula*
    #> [22] pander.ftable*          pander.function*        pander.glm*
    #> [25] pander.Glm*             pander.gtable*          pander.htest*
    #> [28] pander.image*           pander.irts*            pander.list*
    #> [31] pander.lm*              pander.lme*             pander.logical*
    #> [34] pander.lrm*             pander.manova*          pander.matrix*
    #> [37] pander.microbenchmark*  pander.name*            pander.nls*
    #> [40] pander.NULL*            pander.numeric*         pander.ols*
    #> [43] pander.orm*             pander.polr*            pander.POSIXct*
    #> [46] pander.POSIXlt*         pander.prcomp*          pander.randomForest*
    #> [49] pander.rapport*         pander.rlm*             pander.sessionInfo*
    #> [52] pander.smooth.spline*   pander.stat.table*      pander.summary.aov*
    #> [55] pander.summary.aovlist* pander.summary.glm*     pander.summary.lm*
    #> [58] pander.summary.lme*     pander.summary.manova*  pander.summary.nls*
    #> [61] pander.summary.polr*    pander.summary.prcomp*  pander.summary.rms*
    #> [64] pander.summary.survreg* pander.summary.table*   pander.survdiff*
    #> [67] pander.survfit*         pander.survreg*         pander.table*
    #> [70] pander.tabular*         pander.ts*              pander.zoo*
    #> see '?methods' for accessing help and source code

Also, `pander` is integrated with `knitr` by default. `pander` simply identifies if `knitr` is running in the background, and if so, it uses `capture.output` to return the resulting string as an `knit_asis` object, meaning that you do not need to specify the `results='asis'` option in your knitr chunk:

    library(descr, quietly = TRUE)
    pander(CrossTable(mtcars$gear, mtcars$cyl))
    #> Warning in chisq.test(tab, correct = FALSE, ...): Chi-squared approximation may
    #> be incorrect

mtcars$gear | mtcars$cyl 4 | 6 | 8 | Total
--- | --- | --- | --- | ---
3 N Chi-square Row(%) Column(%) Total(%) | 1 3.3502 6.6667% 9.0909% 3.125% | 2 0.5003 13.3333% 28.5714% 6.250% | 12 4.5054 80.0000% 85.7143% 37.500% | 15 46.8750%
4 N Chi-square Row(%) Column(%) Total(%) | 8 3.6402 66.6667% 72.7273% 25.000% | 4 0.7202 33.3333% 57.1429% 12.500% | 0 5.2500 0.0000% 0.0000% 0.000% | 12 37.5000%
5 N Chi-square Row(%) Column(%) Total(%) | 2 0.0460 40.0000% 18.1818% 6.250% | 1 0.0080 20.0000% 14.2857% 3.125% | 2 0.0161 40.0000% 14.2857% 6.250% | 5 15.6250%
Total | 11 34.375% | 7 21.875% | 14 43.75% | 32

    library(tables, quietly = TRUE)
    tab <- tabular( (Species + 1) ~ (n=1) + Format(digits=2)*
             (Sepal.Length + Sepal.Width)*(mean + sd), data=iris )
    pander(tab)

Species | n | Sepal.Length mean | sd | Sepal.Width mean | sd
--- | --- | --- | --- | --- | ---
setosa | 50 | 5.01 | 0.35 | 3.43 | 0.38
versicolor | 50 | 5.94 | 0.52 | 2.77 | 0.31
virginica | 50 | 6.59 | 0.64 | 2.97 | 0.32
All | 150 | 5.84 | 0.83 | 3.06 | 0.44

In a nutshell, this is achieved by modification that whenever you call `pander` inside of a `knitr` document, instead of returning the markdown text to the standard output (the default behavior), `pander` returns a `knit_asis` class object, which renders correctly in the resulting document — without the double comment chars, thus properly rendering the tables in HTML, PDF, or other document formats.

If you don’t want the results of `pander` to be converted automatically, just set `knitr.auto.asis` to `FALSE` using `panderOptions`:

    panderOptions('knitr.auto.asis', FALSE)
    pander(head(iris))
    #>
    #> -------------------------------------------------------------------
    #>  Sepal.Length   Sepal.Width   Petal.Length   Petal.Width   Species
    #> -------------- ------------- -------------- ------------- ---------
    #>      5.1            3.5           1.4            0.2       setosa
    #>
    #>      4.9             3            1.4            0.2       setosa
    #>
    #>      4.7            3.2           1.3            0.2       setosa
    #>
    #>      4.6            3.1           1.5            0.2       setosa
    #>
    #>       5             3.6           1.4            0.2       setosa
    #>
    #>      5.4            3.9           1.7            0.4       setosa
    #> -------------------------------------------------------------------

    panderOptions('knitr.auto.asis', TRUE)

#### Rendering markdown inside loop/vectorized function

One frequenly asked question is how to use `pander` with `knitr` in a loop or vectorized function. For example, we have 3 tables that we want to render using `lapply`:

    dfs <- list(mtcars[1:3, 1:4], mtcars[4:6, 1:4], mtcars[7:9, 1:4])
    lapply(dfs, pander)
    #> [[1]]
    #> [1] "\n---------------------------------------------\n      &nbsp;         mpg    cyl   disp   hp  \n------------------- ------ ----- ------ -----\n   **Mazda RX4**      21     6    160    110 \n\n **Mazda RX4 Wag**    21     6    160    110 \n\n  **Datsun 710**     22.8    4    108    93  \n---------------------------------------------\n\n"
    #> attr(,"class")
    #> [1] "knit_asis"
    #> attr(,"knit_cacheable")
    #> [1] NA
    #>
    #> [[2]]
    #> [1] "\n-------------------------------------------------\n        &nbsp;           mpg    cyl   disp   hp  \n----------------------- ------ ----- ------ -----\n  **Hornet 4 Drive**     21.4    6    258    110 \n\n **Hornet Sportabout**   18.7    8    360    175 \n\n      **Valiant**        18.1    6    225    105 \n-------------------------------------------------\n\n"
    #> attr(,"class")
    #> [1] "knit_asis"
    #> attr(,"knit_cacheable")
    #> [1] NA
    #>
    #> [[3]]
    #> [1] "\n-------------------------------------------\n     &nbsp;       mpg    cyl   disp    hp  \n---------------- ------ ----- ------- -----\n **Duster 360**   14.3    8     360    245 \n\n **Merc 240D**    24.4    4    146.7   62  \n\n  **Merc 230**    22.8    4    140.8   95  \n-------------------------------------------\n\n"
    #> attr(,"class")
    #> [1] "knit_asis"
    #> attr(,"knit_cacheable")
    #> [1] NA

As you can see, this doesn’t work correctly because `pander` tries to return a `knit_asis` class object when run inside `knitr`, but for loops/vectorized functions this results in incorrect output. The recommended way to solve this is to disable this behavior by setting `knitr.auto.asis` to `FALSE` using `panderOptions`. However, we also need to tell `knitr` to convert the table on the fly by specifying `results='asis'` in the chunk options:

    panderOptions('knitr.auto.asis', FALSE)
    dfs <- list(mtcars[1:3, 1:4], mtcars[4:6, 1:4], mtcars[7:9, 1:4])
    invisible(lapply(dfs, pander))

| mpg | cyl | disp | hp
--- | --- | --- | --- | ---
Mazda RX4 | 21 | 6 | 160 | 110
Mazda RX4 Wag | 21 | 6 | 160 | 110
Datsun 710 | 22.8 | 4 | 108 | 93

| mpg | cyl | disp | hp
--- | --- | --- | --- | ---
Hornet 4 Drive | 21.4 | 6 | 258 | 110
Hornet Sportabout | 18.7 | 8 | 360 | 175
Valiant | 18.1 | 6 | 225 | 105

| mpg | cyl | disp | hp
--- | --- | --- | --- | ---
Duster 360 | 14.3 | 8 | 360 | 245
Merc 240D | 24.4 | 4 | 146.7 | 62
Merc 230 | 22.8 | 4 | 140.8 | 95