install.packages("skimr")Beautiful Summary Statistics in R with skimr
One skim() call for a rich, readable summary of any data frame
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.
- One call —
skim(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:
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)| 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)| 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()| 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)| 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)| 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
@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}
}