Problem Set 4
Data visualization with nflfastR
We will now apply our newly acquired plotting skills to make some
visualizations working with data from nflfastR. If you have not
installed nflfastR yet, run
install.packages("nflfastR") once in the console. Then load
in our usual libraries, as well as nflfastR.
- Let’s load in the data for the 2024 season. This dataset contains play-by-play data for all games in the 2024 NFL season.
- We can see that there are more than 100 columns. Let’s tackle
specific questions one at a time. First, look at quarterback completions
by filtering for where
play_type == "pass"and then selecting the following columns in a new table calledpbp_pass.
pbp_pass <- pbp_2024 %>%
filter(play_type == "pass") %>%
select(play_type, pass_location, yards_gained, air_yards, yards_after_catch,
passer_player_name, complete_pass, incomplete_pass, cpoe)
head(pbp_pass)## ── nflverse play by play data ──────────────────────────────────────────────────
## ℹ Data updated: 2025-09-03 05:24:26 EDT
## # A tibble: 6 × 9
## play_type pass_location yards_gained air_yards yards_after_catch
## <chr> <chr> <dbl> <dbl> <dbl>
## 1 pass left 22 -3 25
## 2 pass middle 9 2 7
## 3 pass middle 8 6 2
## 4 pass right 0 12 NA
## 5 pass <NA> 0 NA NA
## 6 pass left 5 5 0
## # ℹ 4 more variables: passer_player_name <chr>, complete_pass <dbl>,
## # incomplete_pass <dbl>, cpoe <dbl>
nflfastR unfortunately does not include a position field to identify quarterbacks. To work around this, group by player and keep players with more than 150 pass attempts to remove players without sufficient play time. Hint: pass attempts will be the sum of entries in
complete_passandincomplete_pass.Let’s take a preliminary look at how location might affect pass outcomes. First, use
!is.na()withfilter()to filter out pass attempts without a location label. Then create a bar plot to visualize the frequency of attempts in each pass location.Let’s look at the distribution of yards gained based on pass location with some box plots. Use
geom_boxplot()to create a box plot ofyards_gainedbypass_location.Completion percentage over expected is a metric that adjusts for the difficulty of a QB’s throw based on throw timing, coverage, and location. Take a look at how
cpoediffers based on pass location alone withgeom_violin().Let’s now turn to aggregated pass statistics per quarterback. Group by
passer_player_nameand create a table calledpbp_groupedwith columnspasser_player_name,attempts,avg_yards_gained, andavg_cpoeusingreframe().First visualize how
attemptsandavg_yards_gainedare related by using a scatterplot.
Next, add an abline to the plot using
geom_abline()to see how linear the data may be. Use theslopeandinterceptarguments to set the slope to 0.005 and the intercept to 5. Set the linetype to dashed, and the color to black. This line was chosen by hand; in Lecture 6, you will learn how to fit regression models to the data mathematically to make a line of best fit.Now, add a third variable. Set the color to equal
avg_cpoeand usescale_color_distiller()to set the color scale.Finally, visualize the quarterbacks with the highest
avg_yards_gained. Usearrange()andslice_head()to obtain the top 10 quarterbacks, then generate a bar plot withgeom_col(). Make sure to either use 45 degree axis labels or flip the coordinates withcoord_flip()so that the QB names are visible.Let’s do one more small case study on explosive plays. First, start by filtering the data as below to get the appropriate columns.
pbp_explosive <- pbp_2024 %>%
filter(play_type %in% c("pass", "run"),
!is.na(yards_gained),
!is.na(defteam),
!is.na(down),
!is.na(yardline_100)) %>%
mutate(
explosive = case_when(
play_type == "run" & yards_gained >= 10 ~ 1,
play_type == "pass" & yards_gained >= 20 ~ 1,
TRUE ~ 0
)
) %>%
filter(explosive == 1) %>%
select(
game_id,
play_id,
defteam,
play_type,
ydstogo,
yardline_100,
yards_gained,
explosive
)
head(pbp_explosive)## ── nflverse play by play data ──────────────────────────────────────────────────
## ℹ Data updated: 2025-09-03 05:24:26 EDT
## # A tibble: 6 × 8
## game_id play_id defteam play_type ydstogo yardline_100 yards_gained explosive
## <chr> <dbl> <chr> <chr> <dbl> <dbl> <dbl> <dbl>
## 1 2024_01… 83 BUF pass 7 67 22 1
## 2 2024_01… 622 BUF pass 4 65 24 1
## 3 2024_01… 707 BUF run 10 28 11 1
## 4 2024_01… 902 ARI run 10 64 15 1
## 5 2024_01… 946 ARI pass 5 44 23 1
## 6 2024_01… 1269 BUF run 6 56 12 1
Visualize the distribution of explosive plays by defensive team. Use
geom_bar()to create a bar plot of the number of explosive plays bydefteam.Take a closer look at the worst defenses against explosive plays. Group by defensive team and play type, then use
reframe()to obtain the number of explosive plays in each category. Usearrange()andslice_head()to obtain the top 10 teams within each play type. Replot the bar graphs and facet by pass or run plays.Finally, visualize the relationship between explosive plays and yards to go with a heat map. First, run the following code below to obtain the needed columns for this plot.
pbp_explosive_rate <- pbp_2024 %>%
filter(play_type %in% c("pass", "run"),
!is.na(yards_gained),
!is.na(defteam),
!is.na(down),
!is.na(yardline_100)) %>%
mutate(
explosive = case_when(
play_type == "run" & yards_gained >= 10 ~ 1,
play_type == "pass" & yards_gained >= 20 ~ 1,
TRUE ~ 0
)
) %>%
select(
game_id,
play_id,
defteam,
play_type,
ydstogo,
yardline_100,
yards_gained,
explosive
)Then, group by defteam and ydstogo and
obtain the explosive play rate, instead of number of explosive
plays. Then use geom_tile() to create a heat map with
defteam on the x-axis, ydstogo on the y-axis,
and fill based on the explosive play rate.