Как отформатировать сложную таблицу для вывода rmarkdown PDF

У меня есть таблица, которую я хотел бы вывести в формате PDF из документа rmarkdown. Однако, с моими ограниченными навыками латекса, я не могу понять, как получить охват столбцов, границ ячеек и шрифта, как я их хочу, используя xtable с различными дополнениями Latex.

Мне удалось получить то, что я хотел, используя функцию FlexTable из пакета ReporteRs, но похоже, что FlexTable может использоваться только с rmarkdown для вывода html-вывода, но не для вывода PDF файла.

Итак, я ищу помощь в форматировании моей таблицы с помощью xtable или любого другого 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 таблицы, отображаемой в окне браузера):

введите описание изображения здесь

Теперь для моей попытки сделать то же самое с 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)
```

Файл header.tex, который используется для вставки в документ rmarkdown выше:

% 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:

введите описание изображения здесь

Ответ 1

Цитата этот комментарий:

Я ищу способ сделать это программно из документа rmarkdown без необходимости жесткого кодирования форматирования, чтобы он был воспроизводимым и гибким.

В следующем решении используется жестко закодированный "шаблон", но шаблон может быть заполнен любыми данными (при условии, что он имеет ту же структуру 2x8).

Сгенерированная таблица выглядит следующим образом:

Вывод

Полный код ниже.


В принципе, итоговая таблица состоит из 9 столбцов, поэтому основная структура LaTeX

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

Однако удобно фиксировать ширину ячеек. Это возможно с помощью настраиваемого типа столбца C (взятого из здесь, на TEX.SE), который позволяет сосредоточить контент с фиксированной шириной. Это, наряду с более компактным синтаксисом для повторяющихся типов столбцов, дает:

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

(Первый столбец с гибкой шириной, затем 8 центрированных столбцов, каждый шириной 1 см).

Ячейки, охватывающие несколько столбцов, можно использовать с помощью \multicolumn. Эти ячейки также должны иметь фиксированную ширину, чтобы подписи клеток разбивались на две строки. Обратите внимание, что ошибочно полагать, что ячейки, охватывающие два столбца 1 см, должны иметь ширину 2 см, потому что у двух растянутых ячеек есть дополнительное дополнение между ними. Некоторые измерения показали, что около 2,436 см дает хорошие результаты.

Замечание в первом столбце: Хотя \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.function и sanitize.rownames.function означает "не дезинфицировать". Нам это нужно, потому что имена столбцов и строк содержат специальные символы LaTeX, которые не следует экранировать (\emph).

Выход должен заменить элемент %rest of table сверху.


Концептуально, код использует xtable для генерации только тела таблицы, но не заголовка, потому что намного легче написать заголовок вручную.

Хотя весь заголовок таблицы "жестко закодирован", данные могут быть изменены по мере необходимости.

Не забывайте избегать всех \ со вторым \! Кроме того, в заголовок должно быть добавлено следующее (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)
```

Ответ 2

Не завершите, но, возможно, что-то для начала: используйте \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)

```

введите описание изображения здесь


обновить с помощью нескольких других несовершенных попыток

Создать файл заголовка

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 ,'}}')})

введите описание изображения здесь


Версия 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")

введите описание изображения здесь


Версия 4 - помощь от% https://tex.stackexchange.com/info/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)

введите описание изображения здесь