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)
#> 
#> Attaching package: 'Hmisc'
#> The following objects are masked from 'package:base':
#> 
#>     format.pval, round.POSIXt, trunc.POSIXt, units
tab <- tabular( (Species + 1) ~ (n=1) + Format(digits=2)*
         (Sepal.Length + Sepal.Width)*(mean + sd), data=iris )
knitr::kable(tab)
#> Error in dimnames(x) <- dn: length of 'dimnames' [2] not equal to array extent

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

methods(pander)
#>  [1] pander.Arima*           pander.CrossTable*     
#>  [3] pander.Date*            pander.Glm*            
#>  [5] pander.NULL*            pander.POSIXct*        
#>  [7] pander.POSIXlt*         pander.anova*          
#>  [9] pander.aov*             pander.aovlist*        
#> [11] pander.call*            pander.cast_df*        
#> [13] pander.character*       pander.clogit*         
#> [15] pander.coxph*           pander.cph*            
#> [17] pander.data.frame*      pander.data.table*     
#> [19] pander.default*         pander.density*        
#> [21] pander.describe*        pander.ets*            
#> [23] pander.evals*           pander.factor*         
#> [25] pander.formula*         pander.ftable*         
#> [27] pander.function*        pander.glm*            
#> [29] pander.gtable*          pander.htest*          
#> [31] pander.image*           pander.irts*           
#> [33] pander.list*            pander.lm*             
#> [35] pander.lme*             pander.logical*        
#> [37] pander.lrm*             pander.manova*         
#> [39] pander.matrix*          pander.microbenchmark* 
#> [41] pander.name*            pander.nls*            
#> [43] pander.numeric*         pander.ols*            
#> [45] pander.orm*             pander.polr*           
#> [47] pander.prcomp*          pander.randomForest*   
#> [49] pander.rapport*         pander.rlm*            
#> [51] pander.sessionInfo*     pander.smooth.spline*  
#> [53] pander.stat.table*      pander.summary.aov*    
#> [55] pander.summary.aovlist* pander.summary.glm*    
#> [57] pander.summary.lm*      pander.summary.lme*    
#> [59] pander.summary.manova*  pander.summary.nls*    
#> [61] pander.summary.polr*    pander.summary.prcomp* 
#> [63] pander.summary.rms*     pander.summary.survreg*
#> [65] pander.summary.table*   pander.survdiff*       
#> [67] pander.survfit*         pander.survreg*        
#> [69] pander.table*           pander.tabular*        
#> [71] 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$cyl\  \  \  \
mtcars$gear 4 6 8 Total


3\  \  \  \  \
N\ 1\ 2\ 12\ 15\
Chi-square\ 3.3502\ 0.5003\ 4.5054\ \
Row(%)\ 6.6667%\ 13.3333%\ 80.0000%\ 46.8750%\
Column(%)\ 9.0909%\ 28.5714%\ 85.7143%\ \
Total(%) 3.125% 6.250% 37.500%

4\  \  \  \  \
N\ 8\ 4\ 0\ 12\
Chi-square\ 3.6402\ 0.7202\ 5.2500\ \
Row(%)\ 66.6667%\ 33.3333%\ 0.0000%\ 37.5000%\
Column(%)\ 72.7273%\ 57.1429%\ 0.0000%\ \
Total(%) 25.000% 12.500% 0.000%

5\  \  \  \  \
N\ 2\ 1\ 2\ 5\
Chi-square\ 0.0460\ 0.0080\ 0.0161\ \
Row(%)\ 40.0000%\ 20.0000%\ 40.0000%\ 15.6250%\
Column(%)\ 18.1818%\ 14.2857%\ 14.2857%\ \
Total(%) 6.250% 3.125% 6.250%

Total\         11\            7\           14\            32\     
              34.375%       21.875%       43.75%                  

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

  \         \     Sepal.Length\     \     Sepal.Width\     \   

Species n mean sd 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))

  &nbsp;         mpg    cyl   disp   hp  

Mazda RX4 21 6 160 110

Mazda RX4 Wag 21 6 160 110

Datsun 710 22.8 4 108 93


    &nbsp;           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 


 &nbsp;       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