Data Wrangling in R with dplyr and tidyr
Edited by Dev Kilaru, Senior Data Analyst. How we make our content
Introduction
The dplyr package gives you a small set of verbs that handle most data manipulation: filter rows, select columns, create new columns, sort, and summarize by group. Combined with the pipe they read like a recipe, which makes analysis code far easier to review than nested base R. The tidyr package handles reshaping. This lesson uses the built-in mtcars dataset, so you can run every example without downloading anything.
Prerequisites
- The tidyverse installed with install.packages("tidyverse")
- Familiarity with data frames and the pipe from earlier lessons
Step-by-Step Guide
Step 1: Load the packages and look at the data
Load the tidyverse, which attaches dplyr, tidyr, ggplot2, and readr together. The mtcars dataset has 32 cars with columns such as mpg for fuel economy, cyl for number of cylinders, hp for horsepower, wt for weight, and am for transmission type. Its car names are stored as row names rather than a column, so convert them into a proper column first. Moving identifiers into a column is a good habit, because dplyr verbs work on columns and may discard row names.
library(tidyverse)
cars <- mtcars |>
rownames_to_column("model") |>
as_tibble()
glimpse(cars)Step 2: Pick rows with filter and columns with select
The filter verb keeps rows where a condition is TRUE, and multiple conditions separated by commas must all hold. Unlike base R indexing, filter silently drops rows where the test is NA, so there are no surprise NA rows. The select verb picks columns by name, and you can use helpers such as starts_with, ends_with, and everything, or a minus sign to drop columns. Both return a new data frame and leave the original untouched.
cars |>
filter(cyl == 4, mpg > 30) |>
select(model, mpg, hp, wt)
cars |> select(-qsec, -vs) # drop two columns
cars |> select(model, starts_with("d"))Step 3: Create columns with mutate and sort with arrange
The mutate verb adds or changes columns, and later expressions in the same call can use columns made earlier. Use case_when for several categories in one readable block, with the first matching condition winning and a final TRUE line acting as the default. The arrange verb sorts rows, and wrapping a column in desc reverses the order. Together they cover most of the cleanup and feature engineering you will do before analysis.
cars |>
mutate(
kpl = mpg * 0.4251, # miles per gallon to km per litre
size = case_when(
wt < 2.5 ~ "light",
wt < 3.5 ~ "medium",
TRUE ~ "heavy"
)
) |>
arrange(desc(kpl)) |>
select(model, mpg, kpl, size)Step 4: Summarize by group
The core analysis pattern is split, apply, combine. Call group_by to define the groups, then summarise to collapse each group to one row of statistics. Use n() to count rows in each group. For a quick count of categories, count is a shortcut. The summary of the four-cylinder group shows the expected result: those cars average about 26.7 miles per gallon, far better than the 15.1 of eight-cylinder cars. Call ungroup when you are finished if you continue working with the result, so later steps do not stay grouped by accident.
cars |>
group_by(cyl) |>
summarise(
cars = n(),
avg_mpg = mean(mpg),
max_hp = max(hp)
)
# cyl 4: 11 cars, avg_mpg about 26.7
# cyl 6: 7 cars, avg_mpg about 19.7
# cyl 8: 14 cars, avg_mpg about 15.1
cars |> count(cyl, am) # how many cars in each cylinder and transmission combinationStep 5: Join tables with left_join
Real projects combine tables, such as orders and customers, through a shared key. A left_join keeps every row of the first table and adds matching columns from the second, filling NA where there is no match. Use inner_join to keep only rows found in both tables. Name the key with the by argument, and use a named vector when the key columns have different names in each table. After a join, check the row count, because duplicated keys in the second table multiply rows and are a frequent source of wrong totals.
origin <- tibble(
cyl = c(4, 6, 8),
engine_class = c("economy", "mid-range", "performance")
)
cars |>
left_join(origin, by = "cyl") |>
count(engine_class)Step 6: Reshape with pivot_longer and pivot_wider
Tidy data has one variable per column and one observation per row. Reports are often wide, with a column per month, but plotting and grouping usually need long form with a column naming the month and a column holding the value. The pivot_longer function gathers columns into name and value pairs, and pivot_wider spreads them back out. Learning to move between the two shapes is what lets the same data feed both a chart and a summary table.
sales_wide <- tibble(
region = c("East", "West"),
jan = c(100, 80),
feb = c(120, 90)
)
sales_long <- sales_wide |>
pivot_longer(cols = c(jan, feb), names_to = "month", values_to = "revenue")
sales_long |>
pivot_wider(names_from = month, values_from = revenue)FAQs
- What is the difference between dplyr and base R?
- Both can do the same work. dplyr uses consistent verbs and works naturally with the pipe, so the code reads in the order the steps happen. Base R needs no packages, which matters for small scripts and for code that must run on locked-down servers.
- Why did my summary return one row per group, then keep behaving oddly?
- Grouping persists on the result until you remove it. summarise drops one level of grouping, so a result grouped by two columns is still grouped by the first. Add ungroup() after summarising when you want ordinary behavior again.
- How do I avoid wrong totals after a join?
- Check that the key is unique in the table you are joining in, using count on the key column with a filter for counts above one. Compare row counts before and after the join, and investigate any increase you did not expect.
Summary
dplyr gives you a vocabulary for data manipulation: filter and select to narrow, mutate and arrange to derive and sort, group_by with summarise to aggregate, joins to combine tables, and tidyr's pivots to reshape. Practice by chaining a few of them on a dataset you care about. In the next lesson you will turn these tidy tables into charts using ggplot2.
Related tutorials
- Schedule and Debug Shell Scripts: cron, bash -x, and Portability
- Shell Scripting Project: A Backup Script with Rotation and a Log Report
- Writing Safe Bash Scripts: set -euo pipefail, getopts, and trap
- Shell Pipes, Redirection, grep, sed, and awk for Text Processing
- Bash Loops, Arrays, and Functions Explained
Comments
Log in or create a free account to join the discussion.