# If you haven't installed dplyr yet, uncomment the line below:
# install.packages("dplyr")
library(dplyr)Lab 9: Data Wrangling with dplyr
BI 255 · Bethune-Cookman University · Fall 2026
Wrangling NFL Data with dplyr
Every real dataset needs to be cleaned, reorganized, and summarized before you can analyze it. dplyr (pronounced “dee-ply-er”) is the most popular R package for this and once you learn its six main verbs, you’ll use them in virtually every analysis you ever do.
Today we’ll learn dplyr using NFL player stats, which come in exactly the kind of messy, multi-column format that real biological data does too.
By the end of this lab you will be able to:
- Install and load packages using
install.packages()andlibrary() - Use the pipe operator
|>to chain operations - Filter rows with
filter() - Select columns with
select() - Create new columns with
mutate() - Sort data with
arrange() - Summarize data with
summarize()andgroup_by()
Part 1: Installing and Loading Packages
R comes with a lot built in, but much of its power comes from packages — collections of extra functions written by the R community.
Install = download to your computer (do this ONCE):
install.packages("dplyr")Load = activate for use in this session (do this EVERY time):
library(dplyr)Think of installing like buying a book, and loading like opening it to read.
Part 2: Our Dataset — 2025 NFL Season Stats
# NFL skill player stats for the 2025-26 season (simulated)
nfl <- data.frame(
player = c("Patrick Mahomes", "Lamar Jackson", "Jalen Hurts",
"Josh Allen", "Justin Herbert", "Dak Prescott",
"Brock Purdy", "Tua Tagovailoa", "CJ Stroud", "Joe Burrow",
"Tyreek Hill", "Justin Jefferson", "CeeDee Lamb",
"Stefon Diggs", "Davante Adams", "DK Metcalf",
"Cooper Kupp", "Ja'Marr Chase", "Travis Kelce",
"Mark Andrews", "Darren Waller", "Sam LaPorta",
"Derrick Henry", "Christian McCaffrey", "Josh Jacobs",
"Tony Pollard", "Saquon Barkley", "Breece Hall",
"Isiah Pacheco", "Jonathan Taylor"),
position = c(rep("QB", 10), rep("WR", 8), rep("TE", 4), rep("RB", 8)),
team = c("KC", "BAL", "PHI", "BUF", "LAC", "DAL", "SF", "MIA", "HOU", "CIN",
"MIA", "MIN", "DAL", "BUF", "GB", "SEA", "LAR", "CIN",
"KC", "BAL", "LV", "DET",
"TEN", "SF", "GB", "DAL", "PHI", "NYJ", "KC", "IND"),
conference = c(rep("AFC", 1), rep("AFC", 1), rep("NFC", 1), rep("AFC", 1),
rep("AFC", 1), rep("NFC", 1), rep("NFC", 1), rep("AFC", 1),
rep("AFC", 1), rep("AFC", 1),
rep("AFC", 1), rep("NFC", 1), rep("NFC", 1), rep("AFC", 1),
rep("NFC", 1), rep("NFC", 1), rep("NFC", 1), rep("AFC", 1),
rep("AFC", 1), rep("AFC", 1), rep("AFC", 1), rep("NFC", 1),
rep("AFC", 1), rep("NFC", 1), rep("NFC", 1), rep("NFC", 1),
rep("NFC", 1), rep("AFC", 1), rep("AFC", 1), rep("AFC", 1)),
games = c(16, 17, 15, 17, 16, 14, 17, 15, 17, 13,
17, 17, 17, 14, 16, 17, 15, 17,
17, 16, 12, 17,
17, 14, 17, 17, 17, 16, 17, 11),
yards = c(4839, 3678, 3858, 4306, 3764, 3247, 4280, 3561, 4108, 2987,
1481, 1529, 1749, 1095, 1144, 1249, 1002, 1455,
984, 847, 762, 1130,
1458, 1459, 1166, 1024, 1499, 1187, 872, 897),
touchdowns = c(38, 24, 31, 40, 26, 22, 28, 24, 33, 19,
13, 10, 17, 9, 12, 11, 8, 14,
8, 9, 6, 10,
12, 11, 8, 7, 13, 9, 6, 7),
pro_bowl = c(TRUE, TRUE, TRUE, TRUE, FALSE, TRUE, FALSE, FALSE, TRUE, TRUE,
TRUE, TRUE, TRUE, FALSE, FALSE, FALSE, FALSE, TRUE,
TRUE, TRUE, FALSE, FALSE,
FALSE, TRUE, FALSE, FALSE, TRUE, FALSE, FALSE, FALSE)
)Let’s get a quick look at what we’re working with:
dim(nfl)[1] 30 8
head(nfl) player position team conference games yards touchdowns pro_bowl
1 Patrick Mahomes QB KC AFC 16 4839 38 TRUE
2 Lamar Jackson QB BAL AFC 17 3678 24 TRUE
3 Jalen Hurts QB PHI NFC 15 3858 31 TRUE
4 Josh Allen QB BUF AFC 17 4306 40 TRUE
5 Justin Herbert QB LAC AFC 16 3764 26 FALSE
6 Dak Prescott QB DAL NFC 14 3247 22 TRUE
Part 3: The Pipe Operator |>
Before learning the dplyr verbs, we need to understand the pipe |>. It takes the output of one function and passes it as the first argument to the next function.
# Without pipe (hard to read, right?)
head(filter(nfl, position == "QB"), 3) player position team conference games yards touchdowns pro_bowl
1 Patrick Mahomes QB KC AFC 16 4839 38 TRUE
2 Lamar Jackson QB BAL AFC 17 3678 24 TRUE
3 Jalen Hurts QB PHI NFC 15 3858 31 TRUE
# With pipe (reads left-to-right like a sentence)
nfl |> filter(position == "QB") |> head(3) player position team conference games yards touchdowns pro_bowl
1 Patrick Mahomes QB KC AFC 16 4839 38 TRUE
2 Lamar Jackson QB BAL AFC 17 3678 24 TRUE
3 Jalen Hurts QB PHI NFC 15 3858 31 TRUE
nfl |> filter(position == "QB") |> head(3)
Read this as: “Take nfl, THEN filter to QBs only, THEN show the first 3 rows.”
The pipe makes your code read like a recipe, and you’ll use it in almost every data analysis going forward.
Keyboard shortcut: Ctrl+Shift+M (Windows) / Cmd+Shift+M (MacOS)
Part 4: filter() — Selecting Rows
filter() keeps only the rows that match your conditions.
# All Pro Bowl players
nfl |> filter(pro_bowl == TRUE) player position team conference games yards touchdowns pro_bowl
1 Patrick Mahomes QB KC AFC 16 4839 38 TRUE
2 Lamar Jackson QB BAL AFC 17 3678 24 TRUE
3 Jalen Hurts QB PHI NFC 15 3858 31 TRUE
4 Josh Allen QB BUF AFC 17 4306 40 TRUE
5 Dak Prescott QB DAL NFC 14 3247 22 TRUE
6 CJ Stroud QB HOU AFC 17 4108 33 TRUE
7 Joe Burrow QB CIN AFC 13 2987 19 TRUE
8 Tyreek Hill WR MIA AFC 17 1481 13 TRUE
9 Justin Jefferson WR MIN NFC 17 1529 10 TRUE
10 CeeDee Lamb WR DAL NFC 17 1749 17 TRUE
11 Ja'Marr Chase WR CIN AFC 17 1455 14 TRUE
12 Travis Kelce TE KC AFC 17 984 8 TRUE
13 Mark Andrews TE BAL AFC 16 847 9 TRUE
14 Christian McCaffrey RB SF NFC 14 1459 11 TRUE
15 Saquon Barkley RB PHI NFC 17 1499 13 TRUE
# All receivers (WR and TE) with more than 1,000 yards
nfl |> filter(position %in% c("WR", "TE"), yards > 1000) player position team conference games yards touchdowns pro_bowl
1 Tyreek Hill WR MIA AFC 17 1481 13 TRUE
2 Justin Jefferson WR MIN NFC 17 1529 10 TRUE
3 CeeDee Lamb WR DAL NFC 17 1749 17 TRUE
4 Stefon Diggs WR BUF AFC 14 1095 9 FALSE
5 Davante Adams WR GB NFC 16 1144 12 FALSE
6 DK Metcalf WR SEA NFC 17 1249 11 FALSE
7 Cooper Kupp WR LAR NFC 15 1002 8 FALSE
8 Ja'Marr Chase WR CIN AFC 17 1455 14 TRUE
9 Sam LaPorta TE DET NFC 17 1130 10 FALSE
# AFC players with 10 or more touchdowns
nfl |> filter(conference == "AFC", touchdowns >= 10) player position team conference games yards touchdowns pro_bowl
1 Patrick Mahomes QB KC AFC 16 4839 38 TRUE
2 Lamar Jackson QB BAL AFC 17 3678 24 TRUE
3 Josh Allen QB BUF AFC 17 4306 40 TRUE
4 Justin Herbert QB LAC AFC 16 3764 26 FALSE
5 Tua Tagovailoa QB MIA AFC 15 3561 24 FALSE
6 CJ Stroud QB HOU AFC 17 4108 33 TRUE
7 Joe Burrow QB CIN AFC 13 2987 19 TRUE
8 Tyreek Hill WR MIA AFC 17 1481 13 TRUE
9 Ja'Marr Chase WR CIN AFC 17 1455 14 TRUE
10 Derrick Henry RB TEN AFC 17 1458 12 FALSE
filter() Operators
| Operator | Meaning | Example |
|---|---|---|
== |
Equal to | position == "QB" |
!= |
Not equal to | team != "KC" |
>, <, >=, <= |
Comparisons | yards > 1000 |
& |
AND (both must be true) | yards > 1000 & touchdowns > 10 |
\| |
OR (at least one true) | position == "WR" \| position == "TE" |
%in% |
Matches any in a list | team %in% c("KC", "PHI", "SF") |
is.na() |
Is missing | is.na(yards) |
Part 5: select() — Choosing Columns
select() lets you keep only the columns you need:
# Just player name, position, and touchdowns
nfl |> select(player, position, touchdowns) player position touchdowns
1 Patrick Mahomes QB 38
2 Lamar Jackson QB 24
3 Jalen Hurts QB 31
4 Josh Allen QB 40
5 Justin Herbert QB 26
6 Dak Prescott QB 22
7 Brock Purdy QB 28
8 Tua Tagovailoa QB 24
9 CJ Stroud QB 33
10 Joe Burrow QB 19
11 Tyreek Hill WR 13
12 Justin Jefferson WR 10
13 CeeDee Lamb WR 17
14 Stefon Diggs WR 9
15 Davante Adams WR 12
16 DK Metcalf WR 11
17 Cooper Kupp WR 8
18 Ja'Marr Chase WR 14
19 Travis Kelce TE 8
20 Mark Andrews TE 9
21 Darren Waller TE 6
22 Sam LaPorta TE 10
23 Derrick Henry RB 12
24 Christian McCaffrey RB 11
25 Josh Jacobs RB 8
26 Tony Pollard RB 7
27 Saquon Barkley RB 13
28 Breece Hall RB 9
29 Isiah Pacheco RB 6
30 Jonathan Taylor RB 7
# Remove a column using -
nfl |> select(-pro_bowl) |> head(4) player position team conference games yards touchdowns
1 Patrick Mahomes QB KC AFC 16 4839 38
2 Lamar Jackson QB BAL AFC 17 3678 24
3 Jalen Hurts QB PHI NFC 15 3858 31
4 Josh Allen QB BUF AFC 17 4306 40
# Select a range of columns
nfl |> select(player:team) |> head(4) player position team
1 Patrick Mahomes QB KC
2 Lamar Jackson QB BAL
3 Jalen Hurts QB PHI
4 Josh Allen QB BUF
# Reorder columns and drop the rest
nfl |> select(player, team, position, yards, touchdowns) |> head(4) player team position yards touchdowns
1 Patrick Mahomes KC QB 4839 38
2 Lamar Jackson BAL QB 3678 24
3 Jalen Hurts PHI QB 3858 31
4 Josh Allen BUF QB 4306 40
Part 6: mutate() — Creating New Columns
mutate() adds new columns calculated from existing ones:
# Yards per game
nfl |>
mutate(yards_per_game = round(yards / games, 1)) |>
select(player, position, games, yards, yards_per_game) |>
head(8) player position games yards yards_per_game
1 Patrick Mahomes QB 16 4839 302.4
2 Lamar Jackson QB 17 3678 216.4
3 Jalen Hurts QB 15 3858 257.2
4 Josh Allen QB 17 4306 253.3
5 Justin Herbert QB 16 3764 235.2
6 Dak Prescott QB 14 3247 231.9
7 Brock Purdy QB 17 4280 251.8
8 Tua Tagovailoa QB 15 3561 237.4
# Touchdowns per game AND a performance tier
nfl |>
mutate(
td_per_game = round(touchdowns / games, 2),
performance = case_when(
touchdowns >= 30 ~ "Elite",
touchdowns >= 15 ~ "Pro-Bowl Level",
touchdowns >= 8 ~ "Solid Starter",
TRUE ~ "Developing"
)
) |>
select(player, position, touchdowns, td_per_game, performance) |>
arrange(desc(touchdowns)) player position touchdowns td_per_game performance
1 Josh Allen QB 40 2.35 Elite
2 Patrick Mahomes QB 38 2.38 Elite
3 CJ Stroud QB 33 1.94 Elite
4 Jalen Hurts QB 31 2.07 Elite
5 Brock Purdy QB 28 1.65 Pro-Bowl Level
6 Justin Herbert QB 26 1.62 Pro-Bowl Level
7 Lamar Jackson QB 24 1.41 Pro-Bowl Level
8 Tua Tagovailoa QB 24 1.60 Pro-Bowl Level
9 Dak Prescott QB 22 1.57 Pro-Bowl Level
10 Joe Burrow QB 19 1.46 Pro-Bowl Level
11 CeeDee Lamb WR 17 1.00 Pro-Bowl Level
12 Ja'Marr Chase WR 14 0.82 Solid Starter
13 Tyreek Hill WR 13 0.76 Solid Starter
14 Saquon Barkley RB 13 0.76 Solid Starter
15 Davante Adams WR 12 0.75 Solid Starter
16 Derrick Henry RB 12 0.71 Solid Starter
17 DK Metcalf WR 11 0.65 Solid Starter
18 Christian McCaffrey RB 11 0.79 Solid Starter
19 Justin Jefferson WR 10 0.59 Solid Starter
20 Sam LaPorta TE 10 0.59 Solid Starter
21 Stefon Diggs WR 9 0.64 Solid Starter
22 Mark Andrews TE 9 0.56 Solid Starter
23 Breece Hall RB 9 0.56 Solid Starter
24 Cooper Kupp WR 8 0.53 Solid Starter
25 Travis Kelce TE 8 0.47 Solid Starter
26 Josh Jacobs RB 8 0.47 Solid Starter
27 Tony Pollard RB 7 0.41 Developing
28 Jonathan Taylor RB 7 0.64 Developing
29 Darren Waller TE 6 0.50 Developing
30 Isiah Pacheco RB 6 0.35 Developing
case_when() — The Multi-Condition ifelse()
case_when() is like a series of if-else statements. Each line is: condition ~ value_to_assign
The last line (TRUE ~ ...) is the “else” — catches everything that didn’t match.
Part 7: arrange() — Sorting
# Sort by touchdowns, highest first
nfl |>
select(player, position, touchdowns) |>
arrange(desc(touchdowns)) player position touchdowns
1 Josh Allen QB 40
2 Patrick Mahomes QB 38
3 CJ Stroud QB 33
4 Jalen Hurts QB 31
5 Brock Purdy QB 28
6 Justin Herbert QB 26
7 Lamar Jackson QB 24
8 Tua Tagovailoa QB 24
9 Dak Prescott QB 22
10 Joe Burrow QB 19
11 CeeDee Lamb WR 17
12 Ja'Marr Chase WR 14
13 Tyreek Hill WR 13
14 Saquon Barkley RB 13
15 Davante Adams WR 12
16 Derrick Henry RB 12
17 DK Metcalf WR 11
18 Christian McCaffrey RB 11
19 Justin Jefferson WR 10
20 Sam LaPorta TE 10
21 Stefon Diggs WR 9
22 Mark Andrews TE 9
23 Breece Hall RB 9
24 Cooper Kupp WR 8
25 Travis Kelce TE 8
26 Josh Jacobs RB 8
27 Tony Pollard RB 7
28 Jonathan Taylor RB 7
29 Darren Waller TE 6
30 Isiah Pacheco RB 6
# Sort alphabetically by team, then by yards within each team
nfl |>
select(player, team, yards) |>
arrange(team, desc(yards)) |>
head(10) player team yards
1 Lamar Jackson BAL 3678
2 Mark Andrews BAL 847
3 Josh Allen BUF 4306
4 Stefon Diggs BUF 1095
5 Joe Burrow CIN 2987
6 Ja'Marr Chase CIN 1455
7 Dak Prescott DAL 3247
8 CeeDee Lamb DAL 1749
9 Tony Pollard DAL 1024
10 Sam LaPorta DET 1130
Part 8: summarize() and group_by() — The Power Combo
This is the most important dplyr pattern. group_by() splits the data into groups, and summarize() collapses each group into summary statistics.
# Average yards and touchdowns by position
nfl |>
group_by(position) |>
summarize(
n_players = n(),
avg_yards = round(mean(yards), 0),
avg_tds = round(mean(touchdowns), 1),
total_tds = sum(touchdowns),
pro_bowl_pct = round(mean(pro_bowl) * 100, 1)
)# A tibble: 4 × 6
position n_players avg_yards avg_tds total_tds pro_bowl_pct
<chr> <int> <dbl> <dbl> <dbl> <dbl>
1 QB 10 3863 28.5 285 70
2 RB 8 1195 9.1 73 25
3 TE 4 931 8.2 33 50
4 WR 8 1338 11.8 94 50
# Compare AFC vs NFC
nfl |>
group_by(conference) |>
summarize(
players = n(),
avg_yards = round(mean(yards), 0),
avg_tds = round(mean(touchdowns), 1),
pro_bowlers = sum(pro_bowl)
)# A tibble: 2 × 5
conference players avg_yards avg_tds pro_bowlers
<chr> <int> <dbl> <dbl> <int>
1 AFC 17 2252 17.5 9
2 NFC 13 1872 14.5 6
# Top team by total touchdowns
nfl |>
group_by(team) |>
summarize(team_tds = sum(touchdowns)) |>
arrange(desc(team_tds)) |>
head(8)# A tibble: 8 × 2
team team_tds
<chr> <dbl>
1 KC 52
2 BUF 49
3 DAL 46
4 PHI 44
5 SF 39
6 MIA 37
7 BAL 33
8 CIN 33
summarize() Functions
| Function | What it does |
|---|---|
n() |
Count rows in group |
mean(x) |
Average |
sum(x) |
Total |
median(x) |
Middle value |
sd(x) |
Standard deviation |
min(x), max(x) |
Smallest / largest |
n_distinct(x) |
Count unique values |
Part 9: Chaining Everything Together
The real power of dplyr is chaining multiple verbs into a readable pipeline:
# Full analysis: Pro Bowl skill players in the AFC with 15+ TDs
# Show their yards per game, sorted by performance
nfl |>
filter(conference == "AFC",
pro_bowl == TRUE,
touchdowns >= 10) |>
mutate(ypg = round(yards / games, 1)) |>
select(player, team, position, touchdowns, ypg) |>
arrange(desc(touchdowns)) player team position touchdowns ypg
1 Josh Allen BUF QB 40 253.3
2 Patrick Mahomes KC QB 38 302.4
3 CJ Stroud HOU QB 33 241.6
4 Lamar Jackson BAL QB 24 216.4
5 Joe Burrow CIN QB 19 229.8
6 Ja'Marr Chase CIN WR 14 85.6
7 Tyreek Hill MIA WR 13 87.1
# Summary: Average stats for Pro Bowl vs Non-Pro Bowl players
nfl |>
group_by(pro_bowl) |>
summarize(
count = n(),
avg_yards = round(mean(yards)),
avg_tds = round(mean(touchdowns), 1),
avg_games = round(mean(games), 1)
)# A tibble: 2 × 5
pro_bowl count avg_yards avg_tds avg_games
<lgl> <int> <dbl> <dbl> <dbl>
1 FALSE 15 1639 12.2 15.6
2 TRUE 15 2535 20.1 16.1
# Visualization: Touchdowns by position group
pos_summary <- nfl |>
group_by(position) |>
summarize(avg_tds = mean(touchdowns))
barplot(
pos_summary$avg_tds,
names.arg = pos_summary$position,
col = c("#013369", "#D50A0A", "#1B912F", "#F0D60F"),
ylab = "Average Touchdowns",
main = "Average Touchdowns by Position\n2025–26 NFL Season",
border = "white"
)3-Minute Knowledge Check
Close your notes. Answer these on your own — you have 3 minutes. We’ll go through the answers together.
1. What does the pipe operator |> do?
a) Divides two numbers b) Passes the result of one function into the next c) Creates a new column d) Filters rows
2. Which dplyr verb would you use to keep only NFL players who scored more than 10 touchdowns?
a) `select()` b) `mutate()` c) `filter()` d) `arrange()`
3. You want to add a new column ypg (yards per game) to your data frame. Which verb do you use, and write the code?
4. What is the correct order for calculating average touchdowns per position?
summarize()thengroup_by()group_by()thensummarize()filter()thengroup_by()- Order doesn’t matter
5. Inside summarize(), which function counts the number of rows in each group?
count()length()n()tally()
1. b) Passes the result of one function into the next — The pipe |> takes whatever is on its left and feeds it as the first argument to the function on its right. It lets you chain steps together in a readable left-to-right sequence.
2. c) filter() — filter() selects rows based on conditions. Example: nfl |> filter(touchdowns > 10).
3. mutate() — nfl |> mutate(ypg = round(yards / games, 1)). mutate() creates or modifies columns using expressions based on existing columns.
4. b) group_by() then summarize() — You must tell R what groups to use before you calculate the summaries. group_by(position) |> summarize(avg_td = mean(touchdowns)).
5. c) n() — Inside summarize(), n() counts the number of rows in each group. Note: length() also works on a column but n() is the idiomatic dplyr approach.
Lab 9 Checklist
Before you leave, make sure you can:
Using the nfl dataset, find the top 5 players in yards per game (you’ll need to create this column with mutate()). Then filter to only those who played at least 15 games. Show their name, team, position, and yards per game. Which position dominates the top 5?
Before Next Class (Monday, Week 5)
- Monday Sep 14 (Lab 10): Frequency Data & Confidence Intervals for Proportions
- Read Wilke 2021 Ch. 1, 2, 5, 7 (links on Canvas)
- dplyr will be used in almost every lab from now on — get comfortable with the 6 verbs!
| Verb | What it does | Key argument |
|---|---|---|
filter() |
Keep rows matching conditions | Logical expressions |
select() |
Keep/drop columns | Column names |
mutate() |
Add/change columns | new_col = expression |
arrange() |
Sort rows | Column name(s), desc() |
summarize() |
Collapse to summary stats | stat = function(col) |
group_by() |
Split into groups before summarizing | Column to group by |