--- title: "Summary Tables and Customisation" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Summary Tables and Customisation} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r setup, include = FALSE} knitr::opts_chunk$set( collapse = TRUE, comment = "#>", eval = TRUE ) ``` ## Introduction The descriptive table builder is the centrepiece of **gtstats**. It lets you assemble a publication-ready "Table 1" — the standard baseline characteristics table used in clinical and epidemiological papers — through a simple, pipe-based workflow. Think of the table as layers. Start with the descriptive foundation, then add only the layers required by the report: | Step | Function | What it does | |---|---|---| | 1 | `summary_table()` | Builds summaries, grouping and the Overall column | | 2 | `add_ci()` | Adds CIs globally or to selected variables | | 3 | `add_p()` | Adds a p-value column when comparisons are appropriate | | 4 | `add_proportion()` | Highlights one selected event as a new row | | 5 | `add_rate()` | Adds an event-rate row | | 6 | `add_total()` | Adds a participant-count row | | 7 | `add_row()` | Adds a free-text row | | 8 | `customise_table()` | Finishes appearance and labels | ```{r load} library(gtstats) ``` --- ## A minimal table For an ordinary Table 1, select all variable types together. There is no need to add continuous and categorical variables separately. ```{r minimal} summary_table(mtcars, by = am, include = c(mpg, wt, cyl)) ``` `by = am` groups the table by the `am` variable (transmission type). Each level becomes a column. --- ## Adding an overall column Set `overall = TRUE` to include an additional column showing statistics for the full sample. ```{r overall} summary_table(mtcars, by = am, include = c(mpg, wt, cyl), overall = TRUE) ``` --- ## Summary-table options at a glance The default call is deliberately simple. These global options cover the most common reporting choices without requiring separate continuous and categorical workflows. | Reporting choice | Option | Example | |---|---|---| | Overall column | `overall` | `TRUE`, `"first"`, or `"last"` | | Continuous summary | `statistic` | `"recommended"`, `"mean_sd"`, `"mean_ci"`, `"median_iqr"`, or `"both"` | | Categorical display | `categorical` | `"n_percent"`, `"n_over_N_percent"`, `"n"`, or `"percent"` | | Categorical columns | `categorical_layout` | `"combined"` (default) or `"separate"` for categorical-only tables without CIs | | Overall categorical cells | `overall_categorical` | `"auto"` (default), `"n_percent"`, `"n_over_N_percent"`, `"n"`, or `"percent"` | | Binary rows | `show_dichotomous` | `"all_levels"` (default) or `"single_row"` | | Binary event | `value` | Named choices such as `c(smoke = "Yes")`; otherwise the second level is used | | Percentage denominator | `percent` | `"column"`, `"row"`, `"overall"`, or `"none"` | | Precision | `digits` | `c(continuous = 1, percent = 0, ci = 1)` | | Missing-value handling | `missing` | `"ifany"`, `"always"`, `"no"`, or `"as_category"` | | CI layout | `layout` | `"compact"` (default) or `"separate"` | For a row-percentage table, `overall_categorical = "auto"` deliberately shows Overall categorical counts. A percentage of the entire sample is usually not the same estimand as the grouped row percentages. Override this with `overall_categorical = "n_percent"` only when that is the intended display. ### How global choices and variable exceptions work The defaults are deliberately useful, but they never lock the user in. A single value applies to every eligible variable. A named vector changes only the named variables; all unlisted variables continue to use the recommended summary. ```{r global-and-exceptions} # Mean (SD) for every continuous variable summary_table( birthwt, include = c(age, lwt, bwt, smoke), statistic = "mean_sd" ) # Recommended summaries for all variables except maternal weight summary_table( birthwt, include = c(age, lwt, bwt, smoke), statistic = c(lwt = "median_iqr") ) ``` The same principle applies to `label`, `value`, and the named precision settings. This makes the short call beginner-friendly while preserving precise control for a manuscript. ### Which layout option controls which columns? Two similarly named options solve different reporting problems: | Need | Use | Result | |---|---|---| | Keep ordinary categorical values as `n (%)` | `categorical_layout = "combined"` | One summary column per cohort | | Put `n` and `%` in different columns | `categorical_layout = "separate"` | Separate count and percentage child columns; intended for categorical-only tables without CIs | | Keep an estimate and its CI together | `layout = "compact"` plus `add_ci()` | One concise cell per cohort | | Put estimates and CIs in different columns | `layout = "separate"` plus `add_ci()` | A cohort spanner with an estimate column and an explicit confidence-interval column | `layout = "separate"` does not create empty confidence-interval columns. The CI columns appear only after `add_ci()`, `add_proportion(ci = TRUE)`, or an equivalent CI layer is present. ### Compact binary variables `show_dichotomous = "single_row"` is a display choice for common Yes/No and Present/Absent variables. It does not turn a two-level association test into a one-level test. By default, gtstats displays the second declared factor level. Use `value` whenever the event should be explicit. ```{r compact-binary-explicit} summary_table( birthwt, by = low, include = c(smoke, ht, race), show_dichotomous = "single_row", value = c(smoke = "Yes", ht = "Yes") ) |> add_p() ``` Here `smoke` and `ht` occupy one row each, `race` retains all levels, and `add_p()` still tests each complete source variable. ### Precision and labels Use one number when all displayed values need the same precision, or name the parts that differ: ```{r precision-and-labels} summary_table( birthwt, by = low, include = c(age, smoke), digits = c(continuous = 1, percent = 0, ci = 1), label = c( age = "Maternal age, years", smoke = "Smoking during pregnancy" ) ) ``` Variable labels affect presentation only. They do not rename the columns in the source data or alter automatic test selection. Confidence intervals are deliberately a visible second layer: ```{r ci-layer} summary_table( mtcars, by = am, include = c(mpg, cyl, vs), layout = "separate" ) |> add_ci() summary_table( mtcars, by = am, include = c(mpg, cyl, vs), layout = "separate" ) |> add_ci(vars = c(mpg, vs), conf.level = 0.90) ``` For a categorical-only table without confidence intervals, counts and percentages can instead occupy distinct child columns: ```{r separate-n-percent-layout} summary_table( mtcars, by = am, include = c(cyl, vs), categorical_layout = "separate" ) ``` For compact clinical Table 1 layouts, binary variables can occupy one row while ordinary categorical variables continue to show every level: ```{r compact-binary-layout} summary_table( birthwt, by = low, include = c(smoke, ht, race), show_dichotomous = "single_row", value = c(smoke = "Yes", ht = "Yes") ) ``` This changes presentation only. `add_p()` still tests the full binary variable. For different continuous summaries by variable, provide a named vector: ```{r mixed-continuous-formats} summary_table( mtcars, by = am, include = c(mpg, wt, cyl), statistic = c(mpg = "mean_ci", wt = "median_iqr"), overall = "last" ) ``` --- ## Choosing percentage denominators and missing rows Categorical percentages should always identify their denominator: ```{r percentage-denominators} summary_table(mtcars, by = am, overall = TRUE, include = c(cyl, vs), percent = "column") summary_table(mtcars, by = am, overall = TRUE, include = c(cyl, vs), percent = "row") summary_table(mtcars, by = am, overall = TRUE, include = c(cyl, vs), percent = "overall") summary_table(mtcars, by = am, overall = TRUE, include = c(cyl, vs), categorical = "n") ``` `"column"` describes levels within each group, `"row"` distributes each level across groups, `"overall"` uses the full non-missing variable denominator, and `"none"` displays counts only. The selected rule is retained in the object and shown in the table footnote. Missing-value rows are equally explicit: ```{r missing-rows} summary_table( mtcars, by = am, overall = TRUE, include = c(mpg, wt, vs), missing = "ifany" ) ``` Use `"always"` to show zero-missing rows or `"no"` to suppress them. With these settings, observed-category percentages use non-missing values. Use `missing = "as_category"` to display Missing as a category and include it when calculating categorical percentages. ```{r missing-as-category} missing_example <- data.frame( catheter = factor( c(rep("Yes", 32), rep(NA_character_, 68)), levels = c("No", "Yes") ) ) summary_table( missing_example, include = catheter, missing = "as_category" ) ``` ### Showing the denominator in every categorical cell Use `categorical = "n_over_N_percent"` when the table itself should show the non-missing denominator for every category. This is particularly helpful for small groups, supplementary tables, and teaching. ```{r n-over-n-percent} summary_table( mtcars, by = am, include = cyl, categorical = "n_over_N_percent", percent = "column", digits = c(percent = 0) ) ``` ### Mean with a confidence interval Use `add_ci()` when the goal is descriptive precision rather than a p-value. For a mean-based continuous summary it adds a t confidence interval while retaining the displayed mean (SD). ```{r mean-ci} summary_table( mtcars, include = c(mpg, wt), statistic = "mean_sd", layout = "separate" ) |> add_ci() ``` --- ## Adding proportion rows `add_proportion()` is a specialist layer that adds one highlighted event row for a binary or categorical variable. Optionally add Wilson confidence intervals (the default) or request exact binomial confidence intervals with `ci = TRUE`. When confidence intervals make a table too dense, use `layout = "separate"`. Each displayed cohort then becomes a spanning header with a dynamically labelled summary column and an explicit **95% CI** column (or the selected confidence level). The confidence level and method are stated once in the footnote. Separate child columns appear only after a CI layer is added, so `layout = "separate"` alone does not create empty columns. The compact layout remains the default for ordinary Table 1 output. ```{r separate-ci-layout} summary_table( birthwt, by = smoke, include = c(age, low), overall = "first", layout = "separate" ) |> add_ci() ``` ```{r proportion} summary_table(mtcars, by = am, include = c(mpg, wt, cyl), overall = TRUE) |> add_proportion(var = vs) |> to_gt() ``` To display confidence intervals and pin the row to a specific level: ```{r proportion-ci} summary_table(mtcars, by = am, include = c(mpg, wt, cyl), overall = TRUE) |> add_proportion(var = vs, level = "1", ci = TRUE) |> to_gt() ``` --- ## Adding a total row `add_total()` appends a row at the bottom showing the total N per group. ```{r total} summary_table(mtcars, by = am, include = c(mpg, wt, cyl), overall = TRUE) |> add_proportion(var = vs) |> add_total() |> to_gt() ``` --- The rendered table footnote is data-driven: it mentions continuous summaries only when continuous variables are present, and categorical displays only when categorical variables are present. ## Adding p-values `add_p()` appends a p-value column. Its automatic route is identical to `compare_groups(test = "auto")`: - Continuous variables: Welch t-test (2 groups) or Welch ANOVA (3+ groups) by default; use `var_equal = TRUE` only when an equal-variance assumption is justified, to select Student's t-test or classical ANOVA - Marked skewness (absolute sample skewness at least 1 in any group): Wilcoxon rank-sum or Kruskal-Wallis. Shapiro-Wilk and lesser asymmetry are supporting information and do not switch the test by themselves. - Binary, nominal, and independent ordinal variables: chi-square when no expected count is below 1 and no more than 20% are below 5; Fisher's exact test otherwise. Larger sparse tables use a Monte Carlo Fisher p-value. `add_p()` uses the same automatic-selection policy as `compare_groups()`. The publication table stays concise: its p-value markers identify the test, while the variable-specific checks remain available in the audit components. `var_equal` is a user-specified analytical assumption; it is not inferred by a variance hypothesis test. Use `include` when a displayed variable should remain descriptive but should not be tested. This is important when the grouping variable was derived from a displayed variable or when a comparison was not prespecified. ```{r add-p-include} summary_table(birthwt, by = low, include = c(age, bwt, smoke)) |> add_p(include = -bwt) ``` For independent ordered factors, Auto compares the complete distribution of levels using chi-square/Fisher. Specify `test = "wilcox"` or `"kruskal"` only when the planned estimand is an ordered rank shift. Paired and repeated outcomes use their design-specific routes: paired t/Wilcoxon signed-rank, repeated-measures ANOVA/Friedman, McNemar, or Cochran's Q. ```{r add-p} summary_table(mtcars, by = am, include = c(mpg, wt, cyl)) |> add_proportion(var = vs) |> add_p() |> to_gt() ``` Raw and multiplicity-adjusted values are retained separately: ```{r adjusted-p} adjusted <- summary_table(mtcars, by = am, include = c(mpg, wt, cyl, vs)) |> add_p(p_adjust = "BH") adjusted$p_values adjusted$assumptions adjusted$diagnostics adjusted$denominators ``` For a readable audit table, use `diagnostics_stats(adjusted)`. For continuous variables it includes observed group SDs and variances as descriptive context; these values do not act as a variance-test gatekeeper because Welch methods do not require equal variances. `denominators_stats(adjusted)` provides a compact audit table showing the observations contributing to every variable and group. ### Specifying tests manually Pass a named character vector to `test` to override the automatic selection for specific variables: ```{r add-p-manual} summary_table(mtcars, by = am, include = c(mpg, wt, cyl)) |> add_p(test = c(mpg = "welch_t", wt = "wilcox", cyl = "chisq")) |> to_gt() ``` Supported methods: `"auto"`, `"welch_t"`, `"t_test"`, `"wilcox"`, `"anova"`, `"welch_anova"`, `"rm_anova"`, `"kruskal"`, `"friedman"`, `"chisq"`, `"fisher"`, `"mcnemar"`, and `"cochran_q"`. ### Paired tests For before/after or matched data, use `paired = TRUE`: ```{r paired} dat <- data.frame( id = rep(1:4, 2), period = c("before", "before", "before", "before", "after", "after", "after", "after"), score = c(10, 12, 9, 11, 13, 16, 11, 15) ) summary_table(dat, by = period, include = score) |> add_p(paired = TRUE, id = id, test = "wilcox") |> to_gt() ``` --- ## Adding rate rows `add_rate()` appends an event rate row calculated per a chosen multiplier, with exact Poisson confidence intervals. This is useful when your dataset contains event counts and person-time denominators. ```{r rate} summary_table(mtcars, by = am, overall = TRUE) |> add_rate( event = carb, time = cyl, label = "Carburettor rate", multiplier = 1000 ) |> to_gt() ``` --- ## Adding custom rows `add_row()` inserts a free-text row — useful for study period notes, data source annotations, or any label that does not come from a variable. ```{r custom} summary_table(mtcars, by = am, include = c(mpg, wt, cyl), overall = TRUE) |> add_row( label = "Study period", overall = "2020–2024", values = c("am = 1" = "2020–2024", "am = 0" = "2020–2024") ) |> to_gt() ``` --- ## The full workflow Putting it all together — a complete, publication-ready descriptive table: ```{r full-workflow} summary_table(mtcars, by = am, include = c(mpg, wt, cyl), overall = TRUE) |> add_proportion(var = vs, level = "1", ci = TRUE) |> add_total() |> add_p() |> to_gt() ``` --- ## Styling the output `customise_table()` applies a visual theme and relabels columns, rows, and factor levels. Pass the result directly; it returns a flextable by default. ```{r style} summary_table(mtcars, by = am, include = c(mpg, wt, cyl), overall = TRUE) |> add_proportion(var = vs) |> add_total() |> add_p() |> customise_table( theme = "journal", title = "Table 1. Baseline characteristics by transmission type", col_labels = c( "Level" = "", "am = 1" = "Manual", "am = 0" = "Automatic" ), row_labels = c( "mpg" = "Miles per gallon", "wt" = "Weight (1000 lbs)", "cyl" = "Cylinders", "vs (1)" = "V-shaped engine" ), accent_color = "#123B7A" ) ``` Available themes: `"default"`, `"journal"`, `"classic"`, `"minimal"`, `"compact"`. ### Complete customisation guide Customisation is deliberately separated from analysis. It changes how the completed table looks, never its estimates, denominators, confidence intervals, or tests. | Task | Argument | Default and useful choices | |---|---|---| | Choose the renderer | `engine` | `"flextable"` (default, Word/PowerPoint friendly) or `"gt"` (HTML focused) | | Apply a visual preset | `theme` | `"default"`, `"journal"`, `"classic"`, `"minimal"`, `"compact"` | | Add headings | `title`, `subtitle` | One character value or `NULL` | | Add explanatory text | `source_note`, `footnotes` | One source note and/or a character vector of extra footnotes | | Rename columns | `col_labels` | Named vector: current column name = new label | | Rename variable rows | `row_labels` | Named vector: current row label = new label | | Rename category levels | `level_labels` | Named vector: current level = new level | | Group columns visually | `spanning_header` | One heading for result columns, or a named list mapping headings to columns | | Align columns | `align` | Named list containing `left`, `center`, and/or `right` column names | | Remove columns visually | `hide_cols` | Character vector of completed column names | | Emphasise columns | `bold_cols`, `italic_cols` | Character vectors of completed column names | | Control typography | `font_size`, `font` | Numeric size and an installed font name | | Control table width | `width` | Percentage from 0 to 100 for `gt` output | | Control individual widths | `column_widths` | Named numeric widths in inches for flextable output | | Add alternating rows | `row_striping`, `stripe_color` | `TRUE`/`FALSE` and a colour such as `"#F4F4F2"` | | Set the accent | `accent_color` | Hex colour used for rules and emphasis | | Choose borders | `borders` | `"horizontal"`, `"all"`, or `"minimal"` | | Change row spacing | `density` | `"standard"`, `"compact"`, or `"spacious"` | | Retain/remove package notes | `show_footnotes` | `TRUE` or `FALSE` | | Emphasise variable labels | `bold_labels` | `TRUE` or `FALSE` | | Format p-values | `pvalue_style` | `"threshold"`, `"fixed"`, or `"scientific"` | | Tune p-values | `pvalue_digits`, `pvalue_threshold`, `pvalue_prefix` | Digits, threshold, and optional `p =` prefix | #### A journal-style recipe ```{r journal-style-recipe} finished_table <- summary_table( birthwt, by = low, include = c(age, lwt, race, smoke), overall = "last", show_dichotomous = "single_row", value = c(smoke = "Yes") ) |> add_p() |> customise_table( theme = "journal", title = "Table 1. Maternal characteristics", spanning_header = "Birth-weight outcome", density = "compact", borders = "horizontal", font_size = 9, pvalue_style = "threshold", pvalue_digits = 3, accent_color = "#4A4A4A", show_footnotes = TRUE ) ``` #### Relabelling without changing the analysis Mappings always use `current = new`. Inspect the completed table first when you are unsure of a displayed column name. ```{r display-only-relabel} summary_table(birthwt, by = low, include = c(race, smoke)) |> customise_table( col_labels = c( "low = Normal birth weight" = "Normal birth weight", "low = Low birth weight" = "Low birth weight" ), row_labels = c("Maternal race" = "Race"), level_labels = c("Yes" = "Smoker", "No" = "Non-smoker") ) ``` #### A clean table without explanatory notes Use this only when the meaning of every statistic is defined in the manuscript text, caption, or journal template. ```{r minimal-report-table} summary_table(birthwt, include = c(age, race, smoke)) |> customise_table( theme = "minimal", show_footnotes = FALSE, bold_labels = TRUE, density = "compact" ) ``` You can also relabel factor levels within the table using `level_labels`: ```{r level-labels} summary_table(mtcars, by = am, include = cyl, overall = TRUE) |> customise_table( level_labels = c( "4" = "4-cylinder", "6" = "6-cylinder", "8" = "8-cylinder" ) ) ``` --- ## Exporting to Word Results already print as flextables. Call `to_flextable()` explicitly when you want to set its font or autofit behaviour at conversion time. ```{r export} res <- summary_table(mtcars, by = am, include = c(mpg, wt, cyl), overall = TRUE) |> add_proportion(var = vs) |> add_total() |> add_p() ft <- to_flextable(res) ``` In an R Markdown or Quarto document targeting Word output, simply print `ft` in a chunk and it will appear as a formatted table in the document. --- ## Summary The gtstats table builder lets you assemble a complete "Table 1" with very little code: ``` summary_table(data, by = group, overall = TRUE) |> add_ci(vars = c(...)) |> add_p() |> add_proportion(var = ...) |> add_total() |> customise_table(theme = "journal", ...) ``` Each `add_*()` function is independent — add only the rows your table needs, in any order that makes sense for your report.