
These are the R packages I install on a fresh machine before anything else.
None are exotic — that’s the point. I use the native pipe |> throughout;
it ships with base R and needs no dependencies.
NOTE
Every block on this page is a copy-paste-ready listing. Run the setup block
first if you want to follow along locally; later blocks assume it. Install
anything missing once with install.packages().
library(tidyverse)
library(palmerpenguins)
set.seed(42)
The tidyverse
The tidyverse’s contribution isn’t any single function — it’s consistency: data frames in, data frames out; the first argument is always the data; functions do one thing. Learn one package and the next feels familiar.
penguins |>
filter(!is.na(body_mass_g)) |>
group_by(species, sex) |>
summarise(n = n(), mean_mass_g = mean(body_mass_g), .groups = "drop") |>
arrange(desc(mean_mass_g))
library(tidyverse) attaches ggplot2, dplyr, tidyr, readr, purrr, tibble,
stringr, forcats, and lubridate.
ggplot2
Implements the grammar of graphics: data mapped to visual properties (aesthetics), drawn with geometric objects (geoms) in layers. More below.
ggplot(penguins, aes(flipper_length_mm, body_mass_g, colour = species)) +
geom_point(alpha = 0.8) +
labs(title = "Flipper length predicts body mass", colour = "Species")
dplyr
The grammar of data manipulation: filter() rows, select() columns,
mutate() new columns, arrange() order, summarise() groups, .by for
per-call grouping.
penguins |>
filter(!is.na(bill_length_mm)) |>
mutate(bill_ratio = bill_length_mm / bill_depth_mm) |>
summarise(n = n(), mean_ratio = mean(bill_ratio), .by = species) |>
arrange(desc(mean_ratio))
Favorites: count(), slice_max()/slice_min(), case_when(), joins
(left_join() etc.), and across() for applying one transform to many
columns at once.
tidyr
Reshapes data between wide and long with pivot_longer() /
pivot_wider() — the most common data-cleaning task there is. Also handy:
separate_wider_delim(), unnest(), complete().
penguins |>
select(species, bill_length_mm, bill_depth_mm, flipper_length_mm) |>
pivot_longer(cols = ends_with("_mm"), names_to = "measurement", values_to = "value")
readr
Imports CSVs/TSVs faster and more predictably than base R, returning a tibble and never silently converting strings to factors. More below.
purrr
Functional iteration: map_dbl(x, f) applies f to each element and
guarantees a double vector back, erroring loudly otherwise. More below.
tibble
A stricter, friendlier data.frame: no silent type coercion, no partial
name matching, clean columnar printing, and support for list-columns for
“nest, map a model, unnest” workflows.
stringr
Every function starts with str_, string first, pattern second. Most
used: str_detect(), str_replace_all(), str_extract(), str_c().
forcats
Makes factor level order manageable — which controls bar/legend/facet
order everywhere. fct_reorder() sorts a bar chart by value instead of
alphabetically; also fct_infreq(), fct_lump_n(), fct_recode().
lubridate
Makes dates tractable — parsing, arithmetic, time zones. More below.
ymd("2026-07-20") + months(3)
ggplot2 in depth
Every ggplot is built from: data, aesthetic mappings (aes()), geoms,
scales, facets, and a coordinate system/theme. You assemble these with
+, one layer at a time.
ggplot(penguins, aes(flipper_length_mm, body_mass_g)) +
geom_point(aes(colour = species), alpha = 0.7) +
geom_smooth(method = "lm", se = TRUE, colour = "black")
Where you put an aesthetic mapping determines what it affects — the
colour mapping lives only on geom_point(), so one regression line
fits all points.
Scales control axis breaks and palettes (scale_colour_viridis_c(),
scales::label_number()). Facets split a plot into small multiples
with one line (facet_wrap(~ var)). Annotations (geom_hline(),
annotate()) point the reader at what matters. Themes
(theme_minimal(), theme()) control every non-data element — define a
house theme once and reuse it, or set it as default with theme_set().
ggplot(penguins, aes(flipper_length_mm, body_mass_g, colour = species)) +
geom_point(alpha = 0.7, na.rm = TRUE) +
facet_wrap(~ island) +
theme_minimal(base_size = 13) +
theme(legend.position = "bottom")
Finish with ggsave() — vector formats for print, .png at 300 dpi for
web/slides, dimensions set in the save call.
readr in depth
read_csv("id,species,mass_g\n1,Adelie,3750\n2,Gentoo,5400")
readr prints a column specification of what it guessed. Best practice:
state expected types with col_types instead of letting it guess, so
surprises become explicit errors:
read_csv(
"id,species,mass_g\n1,Adelie,3750",
col_types = cols(id = col_integer(), species = col_character(), mass_g = col_double())
)
parse_number(), parse_date(), and friends do the same parsing outside
of import, and the na = c(...) argument to read_csv() declares which
sentinel values mean missing.
purrr in depth
nums <- list(a = 1:5, b = 6:10, c = 11:15)
map_dbl(nums, mean)
Typed variants (map_dbl, map_int, map_chr, map_lgl) guarantee
their output type, unlike sapply(). Build a tibble from many results
with map() |> list_rbind(). When some iterations may fail, possibly()
substitutes a fallback value and safely() captures the error instead of
stopping the loop. map2() and pmap() extend the same idea to two or
more inputs.
lubridate in depth
Parsing functions are named after the order of date parts — ymd,
mdy, dmy — so you never write a format string. floor_date() /
ceiling_date() bucket timestamps into days, weeks, or months.
lubridate distinguishes three time-span types: durations (ddays())
are exact clock time; periods (months()) track calendar units;
intervals (a %--% b) are a specific span you can measure or test
membership against. with_tz() shows the same instant in another zone;
force_tz() relabels a clock reading — mixing the two is the classic
time-zone bug.
start <- ymd("2026-01-31")
start + months(1) # calendar-aware: Feb 28
start + ddays(30) # exactly 30 * 86400 seconds later
Utility packages
The tidyverse gets the headlines, but reproducibility rests on a quieter set of tools.
fs
A consistent, cross-platform file-system API. Functions start with
file_, dir_, or path_, return tidy character vectors, and behave
identically across OSes.
library(fs)
path("data", "raw", "penguins.csv") |> path_ext()
here
Solves paths that break when a script runs from a different working
directory. here::here() builds paths relative to the project root (the
folder with .Rproj/.git), unlike a hard-coded setwd() that only
works on one machine.
glue
String interpolation done right — write the string once, drop
{expressions} inline, vectorized.
library(glue)
species <- "Gentoo"; n <- 124
glue("We measured {n} {species} penguins.")
For real database work, glue::glue_sql() quotes identifiers and escapes
values to guard against injection.
conflicted
When two loaded packages export the same function name, R silently uses
whichever loaded last. conflicted turns that into a loud error and
lets you declare a winner once with conflict_prefer().
arrow
arrow provides a fast, memory-efficient interface to Apache Arrow —
read/write Parquet and CSV, and query datasets far larger than memory
without loading them whole. read_csv_arrow() and read_parquet() are
drop-in-fast alternatives to readr for big files; open_dataset() lets
dplyr verbs run lazily over a folder of files, pushing the computation
down to Arrow.
renv
Gives each project an isolated package library and a lockfile
(renv.lock) recording exact versions — like a Python virtual environment
paired with an npm lockfile, for R. Core
workflow: renv::init(), work normally, renv::snapshot() to record
state, renv::restore() on another machine to reproduce it exactly.
The easystats ecosystem
easystats does for statistical modeling what the tidyverse did for
data wrangling: one consistent API across model types (lm, glm,
lmer, brms, and more).
model <- lm(body_mass_g ~ flipper_length_mm + species + sex, data = penguins)
- insight reaches inside any model object for its formula, data, and parameters — the foundation the rest of easystats builds on.
- parameters turns coefficients, SEs, CIs, and p-values into one
clean table, working the same for
lmor a mixed model. - performance assesses model quality — R², RMSE, AIC/BIC,
collinearity — and
check_model()produces a full diagnostic dashboard. - effectsize computes and interprets standardized effect sizes (Cohen’s d, eta-squared) in plain language.
- correlation runs Pearson/Spearman/partial/Bayesian correlations as one tidy call, ready for plotting.
- datawizard is the data-prep layer for statistics: standardizing, centering, rescaling, recoding.
- modelbased computes model-based predictions and estimated marginal means directly, instead of hand-deriving them from coefficients.
- bayestestR gives the vocabulary of Bayesian inference — credible intervals, probability of direction, ROPE, Bayes factors.
- see is easystats’ ggplot2-based visualization layer, powering
plot()methods across the other packages. - report writes a publication-ready paragraph describing a model, a
data frame, or a
t.test()— a genuine first draft of a results section.
parameters::model_parameters(model)
report::report(model)
Visualization extensions
Three packages that extend ggplot2 for problems it deliberately leaves open.
ggdist visualizes distributions and uncertainty with “slabinterval”
geoms — stat_halfeye() draws a density shape plus a point-and-interval
summary, instead of a bare mean ± SE.
penguins |>
filter(!is.na(body_mass_g)) |>
ggplot(aes(body_mass_g, species, fill = species)) +
ggdist::stat_halfeye(show.legend = FALSE)
patchwork composes separate ggplots with arithmetic: + grids them,
| places side by side, / stacks vertically, and a trailing &
applies a theme change to every subplot at once.
cowplot predates some of patchwork’s features and remains the
go-to when you need pixel-precise axis alignment across panels
(plot_grid()) or its drawing tools for annotating figures with logos or
insets. I default to patchwork for everyday composition and reach for
cowplot when alignment needs to be exact.
Conclusion
None of these packages is magic alone — what makes them favorites is how they compose: readr hands a tibble to dplyr, which hands it to ggplot2, while here and renv keep paths and versions honest. Start with the tidyverse core, add here and renv on day one, and pull in easystats, ggdist, and patchwork as your projects demand them.