Summary Statistics on a Plot in R (ggpubr)

Compute n, mean, median & IQR with rstatix, then label them on a ggpubr plot with a summary table beneath

Data Visualization

Add summary statistics to a plot in R with ggpubr and rstatix. Compute group statistics (n, mean, sd, median, IQR) with get_summary_stats() and desc_statby(), draw the one-call plot-plus-table with ggsummarystats(), and build the recipe by hand — a box plot stacked over a summary table with ggsummarytable() and ggarrange(). Every example renders and runs live in your browser.

Published

June 22, 2026

Modified

July 7, 2026

TipKey takeaways
  • Get the numbers first. rstatix::get_summary_stats() returns a tidy table of group statistics (n, mean, sd, median, IQR, …); desc_statby() is the ggpubr equivalent with se and ci.
  • One call does plot + table: ggsummarystats() draws a box/violin/bar/line plot with a summary table beneath it, aligned to the x groups — pick the plot with ggfunc = ggboxplot (or ggviolin, ggbarplot, ggline).
  • Choose which statistics appear with summaries = c("n", "median", "iqr") and colour or group exactly as you would the underlying plot.
  • Build it by hand for full control: a plot on top, a ggsummarytable() of the stats beneath, stacked with ggarrange(..., ncol = 1, align = "v") — the “beautiful plot with a summary table” recipe.
  • Facet into multipanel with facet.by — each panel gets its own aligned summary table.
  • Every plot below is rendered for you, then editable live in the sandbox.
Get the book — R Graphics Essentials (PDF)

Introduction

A great group-comparison figure does two jobs at once: it shows the distribution and it states the numbers — sample size, median, IQR — so a reader does not have to squint at the boxes. In R, the ggpubr package and its statistics companion rstatix make this a one-call recipe: compute the summary, then label it on the plot or draw a small table beneath each group.

Two pieces do the work:

  • rstatix computes the statistics — get_summary_stats() for a tidy n/mean/sd/median/IQR table, desc_statby() (in ggpubr) when you also want standard error and confidence intervals.
  • ggpubr draws them — ggsummarystats() for the all-in-one plot + aligned summary table, or ggsummarytable() + ggarrange() when you want to build the panel by hand.

This lesson walks the whole workflow on the built-in ToothGrowth data — tooth len across three dose levels and two supp supplements. Every figure is rendered right here, and you can re-run or edit any of them live.

Want a free-standing, fully styled table (coloured cells, custom theme, a table as the figure)? That is the ggtexttable lesson — here we use the table only as a caption strip beneath a plot.

Get the summary statistics first

Before you can label statistics on a plot, you need them as a data frame. get_summary_stats() from rstatix takes the data and the variable and returns a tidy table — one row per group, columns for n, mean, sd, median, IQR and more. Group with a formula or the vars/group argument; here we summarise len within each dose.

library(rstatix)
df <- ToothGrowth
df$dose <- factor(df$dose)

# tidy summary of len within each dose
get_summary_stats(group_by(df, dose), len, type = "common")
# A tibble: 3 × 11
  dose  variable     n   min   max median   iqr  mean    sd    se    ci
  <fct> <fct>    <dbl> <dbl> <dbl>  <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
1 0.5   len         20   4.2  21.5   9.85  5.03  10.6  4.5  1.01   2.11
2 1     len         20  13.6  27.3  19.2   7.12  19.7  4.42 0.987  2.07
3 2     len         20  18.5  33.9  26.0   4.3   26.1  3.77 0.844  1.77

Pass type = to choose the columns: "common" (n, min, max, median, IQR, mean, sd, se, ci), "mean_sd", "median_iqr", "full", or a custom vector. The ggpubr equivalent, desc_statby(), returns the same kind of table with the columns ggpubr’s plots use (mean, sd, se, ci):

library(ggpubr)
df <- ToothGrowth
df$dose <- factor(df$dose)

# group means, sd, se and ci by dose
desc_statby(df, measure.var = "len", grps = "dose")[, c("dose", "length", "mean", "sd", "se", "ci")]
  dose length   mean       sd        se       ci
1  0.5     20 10.605 4.499763 1.0061776 2.105954
2    1     20 19.735 4.415436 0.9873216 2.066488
3    2     20 26.100 3.774150 0.8439257 1.766357

The one-call recipe: ggsummarystats()

ggsummarystats() is the shortcut for the whole figure: give it the data, the plot function and the columns, and it draws the plot with a summary table aligned beneath each group. The ggfunc argument chooses the plot — ggboxplot, ggviolin, ggbarplot or ggline — and everything else (add, color, palette) is passed straight through to it.

library(ggpubr)
df <- ToothGrowth
df$dose <- factor(df$dose)

ggsummarystats(
  df, x = "dose", y = "len",
  ggfunc = ggboxplot, add = "jitter",
  summaries = c("n", "median", "iqr")
)

A ggpubr box plot of tooth length by dose with jittered points, and a summary table of sample size, median and IQR drawn beneath each dose group.

Choose which statistics appear in the strip with summaries = (any columns get_summary_stats() produces). And because ggfunc’s extra arguments pass through, you colour by group exactly as on a normal ggboxplot():

library(ggpubr)
df <- ToothGrowth
df$dose <- factor(df$dose)

ggsummarystats(
  df, x = "dose", y = "len",
  ggfunc = ggboxplot, add = "jitter",
  color = "dose", palette = "npg",
  summaries = c("n", "median", "iqr")
)

A ggpubr box plot of tooth length by dose coloured by dose with the npg palette, over a summary table of n, median and IQR.

A different plot, same table

ggfunc is the only thing you change to swap the plot above the table. A violin shows the distribution shape; a bar or line of means works when the summary, not the spread, is the point. The summary table beneath stays aligned to the same x groups.

library(ggpubr)
df <- ToothGrowth
df$dose <- factor(df$dose)

ggsummarystats(
  df, x = "dose", y = "len",
  ggfunc = ggviolin, add = c("jitter", "median_iqr"),
  summaries = c("n", "median", "iqr")
)

A ggpubr violin plot of tooth length by dose with jittered points, over a summary table of sample size, median and IQR per dose.

Switch to a bar plot of means simply by naming ggfunc = ggbarplot (or ggline for a line):

library(ggpubr)
df <- ToothGrowth
df$dose <- factor(df$dose)

ggsummarystats(
  df, x = "dose", y = "len",
  ggfunc = ggbarplot, add = c("mean_se", "jitter"),
  summaries = c("n", "mean", "sd")
)

A ggpubr bar plot of mean tooth length by dose with standard-error bars, over a summary table of n, mean and sd per dose.

Group by a second variable

Pass a grouping variable to color and ggsummarystats() splits both the plot and the table by that group — one row of statistics per subgroup. Here we colour by supplement (supp):

library(ggpubr)
df <- ToothGrowth
df$dose <- factor(df$dose)

ggsummarystats(
  df, x = "dose", y = "len",
  ggfunc = ggboxplot, add = "jitter",
  color = "supp", palette = "npg",
  summaries = c("n", "median", "iqr")
)

A ggpubr box plot of tooth length by dose with the two supplements coloured by the npg palette, over a summary table with one statistics row per supplement and dose.

Multipanel: a summary table per facet

Give facet.by a grouping variable to split the figure into panels — each panel keeps its own aligned summary table. labeller = "label_both" prints the variable name with the level; "label_value" prints the level alone.

library(ggpubr)
df <- ToothGrowth
df$dose <- factor(df$dose)

ggsummarystats(
  df, x = "dose", y = "len",
  ggfunc = ggboxplot, add = "jitter",
  color = "dose", palette = "jco",
  facet.by = "supp", labeller = "label_value",
  ggtheme = theme_bw(), legend = "top",
  summaries = c("n", "median", "iqr")
)

A two-panel ggpubr box plot of tooth length by dose, one panel per supplement, each panel carrying its own summary table of n, median and IQR.

Build the recipe by hand

ggsummarystats() is the shortcut; building it yourself gives full control over each piece — the plot theme, the exact statistics, the relative heights. The pattern is three steps: (1) compute the statistics, (2) draw the plot and a ggsummarytable() of those statistics, (3) stack them with ggarrange(..., ncol = 1, align = "v") so the table sits beneath the plot, columns aligned to the x groups.

library(ggpubr)
library(rstatix)
df <- ToothGrowth
df$dose <- factor(df$dose)

# 1. compute the statistics with rstatix
summary_stats <- get_summary_stats(group_by(df, dose), len, type = "common")
summary_stats <- summary_stats[, c("dose", "n", "median", "iqr")]

# 2a. the main plot
bxp <- ggboxplot(
  df, x = "dose", y = "len", add = "jitter",
  ggtheme = theme_bw()
)

# 2b. the summary table as a plot, stripped of axes for a clean caption strip
summary_plot <- ggsummarytable(
  summary_stats, x = "dose", y = c("n", "median", "iqr"),
  ggtheme = theme_bw()
) +
  clean_table_theme()

# 3. stack plot over table, vertically aligned, table given 20% of the height
ggarrange(
  bxp, summary_plot,
  ncol = 1, align = "v",
  heights = c(0.80, 0.20)
)

A hand-built ggpubr figure: a box plot of tooth length by dose with jittered points on top, and a summary table of sample size, median and IQR aligned beneath, combined with ggarrange.

ggsummarytable() turns a stats data frame into a labelled strip (one column of numbers per x group); clean_table_theme() removes its axes and gridlines so it reads as a caption. ggarrange() does the stacking — align = "v" keeps the table columns under the matching boxes, and heights sets the plot-to-table ratio.

Need a richly styled table — coloured header rows, highlighted cells, a table that is the figure rather than a strip beneath one? Reach for ggtexttable(): see the ggtexttable lesson for ttheme(), table_cell_bg() and friends.

Try it live

The plots above were rendered at build time. Want to experiment? Edit the code and press Run — it executes in your browser via webR (no server, no install). Try switching ggfunc = ggboxplot to ggviolin, changing the summaries to c("n", "mean", "sd"), or colouring by color = "supp".

Working in Python? A seaborn + statistics-table guide is coming to the Python series.

🟢 With an AI agent

Ask Prova “how do I add a summary table of n, median and IQR beneath my box plot?” — it answers with code you can run on your own data frame, computing the statistics with get_summary_stats() and stacking the table with ggarrange(), so the figure is reproducible. The runtime is the judge. Ask Prova →

Common issues

  • The summary table is missing or empty. summaries = must name columns that get_summary_stats() actually produces — use lowercase "n", "median", "iqr", "mean", "sd". A typo or a stat not in the chosen type yields a blank strip.
  • The table columns don’t line up under the boxes. When you build by hand, use ggarrange(..., align = "v") so the plot and ggsummarytable() share the same x scale; without it the columns drift.
  • get_summary_stats() summarises the whole data, ignoring groups. It only groups if you tell it — wrap the data in group_by(df, dose) (or pass the grouping variable) before calling it.
  • The table strip still shows axes and gridlines. Add clean_table_theme() to the ggsummarytable() so it reads as a caption rather than a second plot.
  • x must be a factor. If the groups come out as numbers or in the wrong order, run df$dose <- factor(df$dose) first.

Frequently asked questions

The fastest way is ggsummarystats() from ggpubr: ggsummarystats(ToothGrowth, x = "dose", y = "len", ggfunc = ggboxplot, summaries = c("n", "median", "iqr")) draws a box plot with a table of sample size, median and IQR aligned beneath each group. To build it by hand, compute the stats with rstatix::get_summary_stats(), turn them into a strip with ggsummarytable(), and stack the two with ggarrange(plot, table, ncol = 1, align = "v").

Use rstatix::get_summary_stats(): get_summary_stats(group_by(df, dose), len, type = "common") returns a tidy table with n, min, max, median, IQR, mean, sd, se and ci per group. Pick the columns with type = ("common", "mean_sd", "median_iqr", "full"). ggpubr’s desc_statby() returns the same kind of table with the mean/sd/se/ci columns its plots use.

ggsummarystats() is the all-in-one: it draws the plot and the aligned summary table together in one call. ggsummarytable() draws only the table — a strip of numbers, one column per x group — which you then combine with your own plot using ggarrange(). Use ggsummarystats() for the quick figure and ggsummarytable() when you want full control over the plot and the table separately.

Pass them to summaries =, e.g. summaries = c("n", "median", "iqr") or c("n", "mean", "sd"). The names must match columns that get_summary_stats() produces (lowercase: n, mean, sd, median, iqr, se, ci, …). The order you list them is the order of the rows in the table strip.

Yes — ggsummarystats() works with any of ggpubr’s core plotters. Set ggfunc = ggviolin, ggbarplot or ggline (instead of ggboxplot) and the aligned summary table is drawn beneath the same x groups. Extra arguments like add = c("mean_se", "jitter"), color and palette pass straight through to the chosen plot function.

Test your understanding

Complete the call so each dose shows a box plot of tooth length with jittered points, over a summary table of n, median and IQR.

# ggfunc names the plot function (no quotes): ggboxplot
# summaries is a character vector of stat columns: c("n", "median", "iqr")
library(ggpubr) df <- ToothGrowth df$dose <- factor(df$dose) ggsummarystats( df, x = "dose", y = "len", ggfunc = ggboxplot, add = "jitter", summaries = c("n", "median", "iqr") )
library(ggpubr)
df <- ToothGrowth
df$dose <- factor(df$dose)

ggsummarystats(
  df, x = "dose", y = "len",
  ggfunc = ggboxplot, add = "jitter",
  summaries = c("n", "median", "iqr")
)

Conceptual check. You want the spread of the data shown numerically, not the precision of the mean. Which column do you put in summariessd or se — and why? (Answer: sd. Standard deviation describes the variability of the observations; standard error shrinks with sample size and describes how precisely the mean is estimated.)

Conclusion

You added summary statistics to a plot the ggpubr way: compute them with rstatix::get_summary_stats() (or desc_statby()), draw the all-in-one plot-plus-table with ggsummarystats() — choosing the plot via ggfunc and the columns via summaries — and, when you need full control, build the recipe by hand with ggsummarytable() + clean_table_theme() stacked under the plot by ggarrange(..., align = "v"). The same pattern facets into multipanel figures, each panel carrying its own table.

For a fully styled, free-standing table — coloured cells, custom themes, a table as the figure — see the ggtexttable lesson. To annotate these comparisons with tests, continue with Add p-values.

Reuse

Citation

BibTeX citation:
@online{2026,
  author = {},
  title = {Summary {Statistics} on a {Plot} in {R} (Ggpubr)},
  date = {2026-06-22},
  url = {https://www.datanovia.com/learn/data-visualization/ggpubr/summary-stats-labels},
  langid = {en}
}
For attribution, please cite this work as:
“Summary Statistics on a Plot in R (Ggpubr).” 2026. June 22. https://www.datanovia.com/learn/data-visualization/ggpubr/summary-stats-labels.