Skip to contents

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
N = 13

0
N = 19

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
N = 32

1
N = 13

0
N = 19

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
N = 189

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
N = 189

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
N = 130

Low birth weight
N = 59

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
N = 130

Low birth weight
N = 59

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
N = 13

0
N = 19

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
N = 13

0
N = 19

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
N = 13

0
N = 19

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
N = 130

Low birth weight
N = 59

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
N = 13

0
N = 19

Overall
N = 32

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
N = 32

1
N = 13

0
N = 19

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
N = 32

1
N = 13

0
N = 19

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
N = 32

1
N = 13

0
N = 19

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
N = 32

1
N = 13

0
N = 19

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
N = 32

1
N = 13

0
N = 19

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
N = 100

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
N = 13

0
N = 19

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
N = 32

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
N = 189

No
N = 115

Yes
N = 74

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 = 32
1
1
N = 13
1
0
N = 19
1
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 = 32
1
1
N = 13
1
0
N = 19
1
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 = 32
1
1
N = 13
1
0
N = 19
1
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 = 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.

summary_table(birthwt, by = low, include = c(age, bwt, smoke)) |>
  add_p(include = -bwt)

Characteristic

Normal birth weight
N = 130

Low birth weight
N = 59

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 = 13
1
0
N = 19
1
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 = 13
1
0
N = 19
1
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 = 4
1
after
N = 4
1
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 = 32
1
1
N = 13
1
0
N = 19
1
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 = 32
1
1
N = 13
1
0
N = 19
1
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 = 32
1
1
N = 13
1
0
N = 19
1
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
N = 32

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
N = 189

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
N = 32

1
N = 13

0
N = 19

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.