Beautiful Summary Statistics in R with skimr

One skim() call for a rich, readable summary of any data frame

Programming

Learn to summarize a data frame in R with skimr — the skim() function gives per-type summaries, missing counts, quantiles, and inline histograms in one readable table. A better str() and summary() for data exploration.

Author
Published

July 8, 2026

Modified

July 9, 2026

TipWhat you’ll learn
  • One callskim(df) returns a rich, human-readable summary of any data frame.
  • Per-type sections — numeric, factor, character, date, and logical columns each get the statistics that make sense for them.
  • What summary() never told you — missing counts, complete rate, and an inline spark-histogram for every numeric column.
  • Grouped summaries, custom statistics, and pipeline use — plus how to drop the little histograms when your font can’t render them.

You just loaded a new data frame. Before you model or plot anything, you need to see it: how many rows, which columns are numeric vs. categorical, where the missing values hide, and what each distribution roughly looks like. In base R that means squinting at str() for types and summary() for statistics — two outputs, neither complete. skimr collapses that into a single, readable table.

Think of skim() as a better str() and summary() in one — it reports each variable type separately, adds the columns those base functions omit (missing, complete rate, standard deviation, a full set of quantiles), and prints a tiny spark-histogram so you can eyeball the shape of every numeric column at a glance.

Install and load skimr

skimr is on CRAN, so a one-time install is all you need:

install.packages("skimr")

Then load it in your session:

library(skimr)

Summarize a whole data frame

Point skim() at any data frame and it does the rest. We’ll use the built-in iris dataset (150 flowers, four numeric measurements plus the Species factor):

library(skimr)
skim(iris)
Data summary
Name iris
Number of rows 150
Number of columns 5
_______________________
Column type frequency:
factor 1
numeric 4
________________________
Group variables None

Variable type: factor

skim_variable n_missing complete_rate ordered n_unique top_counts
Species 0 1 FALSE 3 set: 50, ver: 50, vir: 50

Variable type: numeric

skim_variable n_missing complete_rate mean sd p0 p25 p50 p75 p100 hist
Sepal.Length 0 1 5.84 0.83 4.3 5.1 5.80 6.4 7.9 ▆▇▇▅▂
Sepal.Width 0 1 3.06 0.44 2.0 2.8 3.00 3.3 4.4 ▁▆▇▂▁
Petal.Length 0 1 3.76 1.77 1.0 1.6 4.35 5.1 6.9 ▇▁▆▇▂
Petal.Width 0 1 1.20 0.76 0.1 0.3 1.30 1.8 2.5 ▇▁▇▅▃

Read the output top to bottom. It opens with a Data Summary — 150 rows, 5 columns — then splits the variables by type: one block for the single factor column and one for the four numeric columns. That per-type split is the whole point: a factor and a numeric measurement need different statistics, and skim() shows each the right ones.

For the factor Species you get n_missing, complete_rate, whether it’s ordered, the number of unique levels (n_unique = 3), and top_counts — here each of the three species appears 50 times. For the numeric columns you get the mean, standard deviation, the five-number quantiles (p0, p25, p50, p75, p100), and a hist column — the little unicode bars that sketch each variable’s distribution. iris has no missing values, so every complete_rate reads 1.

That single table answers “what is this data?” faster than any combination of base functions.

Select specific columns

Pass column names after the data frame to skim only those — handy when a wide table would otherwise scroll off the screen:

library(skimr)
skim(iris, Sepal.Length, Petal.Length)
Data summary
Name iris
Number of rows 150
Number of columns 5
_______________________
Column type frequency:
numeric 2
________________________
Group variables None

Variable type: numeric

skim_variable n_missing complete_rate mean sd p0 p25 p50 p75 p100 hist
Sepal.Length 0 1 5.84 0.83 4.3 5.1 5.80 6.4 7.9 ▆▇▇▅▂
Petal.Length 0 1 3.76 1.77 1.0 1.6 4.35 5.1 6.9 ▇▁▆▇▂

You get the same numeric block, restricted to the two columns you asked for. The selection uses tidyselect, so helpers like starts_with("Sepal") work here too.

Summarize by group

skim() respects grouping. Group the data first, and you get the full per-type summary repeated for each group — the fastest way to compare distributions across a categorical variable. Grouping comes from dplyr::group_by(), so load dplyr alongside skimr:

library(skimr)
library(dplyr)

iris |>
  group_by(Species) |>
  skim()
Data summary
Name group_by(iris, Species)
Number of rows 150
Number of columns 5
_______________________
Column type frequency:
numeric 4
________________________
Group variables Species

Variable type: numeric

skim_variable Species n_missing complete_rate mean sd p0 p25 p50 p75 p100 hist
Sepal.Length setosa 0 1 5.01 0.35 4.3 4.80 5.00 5.20 5.8 ▃▂▇▃▁
Sepal.Length versicolor 0 1 5.94 0.52 4.9 5.60 5.90 6.30 7.0 ▂▇▆▃▃
Sepal.Length virginica 0 1 6.59 0.64 4.9 6.23 6.50 6.90 7.9 ▁▃▇▃▂
Sepal.Width setosa 0 1 3.43 0.38 2.3 3.20 3.40 3.68 4.4 ▁▃▇▅▂
Sepal.Width versicolor 0 1 2.77 0.31 2.0 2.52 2.80 3.00 3.4 ▁▅▆▇▂
Sepal.Width virginica 0 1 2.97 0.32 2.2 2.80 3.00 3.18 3.8 ▂▆▇▅▁
Petal.Length setosa 0 1 1.46 0.17 1.0 1.40 1.50 1.58 1.9 ▁▃▇▃▁
Petal.Length versicolor 0 1 4.26 0.47 3.0 4.00 4.35 4.60 5.1 ▂▂▇▇▆
Petal.Length virginica 0 1 5.55 0.55 4.5 5.10 5.55 5.88 6.9 ▃▇▇▃▂
Petal.Width setosa 0 1 0.25 0.11 0.1 0.20 0.20 0.30 0.6 ▇▂▂▁▁
Petal.Width versicolor 0 1 1.33 0.20 1.0 1.20 1.30 1.50 1.8 ▅▇▃▆▁
Petal.Width virginica 0 1 2.03 0.27 1.4 1.80 2.00 2.30 2.5 ▂▇▆▅▇

Now each numeric statistic — mean, sd, quantiles, and the spark-histogram — is broken out per species. Compare the Petal.Length rows across the three groups and the separation between species jumps out immediately, histogram and all.

Customize the statistics

The default statistics are sensible, but you can build your own skimmer with skim_with(). You supply a list of named functions per data type using sfl() (a “skimr function list”); set append = FALSE to replace the defaults instead of adding to them:

library(skimr)

my_skim <- skim_with(
  numeric = sfl(
    iqr = IQR,
    mad = mad,
    p99 = ~ quantile(., probs = 0.99)
  ),
  append = FALSE
)

my_skim(iris, Sepal.Length)
Data summary
Name iris
Number of rows 150
Number of columns 5
_______________________
Column type frequency:
numeric 1
________________________
Group variables None

Variable type: numeric

skim_variable n_missing complete_rate iqr mad p99
Sepal.Length 0 1 1.3 1.04 7.7

skim_with() returns a new function (my_skim here) that behaves like skim() but reports exactly the statistics you defined — the interquartile range, the median absolute deviation, and a custom 99th percentile via a one-sided formula. Reuse my_skim() on any data frame to get that same tailored summary every time.

Use skim() output in a pipeline

skim() returns a skim_df — a tibble under the hood — so its results flow straight into a tidyverse pipeline. Filter, sort, or select the summary like any data frame. For example, pull just the numeric variables and keep the columns you care about:

library(skimr)
library(dplyr)

skim(iris) |>
  filter(skim_type == "numeric") |>
  select(skim_variable, numeric.mean, numeric.sd)
# A tibble: 4 × 3
  skim_variable numeric.mean numeric.sd
  <chr>                <dbl>      <dbl>
1 Sepal.Length          5.84      0.828
2 Sepal.Width           3.06      0.436
3 Petal.Length          3.76      1.77 
4 Petal.Width           1.20      0.762

Because the summary is data, you can feed it into a report, a gt table, or further computation instead of only reading it off the console. skimr also ships yank() to grab a single type’s block and partition() to split the summary into a list by type.

Drop the histograms when your font can’t render them

The hist column uses unicode block characters to draw its spark-histograms. That looks great in a modern console, but it can render as boxes or question marks in some PDF pipelines, log files, or fonts. When that happens, use skim_without_charts() for a clean, ASCII-safe table:

library(skimr)
skim_without_charts(iris)
Data summary
Name iris
Number of rows 150
Number of columns 5
_______________________
Column type frequency:
factor 1
numeric 4
________________________
Group variables None

Variable type: factor

skim_variable n_missing complete_rate ordered n_unique top_counts
Species 0 1 FALSE 3 set: 50, ver: 50, vir: 50

Variable type: numeric

skim_variable n_missing complete_rate mean sd p0 p25 p50 p75 p100
Sepal.Length 0 1 5.84 0.83 4.3 5.1 5.80 6.4 7.9
Sepal.Width 0 1 3.06 0.44 2.0 2.8 3.00 3.3 4.4
Petal.Length 0 1 3.76 1.77 1.0 1.6 4.35 5.1 6.9
Petal.Width 0 1 1.20 0.76 0.1 0.3 1.30 1.8 2.5

Same summary, no spark-histograms — ideal for rendering to PDF or piping into plain-text reports.

Frequently asked questions

summary() prints a compact set of statistics with all variable types mixed together and gives you no missing-value count or standard deviation. skim() reports each variable type separately (numeric, factor, character, date, logical), adds n_missing, complete_rate, and sd, shows a full set of quantiles, and draws an inline spark-histogram for every numeric column. It also returns the result as a data frame you can pipe onward — summary() does not.

Group the data with dplyr::group_by() before calling skim(): iris |> dplyr::group_by(Species) |> skim(). skim() detects the grouping and repeats the full per-type summary once for each group, so you can compare means, spreads, and distribution shapes across the levels of a categorical variable.

The hist column is drawn with unicode block characters. If your font, PDF engine, or log viewer can’t render them, they degrade to boxes or ?. Call skim_without_charts() for the same summary without the spark-histograms, or switch to a console/font with full unicode support.

Yes. skim() returns a skim_df, which is a tibble, so you can filter(), select(), arrange(), or mutate() it like any data frame — for example, skim(iris) |> dplyr::filter(skim_type == "numeric"). Use yank() to extract a single type’s block or partition() to split the summary into a per-type list.

Build a custom skimmer with skim_with(), passing a sfl() list of named functions per data type: skim_with(numeric = sfl(iqr = IQR), append = FALSE). It returns a new function you call just like skim(). Set append = TRUE to add your statistics on top of the defaults, or FALSE to replace them entirely.

Going deeper in /learn

skim() is where data exploration starts — the next step is to visualize what it reveals. For the full, reproducibility-gated walkthroughs:

  • ggplot2 in R — turn the distributions skim() sketches into publication-grade figures.
  • Data Visualization — the full pillar, from first plot to finished graphic.

Citation

BibTeX citation:
@online{kassambara2026,
  author = {Kassambara, Alboukadel},
  title = {Beautiful {Summary} {Statistics} in {R} with Skimr},
  date = {2026-07-08},
  url = {https://www.datanovia.com/blog/skimr-summary-statistics-in-r},
  langid = {en}
}
For attribution, please cite this work as:
Kassambara, Alboukadel. 2026. “Beautiful Summary Statistics in R with Skimr.” July 8. https://www.datanovia.com/blog/skimr-summary-statistics-in-r.