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 |
A minimal table
For an ordinary Table 1, select all variable types together. There is no need to add continuous and categorical variables separately.
summary_table(mtcars, by = am, include = c(mpg, wt, cyl))Characteristic |
1 |
0 |
|---|---|---|
mpg |
24.4 (6.2) |
17.1 (3.8) |
wt |
2.4 (0.6) |
3.8 (0.8) |
cyl |
||
4 |
8 (61.5%) |
3 (15.8%) |
6 |
3 (23.1%) |
4 (21.1%) |
8 |
2 (15.4%) |
12 (63.2%) |
Continuous data are mean (SD). Categorical data are n (%). | ||
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.
summary_table(mtcars, by = am, include = c(mpg, wt, cyl), overall = TRUE)Characteristic |
Overall |
1 |
0 |
|---|---|---|---|
mpg |
20.1 (6.0) |
24.4 (6.2) |
17.1 (3.8) |
wt |
3.2 (1.0) |
2.4 (0.6) |
3.8 (0.8) |
cyl |
|||
4 |
11 (34.4%) |
8 (61.5%) |
3 (15.8%) |
6 |
7 (21.9%) |
3 (23.1%) |
4 (21.1%) |
8 |
14 (43.8%) |
2 (15.4%) |
12 (63.2%) |
Continuous data are mean (SD). Categorical data are n (%). | |||
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.
# Mean (SD) for every continuous variable
summary_table(
birthwt,
include = c(age, lwt, bwt, smoke),
statistic = "mean_sd"
)Characteristic |
Overall |
|---|---|
Maternal age (years) |
23.2 (5.3) |
Maternal weight (lb) |
129.8 (30.6) |
Birth weight (g) |
2944.6 (729.2) |
Smoking during pregnancy |
|
No |
115 (60.8%) |
Yes |
74 (39.2%) |
Continuous data are mean (SD). Categorical data are n (%). | |
# Recommended summaries for all variables except maternal weight
summary_table(
birthwt,
include = c(age, lwt, bwt, smoke),
statistic = c(lwt = "median_iqr")
)Characteristic |
Overall |
|---|---|
Maternal age (years) |
23.2 (5.3) |
Maternal weight (lb) |
121.0 (110.0–140.0) |
Birth weight (g) |
2944.6 (729.2) |
Smoking during pregnancy |
|
No |
115 (60.8%) |
Yes |
74 (39.2%) |
Continuous data: Maternal age (years): mean (SD); Maternal weight (lb): median (IQR); Birth weight (g): mean (SD). Categorical data are n (%). | |
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.
summary_table(
birthwt,
by = low,
include = c(smoke, ht, race),
show_dichotomous = "single_row",
value = c(smoke = "Yes", ht = "Yes")
) |>
add_p()Characteristic |
Normal birth weight |
Low birth weight |
p-value |
|---|---|---|---|
Smoking during pregnancy |
44 (33.8%) |
30 (50.8%) |
0.040ᵃ |
Hypertension |
5 (3.8%) |
7 (11.9%) |
|
Maternal race |
0.082ᵃ |
||
Black |
15 (11.5%) |
11 (18.6%) |
|
Other |
42 (32.3%) |
25 (42.4%) |
|
White |
73 (56.2%) |
23 (39.0%) |
|
Categorical data are n (%). | |||
ᵃ Chi-square test | |||
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:
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"
)
)Characteristic |
Normal birth weight |
Low birth weight |
|---|---|---|
Maternal age, years |
23.7 (5.6) |
22.3 (4.5) |
Smoking during pregnancy |
||
No |
86 (66%) |
29 (49%) |
Yes |
44 (34%) |
30 (51%) |
Continuous data are mean (SD). Categorical data are n (%). | ||
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:
summary_table(
mtcars,
by = am,
include = c(mpg, cyl, vs),
layout = "separate"
) |>
add_ci()1 |
0 |
|||
|---|---|---|---|---|
Characteristic |
Summary |
95% CI |
Summary |
95% CI |
mpg |
24.4 (6.2) |
20.7–28.1 |
17.1 (3.8) |
15.3–19.0 |
cyl |
||||
4 |
8 (61.5%) |
35.5–82.3% |
3 (15.8%) |
5.5–37.6% |
6 |
3 (23.1%) |
8.2–50.3% |
4 (21.1%) |
8.5–43.3% |
8 |
2 (15.4%) |
4.3–42.2% |
12 (63.2%) |
41.0–80.9% |
vs |
||||
0 |
6 (46.2%) |
23.2–70.9% |
12 (63.2%) |
41.0–80.9% |
1 |
7 (53.8%) |
29.1–76.8% |
7 (36.8%) |
19.1–59.0% |
Continuous data are mean (SD). Categorical data are n (%). Categorical proportions include 95% Wilson score CIs. Continuous means include 95% t-based CIs. | ||||
summary_table(
mtcars,
by = am,
include = c(mpg, cyl, vs),
layout = "separate"
) |>
add_ci(vars = c(mpg, vs), conf.level = 0.90)1 |
0 |
|||
|---|---|---|---|---|
Characteristic |
Summary |
90% CI |
Summary |
90% CI |
mpg |
24.4 (6.2) |
21.3–27.4 |
17.1 (3.8) |
15.6–18.7 |
cyl |
||||
4 |
8 (61.5%) |
3 (15.8%) |
||
6 |
3 (23.1%) |
4 (21.1%) |
||
8 |
2 (15.4%) |
12 (63.2%) |
||
vs |
||||
0 |
6 (46.2%) |
26.1–67.5% |
12 (63.2%) |
44.4–78.6% |
1 |
7 (53.8%) |
32.5–73.9% |
7 (36.8%) |
21.4–55.6% |
Continuous data are mean (SD). Categorical data are n (%). Selected categorical proportions include 90% Wilson score CIs. Continuous means include 90% t-based CIs. | ||||
For a categorical-only table without confidence intervals, counts and percentages can instead occupy distinct child columns:
summary_table(
mtcars,
by = am,
include = c(cyl, vs),
categorical_layout = "separate"
)1 |
0 |
|||
|---|---|---|---|---|
Characteristic |
n |
% |
n |
% |
cyl |
||||
4 |
8 |
61.5% |
3 |
15.8% |
6 |
3 |
23.1% |
4 |
21.1% |
8 |
2 |
15.4% |
12 |
63.2% |
vs |
||||
0 |
6 |
46.2% |
12 |
63.2% |
1 |
7 |
53.8% |
7 |
36.8% |
Categorical data are n and %. | ||||
For compact clinical Table 1 layouts, binary variables can occupy one row while ordinary categorical variables continue to show every level:
summary_table(
birthwt,
by = low,
include = c(smoke, ht, race),
show_dichotomous = "single_row",
value = c(smoke = "Yes", ht = "Yes")
)Characteristic |
Normal birth weight |
Low birth weight |
|---|---|---|
Smoking during pregnancy |
44 (33.8%) |
30 (50.8%) |
Hypertension |
5 (3.8%) |
7 (11.9%) |
Maternal race |
||
Black |
15 (11.5%) |
11 (18.6%) |
Other |
42 (32.3%) |
25 (42.4%) |
White |
73 (56.2%) |
23 (39.0%) |
Categorical data are n (%). | ||
This changes presentation only. add_p() still tests the
full binary variable.
For different continuous summaries by variable, provide a named vector:
summary_table(
mtcars,
by = am,
include = c(mpg, wt, cyl),
statistic = c(mpg = "mean_ci", wt = "median_iqr"),
overall = "last"
)Characteristic |
1 |
0 |
Overall |
|---|---|---|---|
mpg |
24.4 (20.7–28.1) |
17.1 (15.3–19.0) |
20.1 (17.9–22.3) |
wt |
2.3 (1.9–2.8) |
3.5 (3.4–3.8) |
3.3 (2.6–3.6) |
cyl |
|||
4 |
8 (61.5%) |
3 (15.8%) |
11 (34.4%) |
6 |
3 (23.1%) |
4 (21.1%) |
7 (21.9%) |
8 |
2 (15.4%) |
12 (63.2%) |
14 (43.8%) |
Continuous data: mpg: mean (95% CI); wt: median (IQR). Categorical data are n (%). | |||
Choosing percentage denominators and missing rows
Categorical percentages should always identify their denominator:
summary_table(mtcars, by = am, overall = TRUE, include = c(cyl, vs), percent = "column")Characteristic |
Overall |
1 |
0 |
|---|---|---|---|
cyl |
|||
4 |
11 (34.4%) |
8 (61.5%) |
3 (15.8%) |
6 |
7 (21.9%) |
3 (23.1%) |
4 (21.1%) |
8 |
14 (43.8%) |
2 (15.4%) |
12 (63.2%) |
vs |
|||
0 |
18 (56.2%) |
6 (46.2%) |
12 (63.2%) |
1 |
14 (43.8%) |
7 (53.8%) |
7 (36.8%) |
Categorical data are n (%). | |||
summary_table(mtcars, by = am, overall = TRUE, include = c(cyl, vs), percent = "row")Characteristic |
Overall |
1 |
0 |
|---|---|---|---|
cyl |
|||
4 |
11 |
8 (72.7%) |
3 (27.3%) |
6 |
7 |
3 (42.9%) |
4 (57.1%) |
8 |
14 |
2 (14.3%) |
12 (85.7%) |
vs |
|||
0 |
18 |
6 (33.3%) |
12 (66.7%) |
1 |
14 |
7 (50.0%) |
7 (50.0%) |
Categorical data are n (%). Percentages use row denominators. Overall categorical values are counts; grouped percentages use row denominators. | |||
summary_table(mtcars, by = am, overall = TRUE, include = c(cyl, vs), percent = "overall")Characteristic |
Overall |
1 |
0 |
|---|---|---|---|
cyl |
|||
4 |
11 (34.4%) |
8 (25.0%) |
3 (9.4%) |
6 |
7 (21.9%) |
3 (9.4%) |
4 (12.5%) |
8 |
14 (43.8%) |
2 (6.2%) |
12 (37.5%) |
vs |
|||
0 |
18 (56.2%) |
6 (18.8%) |
12 (37.5%) |
1 |
14 (43.8%) |
7 (21.9%) |
7 (21.9%) |
Categorical data are n (%). Percentages use overall denominators. | |||
summary_table(mtcars, by = am, overall = TRUE, include = c(cyl, vs), categorical = "n")Characteristic |
Overall |
1 |
0 |
|---|---|---|---|
cyl |
|||
4 |
11 |
8 |
3 |
6 |
7 |
3 |
4 |
8 |
14 |
2 |
12 |
vs |
|||
0 |
18 |
6 |
12 |
1 |
14 |
7 |
7 |
Categorical data are counts. | |||
"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:
summary_table(
mtcars, by = am, overall = TRUE,
include = c(mpg, wt, vs), missing = "ifany"
)Characteristic |
Overall |
1 |
0 |
|---|---|---|---|
mpg |
20.1 (6.0) |
24.4 (6.2) |
17.1 (3.8) |
wt |
3.2 (1.0) |
2.4 (0.6) |
3.8 (0.8) |
vs |
|||
0 |
18 (56.2%) |
6 (46.2%) |
12 (63.2%) |
1 |
14 (43.8%) |
7 (53.8%) |
7 (36.8%) |
Continuous data are mean (SD). Categorical data are n (%). | |||
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.
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"
)Characteristic |
Overall |
|---|---|
catheter |
|
No |
0 (0.0%) |
Yes |
32 (32.0%) |
Missing |
68 (68.0%) |
Categorical data are n (%). | |
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.
summary_table(
mtcars,
by = am,
include = cyl,
categorical = "n_over_N_percent",
percent = "column",
digits = c(percent = 0)
)Characteristic |
1 |
0 |
|---|---|---|
cyl |
||
4 |
8/13 (62%) |
3/19 (16%) |
6 |
3/13 (23%) |
4/19 (21%) |
8 |
2/13 (15%) |
12/19 (63%) |
Categorical data are n (%). | ||
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).
summary_table(
mtcars,
include = c(mpg, wt),
statistic = "mean_sd",
layout = "separate"
) |>
add_ci()Overall |
||
|---|---|---|
Characteristic |
Mean (SD) |
95% CI |
mpg |
20.1 (6.0) |
17.9–22.3 |
wt |
3.2 (1.0) |
2.9–3.6 |
Continuous data are mean (SD). Continuous means include 95% t-based CIs. | ||
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.
summary_table(
birthwt,
by = smoke,
include = c(age, low),
overall = "first",
layout = "separate"
) |>
add_ci()Overall |
No |
Yes |
||||
|---|---|---|---|---|---|---|
Characteristic |
Summary |
95% CI |
Summary |
95% CI |
Summary |
95% CI |
Maternal age (years) |
23.2 (5.3) |
22.5–24.0 |
23.4 (5.5) |
22.4–24.4 |
22.9 (5.0) |
21.8–24.1 |
Birth-weight outcome |
||||||
Normal birth weight |
130 (68.8%) |
61.9–75.0% |
86 (74.8%) |
66.1–81.8% |
44 (59.5%) |
48.1–69.9% |
Low birth weight |
59 (31.2%) |
25.0–38.1% |
29 (25.2%) |
18.2–33.9% |
30 (40.5%) |
30.1–51.9% |
Continuous data are mean (SD). Categorical data are n (%). Categorical proportions include 95% Wilson score CIs. Continuous means include 95% t-based CIs. | ||||||
summary_table(mtcars, by = am, include = c(mpg, wt, cyl), overall = TRUE) |>
add_proportion(var = vs) |>
to_gt()| Characteristic1 |
Overall N = 321 |
1 N = 131 |
0 N = 191 |
|---|---|---|---|
| mpg | 20.1 (6.0) | 24.4 (6.2) | 17.1 (3.8) |
| wt | 3.2 (1.0) | 2.4 (0.6) | 3.8 (0.8) |
| cyl | |||
| 4 | 11 (34.4%) | 8 (61.5%) | 3 (15.8%) |
| 6 | 7 (21.9%) | 3 (23.1%) | 4 (21.1%) |
| 8 | 14 (43.8%) | 2 (15.4%) | 12 (63.2%) |
| vs (1) | 14 (43.8%); 28.2–60.7% | 7 (53.8%); 29.1–76.8% | 7 (36.8%); 19.1–59.0% |
| 1 Continuous data are mean (SD). Categorical data are n (%). Confidence intervals: 95% intervals use the Wilson score method. | |||
To display confidence intervals and pin the row to a specific level:
summary_table(mtcars, by = am, include = c(mpg, wt, cyl), overall = TRUE) |>
add_proportion(var = vs, level = "1", ci = TRUE) |>
to_gt()| Characteristic1 |
Overall N = 321 |
1 N = 131 |
0 N = 191 |
|---|---|---|---|
| mpg | 20.1 (6.0) | 24.4 (6.2) | 17.1 (3.8) |
| wt | 3.2 (1.0) | 2.4 (0.6) | 3.8 (0.8) |
| cyl | |||
| 4 | 11 (34.4%) | 8 (61.5%) | 3 (15.8%) |
| 6 | 7 (21.9%) | 3 (23.1%) | 4 (21.1%) |
| 8 | 14 (43.8%) | 2 (15.4%) | 12 (63.2%) |
| vs (1) | 14 (43.8%); 28.2–60.7% | 7 (53.8%); 29.1–76.8% | 7 (36.8%); 19.1–59.0% |
| 1 Continuous data are mean (SD). Categorical data are n (%). Confidence intervals: 95% intervals use the Wilson score method. | |||
Adding a total row
add_total() appends a row at the bottom showing the
total N per group.
summary_table(mtcars, by = am, include = c(mpg, wt, cyl), overall = TRUE) |>
add_proportion(var = vs) |>
add_total() |>
to_gt()| Characteristic1 |
Overall N = 321 |
1 N = 131 |
0 N = 191 |
|---|---|---|---|
| mpg | 20.1 (6.0) | 24.4 (6.2) | 17.1 (3.8) |
| wt | 3.2 (1.0) | 2.4 (0.6) | 3.8 (0.8) |
| cyl | |||
| 4 | 11 (34.4%) | 8 (61.5%) | 3 (15.8%) |
| 6 | 7 (21.9%) | 3 (23.1%) | 4 (21.1%) |
| 8 | 14 (43.8%) | 2 (15.4%) | 12 (63.2%) |
| vs (1) | 14 (43.8%); 28.2–60.7% | 7 (53.8%); 29.1–76.8% | 7 (36.8%); 19.1–59.0% |
| Total (N) | 32 | 13 | 19 |
| 1 Continuous data are mean (SD). Categorical data are n (%). Confidence intervals: 95% intervals use the Wilson score method. | |||
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 = TRUEonly 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.
summary_table(birthwt, by = low, include = c(age, bwt, smoke)) |>
add_p(include = -bwt)Characteristic |
Normal birth weight |
Low birth weight |
p-value |
|---|---|---|---|
Maternal age (years) |
23.7 (5.6) |
22.3 (4.5) |
0.078ᵃ |
Birth weight (g) |
3267.0 (2948.0–3651.0) |
2211.0 (1928.0–2395.5) |
|
Smoking during pregnancy |
0.040ᵇ |
||
No |
86 (66.2%) |
29 (49.2%) |
|
Yes |
44 (33.8%) |
30 (50.8%) |
|
Continuous data: Maternal age (years): mean (SD); Birth weight (g): median (IQR). Categorical data are n (%). | |||
ᵃ Welch t-test; ᵇ Chi-square test | |||
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.
summary_table(mtcars, by = am, include = c(mpg, wt, cyl)) |>
add_proportion(var = vs) |>
add_p() |>
to_gt()| Characteristic1 |
1 N = 131 |
0 N = 191 |
p-value2 |
|---|---|---|---|
| mpg | 24.4 (6.2) | 17.1 (3.8) | 0.001ᵃ |
| wt | 2.4 (0.6) | 3.8 (0.8) | <0.001ᵃ |
| cyl | 0.007ᵇ | ||
| 4 | 8 (61.5%) | 3 (15.8%) | |
| 6 | 3 (23.1%) | 4 (21.1%) | |
| 8 | 2 (15.4%) | 12 (63.2%) | |
| vs (1) | 7 (53.8%); 29.1–76.8% | 7 (36.8%); 19.1–59.0% | |
| 1 Continuous data are mean (SD). Categorical data are n (%). Confidence intervals: 95% intervals use the Wilson score method. | |||
| 2 ᵃ Welch t-test; ᵇ Fisher's exact test (Monte Carlo p-value) | |||
Raw and multiplicity-adjusted values are retained separately:
adjusted <- summary_table(mtcars, by = am, include = c(mpg, wt, cyl, vs)) |>
add_p(p_adjust = "BH")
adjusted$p_values
#> # A tibble: 4 × 8
#> variable label row_index test symbol p_value p_adjusted p_adjust_method
#> <chr> <chr> <int> <chr> <chr> <dbl> <dbl> <chr>
#> 1 mpg mpg 1 Welch t-te… ᵃ 1.37e-3 0.00275 BH
#> 2 wt wt 2 Welch t-te… ᵃ 6.27e-6 0.0000251 BH
#> 3 cyl cyl 3 Fisher's e… ᵇ 7.10e-3 0.00947 BH
#> 4 vs vs 6 Chi-square… ᶜ 5.56e-1 0.556 BH
adjusted$assumptions
#> # A tibble: 10 × 6
#> assumption status result detail variable analysis_component
#> <chr> <chr> <chr> <chr> <chr> <chr>
#> 1 Independent observations user_… not_c… Confi… mpg add_p
#> 2 Distribution and influentia… partl… no_sk… Inspe… mpg add_p
#> 3 Independent observations user_… not_c… Confi… wt add_p
#> 4 Distribution and influentia… partl… no_sk… Inspe… wt add_p
#> 5 Independent observations user_… not_c… Confi… cyl add_p
#> 6 Mutually exclusive categori… user_… not_c… Confi… cyl add_p
#> 7 Adequate expected cell coun… check… sparse Autom… cyl add_p
#> 8 Independent observations user_… not_c… Confi… vs add_p
#> 9 Mutually exclusive categori… user_… not_c… Confi… vs add_p
#> 10 Adequate expected cell coun… check… guida… Autom… vs add_p
adjusted$diagnostics
#> # A tibble: 18 × 7
#> check result value threshold detail variable analysis_component
#> <chr> <chr> <chr> <chr> <chr> <chr> <chr>
#> 1 Comparison design indep… Inde… Defined … Indep… mpg add_p
#> 2 Variance assumption welch… var_… User-spe… Welch… mpg add_p
#> 3 Automatic test sel… Welch… Appr… No marke… Two-g… mpg add_p
#> 4 Distribution guida… param… Appr… Marked a… Asses… mpg add_p
#> 5 Observed group spr… descr… 0 (n… Descript… Obser… mpg add_p
#> 6 Comparison design indep… Inde… Defined … Indep… wt add_p
#> 7 Variance assumption welch… var_… User-spe… Welch… wt add_p
#> 8 Automatic test sel… Welch… Poss… No marke… Two-g… wt add_p
#> 9 Distribution guida… param… Poss… Marked a… Asses… wt add_p
#> 10 Observed group spr… descr… 0 (n… Descript… Obser… wt add_p
#> 11 Comparison design indep… Inde… Defined … Indep… cyl add_p
#> 12 Variance assumption not_a… var_… Applies … `var_… cyl add_p
#> 13 Automatic test sel… Fishe… 2.84 No expec… Indep… cyl add_p
#> 14 Expected cell coun… sparse 2.84 No expec… Fishe… cyl add_p
#> 15 Comparison design indep… Inde… Defined … Indep… vs add_p
#> 16 Variance assumption not_a… var_… Applies … `var_… vs add_p
#> 17 Automatic test sel… Chi-s… 5.69 No expec… Indep… vs add_p
#> 18 Expected cell coun… guida… 5.69 No expec… Fishe… vs add_p
adjusted$denominators
#> # A tibble: 14 × 9
#> variable level group n_total n_nonmissing n_missing numerator denominator
#> <chr> <chr> <chr> <int> <int> <int> <dbl> <dbl>
#> 1 mpg NA am = 1 13 13 0 NA 13
#> 2 mpg NA am = 0 19 19 0 NA 19
#> 3 wt NA am = 1 13 13 0 NA 13
#> 4 wt NA am = 0 19 19 0 NA 19
#> 5 cyl 4 am = 1 13 13 0 8 13
#> 6 cyl 4 am = 0 19 19 0 3 19
#> 7 cyl 6 am = 1 13 13 0 3 13
#> 8 cyl 6 am = 0 19 19 0 4 19
#> 9 cyl 8 am = 1 13 13 0 2 13
#> 10 cyl 8 am = 0 19 19 0 12 19
#> 11 vs 0 am = 1 13 13 0 6 13
#> 12 vs 0 am = 0 19 19 0 12 19
#> 13 vs 1 am = 1 13 13 0 7 13
#> 14 vs 1 am = 0 19 19 0 7 19
#> # ℹ 1 more variable: rule <chr>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:
summary_table(mtcars, by = am, include = c(mpg, wt, cyl)) |>
add_p(test = c(mpg = "welch_t", wt = "wilcox", cyl = "chisq")) |>
to_gt()| Characteristic1 |
1 N = 131 |
0 N = 191 |
p-value2 |
|---|---|---|---|
| mpg | 24.4 (6.2) | 17.1 (3.8) | 0.001ᵃ |
| wt | 2.4 (0.6) | 3.8 (0.8) | <0.001ᵇ |
| cyl | 0.013ᶜ | ||
| 4 | 8 (61.5%) | 3 (15.8%) | |
| 6 | 3 (23.1%) | 4 (21.1%) | |
| 8 | 2 (15.4%) | 12 (63.2%) | |
| 1 Continuous data are mean (SD). Categorical data are n (%). | |||
| 2 ᵃ Welch t-test; ᵇ Wilcoxon rank-sum test; ᶜ Chi-square test | |||
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:
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()| Characteristic1 |
before N = 41 |
after N = 41 |
p-value2,3 |
|---|---|---|---|
| score | 10.5 (1.3) | 13.8 (2.2) | 0.098ᵃ |
| 1 Continuous data are mean (SD). | |||
| 2 ᵃ Wilcoxon signed-rank test | |||
| 3 score: paired p-value used 4 complete pairs; 0 excluded because complete matched observations were unavailable. | |||
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.
summary_table(mtcars, by = am, overall = TRUE) |>
add_rate(
event = carb,
time = cyl,
label = "Carburettor rate",
multiplier = 1000
) |>
to_gt()| Characteristic1 |
Overall N = 321 |
1 N = 131 |
0 N = 191 |
|---|---|---|---|
| Carburettor rate | 454.5 (365.5–558.7) | 575.8 (407.4–790.3) | 393.9 (294.2–516.6) |
| 1 Rates per 1,000 person-time use complete event-time pairs and 95% exact Poisson confidence intervals. | |||
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.
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()| Characteristic1 |
Overall N = 321 |
1 N = 131 |
0 N = 191 |
|---|---|---|---|
| mpg | 20.1 (6.0) | 24.4 (6.2) | 17.1 (3.8) |
| wt | 3.2 (1.0) | 2.4 (0.6) | 3.8 (0.8) |
| cyl | |||
| 4 | 11 (34.4%) | 8 (61.5%) | 3 (15.8%) |
| 6 | 7 (21.9%) | 3 (23.1%) | 4 (21.1%) |
| 8 | 14 (43.8%) | 2 (15.4%) | 12 (63.2%) |
| Study period | 2020–2024 | 2020–2024 | 2020–2024 |
| 1 Continuous data are mean (SD). Categorical data are n (%). | |||
The full workflow
Putting it all together — a complete, publication-ready descriptive table:
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()| Characteristic1 |
Overall N = 321 |
1 N = 131 |
0 N = 191 |
p-value2 |
|---|---|---|---|---|
| mpg | 20.1 (6.0) | 24.4 (6.2) | 17.1 (3.8) | 0.001ᵃ |
| wt | 3.2 (1.0) | 2.4 (0.6) | 3.8 (0.8) | <0.001ᵃ |
| cyl | 0.007ᵇ | |||
| 4 | 11 (34.4%) | 8 (61.5%) | 3 (15.8%) | |
| 6 | 7 (21.9%) | 3 (23.1%) | 4 (21.1%) | |
| 8 | 14 (43.8%) | 2 (15.4%) | 12 (63.2%) | |
| vs (1) | 14 (43.8%); 28.2–60.7% | 7 (53.8%); 29.1–76.8% | 7 (36.8%); 19.1–59.0% | |
| Total (N) | 32 | 13 | 19 | |
| 1 Continuous data are mean (SD). Categorical data are n (%). Confidence intervals: 95% intervals use the Wilson score method. | ||||
| 2 ᵃ Welch t-test; ᵇ Fisher's exact test (Monte Carlo p-value) | ||||
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.
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"
)Table 1. Baseline characteristics by transmission type | ||||
|---|---|---|---|---|
Characteristic |
Overall |
Manual |
Automatic |
p-value |
mpg |
20.1 (6.0) |
24.4 (6.2) |
17.1 (3.8) |
0.001ᵃ |
wt |
3.2 (1.0) |
2.4 (0.6) |
3.8 (0.8) |
<0.001ᵃ |
cyl |
0.007ᵇ |
|||
4 |
11 (34.4%) |
8 (61.5%) |
3 (15.8%) |
|
6 |
7 (21.9%) |
3 (23.1%) |
4 (21.1%) |
|
8 |
14 (43.8%) |
2 (15.4%) |
12 (63.2%) |
|
vs (1) |
14 (43.8%); 28.2–60.7% |
7 (53.8%); 29.1–76.8% |
7 (36.8%); 19.1–59.0% |
|
Total (N) |
32 |
13 |
19 |
|
Continuous data are mean (SD). Categorical data are n (%). Confidence intervals: 95% intervals use the Wilson score method. | ||||
ᵃ Welch t-test; ᵇ Fisher's exact test (Monte Carlo p-value) | ||||
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
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.
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")
)Characteristic |
Normal birth weight |
Low birth weight |
|---|---|---|
Maternal race |
||
Black |
15 (11.5%) |
11 (18.6%) |
Other |
42 (32.3%) |
25 (42.4%) |
White |
73 (56.2%) |
23 (39.0%) |
Smoking during pregnancy |
||
No |
86 (66.2%) |
29 (49.2%) |
Yes |
44 (33.8%) |
30 (50.8%) |
Categorical data are n (%). | ||
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.
summary_table(birthwt, include = c(age, race, smoke)) |>
customise_table(
theme = "minimal",
show_footnotes = FALSE,
bold_labels = TRUE,
density = "compact"
)Characteristic |
Overall |
|---|---|
Maternal age (years) |
23.2 (5.3) |
Maternal race |
|
White |
96 (50.8%) |
Black |
26 (13.8%) |
Other |
67 (35.4%) |
Smoking during pregnancy |
|
No |
115 (60.8%) |
Yes |
74 (39.2%) |
You can also relabel factor levels within the table using
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"
)
)Characteristic |
Overall |
1 |
0 |
|---|---|---|---|
cyl |
|||
4 |
11 (34.4%) |
8 (61.5%) |
3 (15.8%) |
6 |
7 (21.9%) |
3 (23.1%) |
4 (21.1%) |
8 |
14 (43.8%) |
2 (15.4%) |
12 (63.2%) |
Categorical data are n (%). | |||
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.
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.