如何格式化rmarkdown PDF输出的复杂表

时间:2016-11-20 00:55:47

标签: r formatting latex knitr r-markdown

我有一张表格,我希望从rmarkdown文档以PDF格式输出。但是,由于我的乳胶技能有限,我无法弄清楚如何使用xtable添加各种Latex来获得跨越列,单元格边框和字体的方式。

我能够使用FlexTable包中的ReporteRs函数获得我想要的内容,但看起来FlexTable只能与rmarkdown一起使用生成html输出,但不输出PDF。

所以,我正在寻找帮助,使用xtable或任何其他R包或(可能是自定义的)R函数来格式化我的表,这些R函数可用于以编程方式为PDF输出创建合理复杂的表。另外,如果有某种方法可以哄骗FlexTable来处理PDF输出,那也会很棒。

下面我使用FlexTable创建一个表格,以便您可以看到我的目标。接下来,我提供了一个示例rmarkdown文档,显示了我在使用xtable创建类似表格时(有点蹩脚)努力中所取得的成就。

ReporteRs::FlexTable版本

首先,让我们创建将进入表格的数据:

library(ReporteRs)

x = structure(c(34L, 6L, 9L, 35L), .Dim = c(2L, 2L), .Dimnames = structure(list(
    Actual = c("Fail", "Pass"), Predicted = c("Fail", "Pass")), .Names = c("Actual", 
"Predicted")), class = "table")

x=cbind(x, prop.table(x), prop.table(x, 1), prop.table(x,2))
x[, -c(1,2)] = sapply(x[,-c(1,2)], function(i) paste0(sprintf("%1.1f", i*100),"%"))
x = cbind(Actual=rownames(x), x)

现在为FlexTable创建和格式化:

# Set up general table properties and formatting
cell_p = cellProperties(padding.right=3, padding.left=3)
par_p = parProperties(text.align="right")

# Create table
ft = FlexTable(x, header.columns=FALSE, body.cell.props=cell_p, body.par.props=par_p)

# Add three header rows
ft = addHeaderRow(ft, text.properties=textBold(), c("","Predicted"),
                  colspan=c(1,8), par.properties=parCenter())

ft = addHeaderRow(ft, text.properties=textBold(), 
                  value=c("", "Count", "Overall\nPercent", "Row\nPercent", "Column\nPercent"),
                  colspan=c(1,rep(2,4)), par.properties=parCenter())

ft = addHeaderRow(ft, text.properties=textItalic(), par.properties=parCenter(),
                  value=colnames(x))

# Format specific cells
ft[1:2, 1, to="header", side="left"] = borderProperties(color="white")
ft[1:2, 1, to="header", side="top"] = borderProperties(color="white")

ft[3, 1, to="header"] = textProperties(font.style="normal", font.weight="bold")
ft[ , 1] = textProperties(font.style="italic")

ft[ , 2:3] = cellProperties(padding.right=7, padding.left=7)
ft[ , 1] = cellProperties(padding.right=10, padding.left=10)

# Display ft
ft

这里是最终表格的样子(这是浏览器窗口中显示的表格的PNG屏幕截图):

enter image description here

现在我试图用xtable做同样的事情。

xtable版本

这里是rmarkdown文档和header.tex文件:

---
title: "Untitled"
author: "eipi10"
date: "11/19/2016"
output: 
  pdf_document:
    fig_caption: yes
    includes:
      in_header: header.tex 
---

```{r setup, include=FALSE}
library(knitr)
opts_chunk$set(echo = FALSE, message=FALSE)
```

```{r}
# Fake confusion matrix to work with
x = structure(c(34L, 6L, 9L, 35L), .Dim = c(2L, 2L), .Dimnames = structure(list(
    Actual = c("Fail", "Pass"), Predicted = c("Fail", "Pass")), .Names = c("Actual", 
"Predicted")), class = "table")

x=cbind(x, prop.table(x), prop.table(x, 1), prop.table(x,2))
x[, -c(1,2)] = sapply(x[,-c(1,2)], function(i) paste0(sprintf("%1.1f", i*100),"%"))
x = cbind(Actual=rownames(x), x)
```  

```{r use_xtable, results="asis"}
# Output the confusion matrix created above as a latex table
library(xtable)
options(xtable.comment=FALSE)

# This is a modified version of a function created in the following SO answer:
# http://stackoverflow.com/a/38978541/496488
make_addtorow <- function(row.name, terms, colSpan, width) {
  # Custom row function
  paste0(row.name, 
  paste0('& \\multicolumn{', colSpan, '}{C{', width, 'cm}}{', 
         terms, 
         '}', 
        collapse=''), 
  '\\\\')
}

addtorow <- list()
addtorow$pos <- list(-1,-1,-1,-1) 
addtorow$command <- c(
  "\\hline",
  make_addtorow("", c("Predicted"), 8, 12),
  "\\hline",
  make_addtorow("", c("Count", "Percent", "Row Percent", "Column Percent"), 2, 3)
  )

xtbl = xtable(x, caption="Created with xtable")

align(xtbl) <- c("|L{0cm}|", "L{1.2cm}|", rep("R{1cm}|",8))

print(xtbl, 
      include.rownames=FALSE, 
      tabular.environment="tabularx", 
      width="0.92\\textwidth",
      add.to.row = addtorow)
```

用于编织上述rmarkdown文档的文件header.tex

% xtable manual: https://cran.r-project.org/web/packages/xtable/vignettes/xtableGallery.pdf
\usepackage{array}
\usepackage{tabularx}  
\newcolumntype{L}[1]{>{\raggedright\let\newline\\
\arraybackslash\hspace{0pt}}m{#1}}
\newcolumntype{C}[1]{>{\centering\let\newline\\
\arraybackslash\hspace{0pt}}m{#1}}
\newcolumntype{R}[1]{>{\raggedleft\let\newline\\
\arraybackslash\hspace{0pt}}m{#1}}
\newcolumntype{P}[1]{>{\raggedright\tabularxbackslash}p{#1}}

% Caption on top
% http://tex.stackexchange.com/a/14862/4762
\usepackage{floatrow}
\floatsetup[figure]{capposition=top}

以下是PDF输出中的表格:

enter image description here

3 个答案:

答案 0 :(得分:7)

引用this comment

  

我正在寻找一种方法,可以在rmarkdown文档中以编程方式执行此操作,而无需对格式进行硬编码,以便它具有可重现性和灵活性。

以下解决方案使用硬编码&#34;模板&#34;,但模板可以填充任何数据(前提是它具有相同的2x8结构)。

生成的表格如下:

Output

以下完整代码。

基本上,最终表由9列组成,因此基本的LaTeX结构是

\begin{tabular}{|c|c|c|c|c|c|c|c|c|}
% rest of table
\end{tabular}

但是,固定细胞的宽度很方便。这可以使用自定义列类型C(取自here on TEX.SE),它允许具有固定宽度的居中内容。这与more compact syntax for repeating column types一起给出了:

\begin{tabular}{|c *{8}{|C{1cm}}|}
% rest of table
\end{tabular}

(第一列以柔性宽度为中心,然后是8个中心列,每个1cm宽)。

使用\multicolumn可以跨越多列。这些单元格也应该具有固定的宽度,以便将单元格标题分成两行。请注意,假设跨越两个1cm列的单元格应该具有2cm的宽度是错误的,因为两个跨越的单元格之间具有额外的填充。一些测量显示,大约2.436cm可以产生良好的效果。

备注第一栏:尽管\multicolumn{1}{...}{...}初看起来毫无用处,但它对于更改单个单元格的列类型(包括左/右)边框非常有用。我用它来删除前两行中最左边的垂直线。

\cline{x-y}提供的水平线仅跨越x列到y

把这些碎片放在一起给出了:

\begin{tabular}{|c *{8}{|C{1cm}}|} \cline{2-9}
    \multicolumn{1}{c|}{} & \multicolumn{8}{c|}{\textbf{Predicted}} \\ \cline{2-9}
    \multicolumn{1}{c|}{} & \multicolumn{2}{c|}{\textbf{Count}} & \multicolumn{2}{C{2.436cm}|}{\textbf{Overall Percent}} & \multicolumn{2}{C{2.436cm}|}{\textbf{Row \newline Percent}} & \multicolumn{2}{C{2.436cm}|}{\textbf{Column Percent}} \\ \hline
% rest of table
\end{tabular}

关于数据,我删除了生成的代码的最后一行,以获取样本数据:

> x <- structure(c(34L, 6L, 9L, 35L), .Dim = c(2L, 2L), .Dimnames = structure(list(Actual = c("Fail", "Pass"), Predicted = c("Fail", "Pass")), .Names = c("Actual", "Predicted")), class = "table")
> x <- cbind(x, prop.table(x), prop.table(x, 1), prop.table(x,2))
> x[, -c(1,2)] <- sapply(x[,-c(1,2)], function(i) paste0(sprintf("%1.1f", i*100),"%"))
> x
     Fail Pass Fail    Pass    Fail    Pass    Fail    Pass   
Fail "34" "9"  "40.5%" "10.7%" "79.1%" "20.9%" "85.0%" "20.5%"
Pass "6"  "35" "7.1%"  "41.7%" "14.6%" "85.4%" "15.0%" "79.5%"

要以斜体设置列名和行名,请执行

colnames(x) <- sprintf("\\emph{%s}", colnames(x)) # highlight colnames
rownames(x) <- sprintf("\\emph{%s}", rownames(x)) # highlight rownames

然后,可以使用以下xtable代码:

print(xtable(x),
      only.contents = TRUE, 
      comment = FALSE,
      sanitize.colnames.function = identity, 
      sanitize.rownames.function = identity, 
      hline.after = 0:2)

参数only.contents会抑制封闭的tabular环境。将身份功能分配给sanitize.colnames.functionsanitize.rownames.function意味着&#34;不会消毒&#34;。我们需要这个,因为列名和行名包含不应转义的特殊LaTeX字符(\emph)。

输出应该替换上面的%rest of table占位符。

从概念上讲,代码使用xtable仅生成表体,但不生成标题,因为手动编写标题要容易得多。

尽管整个表格标题是&#34;硬编码&#34;,但数据可以根据需要进行更改。

别忘了在\秒内逃离所有\!此外,必须在标题(header.tex)中添加以下内容:

\usepackage{array}
\newcolumntype{C}[1]{>{\centering\let\newline\\\arraybackslash\hspace{0pt}}m{#1}} % https://tex.stackexchange.com/a/12712/37118

我在函数PrintConfusionMatrix中包含了上面列出的所有元素,可以在任何提供数据和列/行名称的2x8数据框中重复使用。

完整代码:

---
output:
  pdf_document: 
    keep_tex: yes
    includes:
      in_header: header.tex
---


```{r, echo = FALSE}
library(xtable)

# Sample data from question
x <- structure(c(34L, 6L, 9L, 35L), .Dim = c(2L, 2L), .Dimnames = structure(list(Actual = c("Fail", "Pass"), Predicted = c("Fail", "Pass")), .Names = c("Actual", "Predicted")), class = "table")
x <- cbind(x, prop.table(x), prop.table(x, 1), prop.table(x,2))
x[, -c(1,2)] <- sapply(x[,-c(1,2)], function(i) paste0(sprintf("%1.1f", i*100),"%"))
#x <- cbind(Actual=rownames(x), x) # dropped; better not to add row names to data

PrintConfusionMatrix <- function(data, ...) {

  stopifnot(all(dim(x) == c(2, 8)))

  colnames(x) <- sprintf("\\emph{%s}", colnames(x)) # highlight colnames
  rownames(x) <- sprintf("\\emph{%s}", rownames(x)) # highlight rownames

  cat('\\begin{tabular}{|c *{8}{|C{1cm}}|} \\cline{2-9}
    \\multicolumn{1}{c|}{} & \\multicolumn{8}{c|}{\\textbf{Predicted}} \\\\ \\cline{2-9}
    \\multicolumn{1}{c|}{} & \\multicolumn{2}{c|}{\\textbf{Count}} & \\multicolumn{2}{C{2.436cm}|}{\\textbf{Overall Percent}} & \\multicolumn{2}{C{2.436cm}|}{\\textbf{Row \\newline Percent}} & \\multicolumn{2}{C{2.436cm}|}{\\textbf{Column Percent}} \\\\ \\hline
    \\textbf{Actual} ')

  print(xtable(x),
        only.contents = TRUE, 
        comment = FALSE,
        sanitize.colnames.function = identity, 
        sanitize.rownames.function = identity, 
        hline.after = 0:2,
        ...)
  cat("\\end{tabular}")
}
```

```{r, results='asis'}
PrintConfusionMatrix(x)
```

答案 1 :(得分:4)

不完整,但也许可以帮助您入门:使用\cline限制\hline的范围,并使用\multicolumn允许标题跨越多列。尝试了几种不同的方法,每种方式都有不同的问题。

```{r, results="asis"}    

# Fake confusion matrix to work with
x = structure(c(34L, 6L, 9L, 35L), .Dim = c(2L, 2L), .Dimnames = structure(list(
    Actual = c("Fail", "Pass"), Predicted = c("Fail", "Pass")), .Names = c("Actual", 
"Predicted")), class = "table")

x=cbind(x, prop.table(x), prop.table(x, 1), prop.table(x,2))
x[, -c(1,2)] = sapply(x[,-c(1,2)], function(i) paste0(sprintf("%1.1f", i*100),"%"))
x = cbind(Actual=rownames(x), x)


# output
library(xtable)

# Create function for headers to span multiple columns
spanfun <- function(nms, span=2, align="|c|") {
  out = paste0("& \\multicolumn{", span, "}{", align, "}{", nms, "}", collapse=" ")
  paste(out,  "\\\\")
}     

# \\cline limits the range of \hline, so omits first cell
addtorow = list(list( -1, -1, -1, -1), 
                    c("\\cline{2-9} \\multicolumn{1}{c|}{} ",
                      spanfun("Predicted", span=8),
                      "\\cline{2-9} \\multicolumn{1}{c|}{} ",
                      spanfun(c("Count", "Percent", "Row Percent", "Column Percent")) ))


print.xtable(
  xtable(x, align=c("|l|","|l|", rep(c("r|"),8))),
  include.rownames=FALSE, 
  add.to.row=addtorow, include.colnames=TRUE)

```

enter image description here

使用其他一些不完美的尝试进行更新

创建标题文件

txt <- "
\\usepackage{tabularx, array, booktabs,siunitx}
\\newcolumntype{Y}{>{\\raggedleft\\arraybackslash}X}
"
cat(txt, file="so.sty")

第2版

spanfun <- function(nms, span=2, align="|c|") {
  out = paste0("& \\multicolumn{", span, "}{", align, "}{\\bfseries{", nms, "}}", collapse=" ")
  paste(out,  "\\\\")
} 

addtorow = list(list( -1, -1, -1, -1), c("\\cline{2-9} \\multicolumn{1}{c}{} ",
                                   spanfun("Predicted", span=8),
                                   "\\cline{2-9} \\multicolumn{1}{c}{} ",
                                   spanfun(c("Count", "Percent", "Row Percent", "Column Percent"))
            ))

# make pass / fail row (3rd row) italic
# but vertical lines are not aligned
# some double lines
# cell alignment all over the shop
print.xtable(
  xtable(x, align= c("l", "|l|", rep("S|", 8))),
  add.to.row=addtorow,
  include.rownames=FALSE,
  include.colnames=TRUE,
  sanitize.colnames.function=function(x) {paste0('{\\textit{', x ,'}}')})

enter image description here

第3版

addtorow = list(list( -1, -1, -1, -1, 0), c("\\cline{2-9} \\multicolumn{1}{c}{}",
                                   spanfun("Predicted", span=8),
                                   "\\cline{2-9} \\multicolumn{1}{c}{}",
                                   spanfun(c("Count", "Percent", "Row Percent", "Column Percent")),
                                   paste(paste0(" \\multicolumn{1}{|c|}{{\\textit{", colnames(x),"}}}", collapse=" & "), "\\\\")

            ))
# Same issues as preceding example
print(xtable(x, align= c("|l|", "|l|", rep("Y|", 8))), 
      add.to.row = addtorow,
      include.rownames=FALSE,
      include.colnames=FALSE,
      tabular.environment="tabularx",
      width="\\textwidth")

enter image description here

版本4 - 来自%https://tex.stackexchange.com/questions/140353/align-position-of-decimal-point-within-table-of-numbers-text-and-percentage-val的帮助

创建标题文件

txt <- "
\\usepackage{booktabs,dcolumn}
\\newcolumntype{Y}{D..{4.3}}
"
cat(txt, file="so.sty")


addtorow = list(list( -1, -1, -1, -1, 0), c("\\cline{2-9} \\multicolumn{1}{c}{}",
                                   spanfun("Predicted", span=8),
                                   "\\cline{2-9} \\multicolumn{1}{c}{}",
                                   spanfun(c("Count", "Percent", "Row Percent", "Column Percent")),
                                   paste0("\\multicolumn{1}{|c|}{{\\textit{", colnames(x)[1],"}}} & ",
                                     paste0(" \\multicolumn{1}{c|}{{\\textit{", colnames(x)[-1],"}}}", collapse=" & "), "\\\\")

            ))

# Again issues with vertical lines but alignment is better
print(xtable(x, align= c("|l|", "|l|", rep("Y|", 8))), 
      add.to.row = addtorow,
      include.rownames=FALSE,
      include.colnames=FALSE)

enter image description here

答案 2 :(得分:2)

使用KableExtra软件包中的add_header_above命令非常简单。您可以根据需要添加任意数量的列分组。这就是我要做的:

d <- mtcars[1:5,1:5]
kable(d,longtable = T, booktabs = T) %>%
   add_header_above(c(" ", "Group 1" = 2, "Group 2" = 3)) %>%
   add_header_above(c("","Groups" = 5))

enter image description here