---
title: "Merge and Customise Tables"
description: "Combine two or more tables side by side, label each block, and finish the result with modify_table() or flextable."
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{Merge and Customise Tables}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r, include = FALSE}
knitr::opts_chunk$set(collapse = TRUE, comment = "#>", message = FALSE, warning = FALSE)
```

A manuscript table often brings several analyses together: the baseline
characteristics, the crude estimates and the adjusted estimates, or a primary
analysis next to its sensitivity analyses. `merge_tables()` places any number
of `gtregression` tables side by side, aligns the rows by variable and
category, and keeps every footnote.

This article covers:

- merging two, three or more tables;
- labelling each block with `spanners`;
- keeping binary rows consistent;
- merging sensitivity analyses and survival models;
- relabelling the merged table with `modify_table()`;
- further styling with `flextable` functions;
- saving the result.

## Example data

```{r setup}
library(gtregression)
library(dplyr)

data("data_birthwt", package = "gtregression")

birthwt <- data_birthwt |>
  mutate(
    race = factor(race, levels = c(1, 2, 3), labels = c("White", "Black", "Other")),
    smoke = factor(smoke, levels = c(0, 1), labels = c("No", "Yes")),
    ht = factor(ht, levels = c(0, 1), labels = c("No", "Yes")),
    ui = factor(ui, levels = c(0, 1), labels = c("No", "Yes")),
    low = factor(low, levels = c(0, 1), labels = c("Normal BW", "Low BW"))
  )

attr(birthwt$age, "label") <- "Maternal age (years)"
attr(birthwt$lwt, "label") <- "Maternal weight (lb)"
attr(birthwt$race, "label") <- "Race"
attr(birthwt$smoke, "label") <- "Smoking during pregnancy"
attr(birthwt$ht, "label") <- "History of hypertension"
attr(birthwt$ui, "label") <- "Uterine irritability"

exposures <- c("age", "lwt", "race", "smoke", "ht", "ui")
```

## Build the tables you want to combine

Use the same binary layout in every table you plan to merge. Showing both
levels in the descriptive table and the reference row in each regression table
gives one row per category across the whole merged table.

```{r build}
desc <- descriptive_table(
  birthwt,
  exposures = exposures,
  by = "low",
  show_dichotomous = "all_levels"
)

crude <- uni_reg(
  birthwt,
  outcome = "low",
  exposures = exposures,
  approach = "logit",
  show_ref = TRUE
)

adjusted <- multi_reg(
  birthwt,
  outcome = "low",
  exposures = exposures,
  approach = "logit",
  show_ref = TRUE
)
```

## Merge two tables

Supply the tables in the order they should appear, and one spanner label per
table.

```{r merge-two}
crude_adjusted <- merge_tables(
  crude,
  adjusted,
  spanners = c("Crude", "Adjusted")
)

crude_adjusted$table
```

## Merge three or more tables

There is no limit of two: pass as many tables as you need. The classic
"Table 2" combines the descriptive summary, the crude estimates and the
adjusted estimates.

```{r merge-three}
table_2 <- merge_tables(
  desc,
  crude,
  adjusted,
  spanners = c("Low birth weight", "Crude", "Adjusted")
)

table_2$table
```

Rows are matched on the underlying variable and category, not on the visible
label, so the tables stay aligned even when labels differ. A variable that
appears in some tables but not in others is kept and left blank where it was
not analysed.

If one table shows binary variables as a single row and another shows both
levels, `merge_tables()` warns and names the settings that make them
compatible (`show_dichotomous = "all_levels"` with `show_ref = TRUE`, or
`show_dichotomous = "single_row"` with `show_ref = FALSE`, used consistently).

## Sensitivity analyses side by side

Tables of the same type can be merged too. Here a primary model is shown next
to a model restricted to mothers without hypertension and a model with fewer
covariates.

```{r sensitivity}
primary <- multi_reg(
  birthwt,
  outcome = "low",
  exposures = c("smoke", "race", "ui"),
  adjust_for = c("age", "lwt"),
  approach = "logit",
  show_ref = TRUE
)

no_hypertension <- multi_reg(
  filter(birthwt, ht == "No"),
  outcome = "low",
  exposures = c("smoke", "race", "ui"),
  adjust_for = c("age", "lwt"),
  approach = "logit",
  show_ref = TRUE
)

fewer_covariates <- multi_reg(
  birthwt,
  outcome = "low",
  exposures = c("smoke", "race", "ui"),
  adjust_for = "age",
  approach = "logit",
  show_ref = TRUE
)

sensitivity <- merge_tables(
  primary,
  no_hypertension,
  fewer_covariates,
  spanners = c("Primary analysis", "Without hypertension", "Age-adjusted only")
)

sensitivity$table
```

Each adjustment note is carried over from its own table, so readers can see
exactly what each block was adjusted for.

## Survival tables

Cox and parametric survival tables merge in the same way. Unadjusted and
adjusted hazard ratios are a common pair.

```{r survival}
data("data_lungcancer", package = "gtregression")

lung <- data_lungcancer |>
  mutate(
    trt = factor(trt, levels = c(1, 2), labels = c("Standard", "Test")),
    celltype = factor(celltype)
  )

cox_crude <- cox_reg(
  lung,
  time = "time",
  event = "status",
  exposures = c("trt", "celltype", "karno"),
  show_ref = TRUE
)

cox_adjusted <- cox_reg(
  lung,
  time = "time",
  event = "status",
  exposures = c("trt", "celltype", "karno"),
  multivariable = TRUE,
  show_ref = TRUE
)

survival_table <- merge_tables(
  cox_crude,
  cox_adjusted,
  spanners = c("Unadjusted HR", "Adjusted HR")
)

survival_table$table
```

## Relabel with modify_table()

`modify_table()` works on merged tables. Use the original variable names on
the left-hand side; the visible labels can be anything.

```{r modify}
table_2_final <- modify_table(
  table_2,
  variable_labels = c(lwt = "Weight at last menstrual period (lb)"),
  level_labels = list(smoke = c(Yes = "Smoker", No = "Non-smoker")),
  caption = "Table 2. Factors associated with low birth weight",
  caveat = "Odds ratios from logistic regression; adjusted estimates include all listed characteristics."
)

table_2_final$table
```

## Choose a table theme

Every table function and `merge_tables()` accept a `theme`. Presets are
`"minimal"` (the default), `"clinical"`, `"striped"`, `"shaded"` and `"jama"`.
You can also combine primitives: `"plain"`, `"zebra"`, `"lines"`,
`"labels_bold"`, `"compact"` and `"header_shaded"`.

```{r themes}
jama_table <- merge_tables(
  crude,
  adjusted,
  spanners = c("Crude", "Adjusted"),
  theme = "jama"
)

jama_table$table

custom_theme_table <- merge_tables(
  crude,
  adjusted,
  spanners = c("Crude", "Adjusted"),
  theme = c("zebra", "compact", "labels_bold")
)

custom_theme_table$table
```

## Fine-tune with flextable

The merged table is an ordinary `flextable` stored in `$table`. Anything the
[flextable](https://ardata-fr.github.io/flextable-book/) package can do can be
applied to it: fonts, colours, widths, alignment or highlighting particular
cells. Take the table out, change it, and keep the result.

```{r flextable}
library(flextable)

ft <- table_2_final$table

ft <- ft |>
  font(fontname = "Arial", part = "all") |>
  fontsize(size = 9, part = "all") |>
  color(color = "#0F5C5F", part = "header") |>
  bg(bg = "#EEF6F6", part = "header") |>
  align(j = 2:ncol_keys(ft), align = "center", part = "all") |>
  autofit()

ft
```

To change particular cells, refer to columns by their keys and to rows by
number or by a condition on the cell values. Each block of a merged table has
its own keys, ending in `_p1`, `_p2`, `_p3` and so on, in the order the
tables were merged.

```{r flextable-keys}
ft$col_keys
```

For example, to bold the adjusted p-values below 0.05 (the third block):

```{r flextable-highlight}
adjusted_p <- ft$col_keys[startsWith(ft$col_keys, "p.value") & endsWith(ft$col_keys, "_p3")]
p_values <- suppressWarnings(as.numeric(sub("<", "", ft$body$dataset[[adjusted_p]])))

ft <- bold(ft, i = which(p_values < 0.05), j = adjusted_p, part = "body")
ft
```

Changes made this way apply to the flextable only. Keep the original
`gtregression` object if you want to merge it again later, and apply flextable
styling as the final step.

For HTML-first work, use `format = "gt"` in `merge_tables()` and style the
result with the [gt](https://gt.rstudio.com/) package in the same way.

## Save the result

`save_table()` accepts the `gtregression` object or a styled flextable. The
file type is set by `format`.

```{r save, eval = FALSE}
save_table(table_2_final, filename = "table-2", format = "docx")
save_table(ft, filename = "table-2-styled", format = "docx")
save_table(table_2_final, filename = "table-2", format = "html")
```

Several tables can go into one Word document with `save_docx()`:

```{r save-docx, eval = FALSE}
save_docx(
  tables = list(table_2_final, primary),
  filename = "results",
  titles = c("Table 2. Low birth weight", "Table 3. Sensitivity analyses")
)
```

## Which tables can be merged?

| Can be merged | Notes |
|---|---|
| `descriptive_table()` | Use `show_dichotomous` consistently. |
| `uni_reg()`, `multi_reg()` and the stratified functions | Use `show_ref` consistently. |
| `cox_reg()`, `surv_reg()`, `competing_risk_reg()` | Crude and adjusted pairs, or sensitivity analyses. |
| `ordinal_reg()`, `multinomial_reg()`, `multilevel_reg()`, `conditional_logit_reg()` | Each keeps its own estimate type and notes. |
| A merged table | Merge again, or relabel with `modify_table()`. |

`risk_difference()` tables are designed to stand alone and are not merged;
see [Risk Difference and NNT](risk-difference.html).
