Lab 9: Data Wrangling with dplyr

BI 255 · Bethune-Cookman University · Fall 2026

Author

Dr. Rosie Stanbrook-Buyer

Published

September 11, 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.

NoteLearning Objectives

By the end of this lab you will be able to:

  1. Install and load packages using install.packages() and library()
  2. Use the pipe operator |> to chain operations
  3. Filter rows with filter()
  4. Select columns with select()
  5. Create new columns with mutate()
  6. Sort data with arrange()
  7. Summarize data with summarize() and group_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.

ImportantInstalling vs. Loading a Package

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.

# If you haven't installed dplyr yet, uncomment the line below:
# install.packages("dplyr")

library(dplyr)

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
TipReading the Pipe as “then”

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
NoteUseful 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
Tipcase_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
NoteUseful 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.

CautionKnowledge Check Questions

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?

  1. summarize() then group_by()
  2. group_by() then summarize()
  3. filter() then group_by()
  4. Order doesn’t matter

5. Inside summarize(), which function counts the number of rows in each group?

  1. count()
  2. length()
  3. n()
  4. 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:

TipBonus Challenge

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!
NoteThe 6 dplyr Verbs — Quick Reference
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