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.
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 <- ToothGrowthdf$dose <-factor(df$dose)# tidy summary of len within each doseget_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 <- ToothGrowthdf$dose <-factor(df$dose)# group means, sd, se and ci by dosedesc_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 <- ToothGrowthdf$dose <-factor(df$dose)ggsummarystats( df, x ="dose", y ="len",ggfunc = ggboxplot, add ="jitter",summaries =c("n", "median", "iqr"))
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 <- ToothGrowthdf$dose <-factor(df$dose)ggsummarystats( df, x ="dose", y ="len",ggfunc = ggboxplot, add ="jitter",color ="dose", palette ="npg",summaries =c("n", "median", "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 <- ToothGrowthdf$dose <-factor(df$dose)ggsummarystats( df, x ="dose", y ="len",ggfunc = ggviolin, add =c("jitter", "median_iqr"),summaries =c("n", "median", "iqr"))
Switch to a bar plot of means simply by naming ggfunc = ggbarplot (or ggline for a line):
library(ggpubr)df <- ToothGrowthdf$dose <-factor(df$dose)ggsummarystats( df, x ="dose", y ="len",ggfunc = ggbarplot, add =c("mean_se", "jitter"),summaries =c("n", "mean", "sd"))
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 <- ToothGrowthdf$dose <-factor(df$dose)ggsummarystats( df, x ="dose", y ="len",ggfunc = ggboxplot, add ="jitter",color ="supp", palette ="npg",summaries =c("n", "median", "iqr"))
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.
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 <- ToothGrowthdf$dose <-factor(df$dose)# 1. compute the statistics with rstatixsummary_stats <-get_summary_stats(group_by(df, dose), len, type ="common")summary_stats <- summary_stats[, c("dose", "n", "median", "iqr")]# 2a. the main plotbxp <-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 stripsummary_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 heightggarrange( bxp, summary_plot,ncol =1, align ="v",heights =c(0.80, 0.20))
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
NoteHow do I add a summary statistics table to a plot in R?
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").
NoteHow do I compute group summary statistics (n, mean, median, IQR) in R?
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.
NoteWhat is the difference between ggsummarystats() and ggsummarytable()?
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.
NoteHow do I choose which statistics appear under the plot?
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.
NoteCan I add a summary table to a violin, bar or line plot too?
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
ImportantExercise: a box plot with a summary table beneath
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 <- ToothGrowthdf$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 summaries — sd 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.
This lesson is reproducible: the figures are executed at build time (if they render, the code works), and the sandbox + quiz re-run live in your browser. The runtime is the judge.
@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}
}