EPPS Math & Coding Camp

Data Manipulation
with the Tidyverse

dplyr, janitor, tidyr, and broom — cleaning, summarizing, and comparing real data.

JP AdjadehEPPS, UT Dallas
Attendance check-in QR code
Before You Start

What's the tidyverse?

A collection of R packages built around a shared, verb-based grammar for working with data. Where base R uses bracket indexing — data[row, column] — the tidyverse uses named functions like select(), filter(), and mutate(), chained together in a readable sequence.
Before You Start

The pipe: |>

The native pipe |> takes what's on its left and feeds it as the first argument to what's on its right. life_exp |> filter(...) reads as "take life_exp, then filter it."
You'll also see %>% in older tutorials and Stack Overflow answers — same idea, from the magrittr package rather than base R. We'll use |> today.
Package 1 of 4

dplyr

d
The core grammar of data manipulation. A small set of verbs — select(), filter(), arrange(), mutate(), summarize() — cover most of what indexing and subsetting did in base R.

Point and click

Packages pane (bottom right) → Install → type "dplyr" → Install

By code

p_load(dplyr)
dplyr — Task 1

select()

Now Using
life_exp
Your Task
Using the dplyr package and the pipe (|>), write R code to select only the Year, Gender, and Average_Life_Expectancy columns from life_exp.
Check Your Code

Naming the columns you want

life_exp |> select(Year, Gender, Average_Life_Expectancy)
dplyr — Task 1, Prompted

Now try the prompt

life_exp |> select(Year, Gender, Average_Life_Expectancy)
Now Prompt It
Now that you've seen this work, write an AI prompt that would get you to this same code. Compare what the AI gives you to what you already wrote.
dplyr — Task 2

filter()

Your Task
Using dplyr and the pipe (|>), write R code to filter life_exp to only rows where Gender is "Female."
Check Your Code

Keeping rows that match

life_exp |> filter(Gender == "Female")
dplyr — Task 2, Prompted

Now try the prompt

life_exp |> filter(Gender == "Female")
Now Prompt It
Now that you've seen this work, write an AI prompt that would get you to this same code. Compare what the AI gives you to what you already wrote.
dplyr — Task 3

arrange()

Your Task
Using dplyr and the pipe (|>), write R code to arrange life_exp by Average_Life_Expectancy from highest to lowest. Identify which year and gender had the highest value.
Check Your Code

Sorting with desc()

life_exp |> arrange(desc(Average_Life_Expectancy))
dplyr — Task 3, Prompted

Now try the prompt

life_exp |> arrange(desc(Average_Life_Expectancy))
Now Prompt It
Now that you've seen this work, write an AI prompt that would get you to this same code. Compare what the AI gives you to what you already wrote.
dplyr — Task 4

group_by() + summarize()

Your Task
Using dplyr and the pipe (|>), write R code to group life_exp by Gender and compute the average Average_Life_Expectancy for each group. This is your first real summary table — print it, and save it to your working directory as a CSV.
Check Your Code

Your first summary table

life_exp_summary <- life_exp |>
  group_by(Gender) |>
  summarize(mean_life_exp = mean(Average_Life_Expectancy, na.rm = TRUE))
print(life_exp_summary)
write.csv(life_exp_summary, "life_exp_summary.csv", row.names = FALSE)

group_by() + summarize() is the pattern behind almost every summary table you'll build from here on.

dplyr — Task 4, Prompted

Now try the prompt

life_exp_summary <- life_exp |>
  group_by(Gender) |>
  summarize(mean_life_exp = mean(Average_Life_Expectancy, na.rm = TRUE))
print(life_exp_summary)
write.csv(life_exp_summary, "life_exp_summary.csv", row.names = FALSE)
Now Prompt It
Now that you've seen this work, write an AI prompt that would get you to this same code. Compare what the AI gives you to what you already wrote.
Package 2 of 4

janitor

j
Practical cleaning utilities dplyr doesn't include. clean_names() converts inconsistent column names into a consistent format automatically. tabyl() gives quick frequency counts.

Point and click

Packages pane → Install → type "janitor" → Install

By code

p_load(janitor)
Now Using
air
janitor — Task 1

clean_names()

Your Task
Using the janitor package, write R code to clean up air's column names so they're consistent. Check names(air) before and after.
Check Your Code

Consistent names, automatically

names(air)
air <- air |> clean_names()
names(air)

Type_Name becomes type_name, Data.Value becomes data_value — every column now follows the same pattern.

janitor — Task 1, Prompted

Now try the prompt

names(air)
air <- air |> clean_names()
names(air)
Now Prompt It
Now that you've seen this work, write an AI prompt that would get you to this same code. Compare what the AI gives you to what you already wrote.
dplyr on air — Task 5

Chaining filter, group, and summarize

Your Task
Using dplyr and the pipe (|>), write R code to filter air to rows where type_name is "UHF42," then group by name and compute the average data_value for each. Print the result and save it to your working directory as a CSV.
Check Your Code

Three verbs, one pipeline

air_summary <- air |>
  filter(type_name == "UHF42") |>
  group_by(name) |>
  summarize(avg_value = mean(data_value))
print(air_summary)
write.csv(air_summary, "air_summary.csv", row.names = FALSE)
dplyr on air — Task 5, Prompted

Now try the prompt

air_summary <- air |>
  filter(type_name == "UHF42") |>
  group_by(name) |>
  summarize(avg_value = mean(data_value))
print(air_summary)
write.csv(air_summary, "air_summary.csv", row.names = FALSE)
Now Prompt It
Now that you've seen this work, write an AI prompt that would get you to this same code. Compare what the AI gives you to what you already wrote.
janitor — Task 2

tabyl()

Your Task
Using janitor and the pipe (|>), write R code to get a quick count of how many rows exist for each name in air — which one appears most? Print the result and save it to your working directory as a CSV.
Check Your Code

A one-line frequency table

name_counts <- air |> tabyl(name) |> arrange(desc(n))
print(name_counts)
write.csv(name_counts, "name_counts.csv", row.names = FALSE)
janitor — Task 2, Prompted

Now try the prompt

name_counts <- air |> tabyl(name) |> arrange(desc(n))
print(name_counts)
write.csv(name_counts, "name_counts.csv", row.names = FALSE)
Now Prompt It
Now that you've seen this work, write an AI prompt that would get you to this same code. Compare what the AI gives you to what you already wrote.
Debug It

Find the bug

air |> filter(Type_Name == "UHF42") |> nrow()
Console OutputError in `filter()`: i In argument: `Type_Name == "UHF42"`. Caused by error: ! object 'Type_Name' not found
Your Task
This should count how many UHF42 rows are in air, but it throws an error. Prompt an AI to diagnose the problem — then verify its explanation and its fix actually work before trusting either.
Check Your Fix

The column name changed earlier

air |> filter(type_name == "UHF42") |> nrow()
clean_names() ran earlier in this session and converted Type_Name to type_name. The error wasn't a dplyr problem — it was a reference to a column name that no longer exists. An AI can spot this instantly, but only checking the actual column names confirms it's right.
Package 3 of 4

tidyr

t
Handles reshaping and missing data. Two functions matter most today: drop_na() removes rows with missing values; replace_na() fills them with a specified value instead.

Point and click

Packages pane → Install → type "tidyr" → Install

By code

p_load(tidyr)
Now Using
ev
Before You Start

Drop it, or fill it in?

AI can write drop_na() or replace_na() code in seconds. It cannot tell you which one is right — that depends on why the value is missing and what your analysis needs. That judgment call is yours.
tidyr — Task 1

drop_na() vs. replace_na()

Your Task
Using tidyr and the pipe (|>), write R code to count how many rows in ev have a missing County value, then try both drop_na(County) and replace_na() with a placeholder, and compare the resulting row counts.
Check Your Code

Two different outcomes

sum(is.na(ev$County))

ev |> drop_na(County) |> nrow()
ev |> replace_na(list(County = "Unknown")) |> nrow()

drop_na() shrinks the dataset; replace_na() keeps every row but changes what's in it. Neither is automatically correct.

tidyr — Task 1, Prompted

Now try the prompt

sum(is.na(ev$County))

ev |> drop_na(County) |> nrow()
ev |> replace_na(list(County = "Unknown")) |> nrow()
Now Prompt It
Now that you've seen this work, write an AI prompt that would get you to this same code. Compare what the AI gives you to what you already wrote.
Capstone Table

Top manufacturers by count

Your Task
Using dplyr and the pipe (|>), write R code to create a Vehicle_Age column in ev, group by Make, compute the count and average Electric_Range for each manufacturer, and show the top 10 by count. Print the result and save it to your working directory as a CSV.
Check Your Code

A full pipeline, start to finish

top_makes <- ev |>
  mutate(Vehicle_Age = 2026 - Model_Year) |>
  group_by(Make) |>
  summarize(n = n(), avg_range = mean(Electric_Range, na.rm = TRUE)) |>
  arrange(desc(n)) |>
  head(10)
print(top_makes)
write.csv(top_makes, "top_makes.csv", row.names = FALSE)
Capstone Table, Prompted

Now try the prompt

top_makes <- ev |>
  mutate(Vehicle_Age = 2026 - Model_Year) |>
  group_by(Make) |>
  summarize(n = n(), avg_range = mean(Electric_Range, na.rm = TRUE)) |>
  arrange(desc(n)) |>
  head(10)
print(top_makes)
write.csv(top_makes, "top_makes.csv", row.names = FALSE)
Now Prompt It
Now that you've seen this work, write an AI prompt that would get you to this same code. Compare what the AI gives you to what you already wrote.
One More Thing

Polish the output

knitr::kable() turns any data frame into a clean, presentable table — the same one you'd drop into a report or paper.
top_makes |> knitr::kable()
Package 4 of 4

broom

b
Converts model objects into tidy data frames. tidy() turns a regression's coefficients into rows and columns you can filter, sort, and merge like any other data — instead of a wall of console text.

Point and click

Packages pane → Install → type "broom" → Install

By code

p_load(broom)
Now Using
life_exp
broom — Task 1

Fit three models

Your Task
Using base R's lm(), write R code to fit three linear models on life_exp: Model 1 predicts Average_Life_Expectancy from Year; Model 2 adds Gender; Model 3 adds Age_adjusted_Death_Rate. Look at summary() on each.
Check Your Code

Three nested models

model1 <- lm(Average_Life_Expectancy ~ Year, data = life_exp)
model2 <- lm(Average_Life_Expectancy ~ Year + Gender, data = life_exp)
model3 <- lm(Average_Life_Expectancy ~ Year + Gender + Age_adjusted_Death_Rate,
    data = life_exp)

summary() gives you a full report per model, but comparing three of these side by side means reading three separate walls of text.

broom — Task 1, Prompted

Now try the prompt

model1 <- lm(Average_Life_Expectancy ~ Year, data = life_exp)
model2 <- lm(Average_Life_Expectancy ~ Year + Gender, data = life_exp)
model3 <- lm(Average_Life_Expectancy ~ Year + Gender + Age_adjusted_Death_Rate,
    data = life_exp)
Now Prompt It
Now that you've seen this work, write an AI prompt that would get you to this same code. Compare what the AI gives you to what you already wrote.
broom — Task 2

Merge the three into one table

Your Task
Using broom's tidy() and dplyr's bind_rows(), write R code to convert each model into a data frame, add a column labeling which model each row came from, and combine all three into one. Print the result and save it to your working directory as a CSV.
Check Your Code

One table, three models

m1 <- tidy(model1) |> mutate(model = "Model 1")
m2 <- tidy(model2) |> mutate(model = "Model 2")
m3 <- tidy(model3) |> mutate(model = "Model 3")

all_models <- bind_rows(m1, m2, m3)
print(all_models)
write.csv(all_models, "model_comparison.csv", row.names = FALSE)
broom — Task 2, Prompted

Now try the prompt

m1 <- tidy(model1) |> mutate(model = "Model 1")
m2 <- tidy(model2) |> mutate(model = "Model 2")
m3 <- tidy(model3) |> mutate(model = "Model 3")

all_models <- bind_rows(m1, m2, m3)
print(all_models)
write.csv(all_models, "model_comparison.csv", row.names = FALSE)
Now Prompt It
Now that you've seen this work, write an AI prompt that would get you to this same code. Compare what the AI gives you to what you already wrote.
One More Thing

A publication-ready comparison table

all_models |> knitr::kable()

The same merge-and-label pattern works for any set of models you want to compare — not just these three.

Before We Wrap

Final challenge

Using any dataset from today, work through all four packages:

  1. 1
    Clean its column names with janitor
  2. 2
    Build a group_by/summarize table with dplyr
  3. 3
    Check it for missing values with tidyr
  4. 4
    Fit a simple model and turn it into a tidy table with broom

Write the code yourself first for each step, then check your approach with a prompt.

Live Share

Show us what you got

Volunteers walk us through the code, the result, and how the prompt compared once they tried it.

Recap

Four packages, one grammar