Last active
December 14, 2023 15:34
-
-
Save Henryjean/885108b96e5e556d9429e9484e072ddb to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
library(tidyverse) | |
library(nflplotR) | |
library(nflreadr) | |
library(grid) | |
library(ggtext) | |
# data loading and wrangling copied from: https://www.nflfastr.com/articles/beginners_guide.html#figures-with-qb-stats | |
# get pbp and filter to regular season rush and pass plays | |
pbp <- nflreadr::load_pbp(2022) %>% | |
dplyr::filter(season_type == "REG" & week == 1) %>% | |
dplyr::filter(!is.na(posteam) & (rush == 1 | pass == 1)) | |
# offense epa | |
offense <- pbp %>% | |
dplyr::group_by(team = posteam) %>% | |
dplyr::summarise(off_epa = mean(epa, na.rm = TRUE)) | |
# defense epa | |
defense <- pbp %>% | |
dplyr::group_by(team = defteam) %>% | |
dplyr::summarise(def_epa = mean(epa, na.rm = TRUE)) | |
# join offense and defense | |
df <- offense %>% | |
inner_join(defense, by = "team") | |
# get off_epa and def_epa max and min for four quadrants, hard coding this until figure out a way to do it more seamlessly | |
# round(max(c(abs(max(df$off_epa)), abs(min(df$off_epa)), abs(max(df$def_epa)), abs(min(df$def_epa)))) / .05) * .05 | |
off_epa_min <- -.45 | |
off_epa_max <- .45 | |
def_epa_min <- -.45 | |
def_epa_max <- .45 | |
# set rotation to 45 degrees | |
rotation <- 45 | |
# make plot | |
p <- df %>% | |
ggplot(aes(x = off_epa, y = def_epa)) + | |
# add color blocking | |
annotate("rect", xmin = (off_epa_max + off_epa_min) / 2, xmax = off_epa_max, | |
ymin = def_epa_min, ymax = (def_epa_max + def_epa_min) / 2, fill= "#488f31", alpha = .15, color = 'transparent') + | |
annotate("rect", xmin = off_epa_min, xmax = (off_epa_max + off_epa_min) / 2, | |
ymin = (def_epa_max + def_epa_min) / 2, ymax = def_epa_max, fill= "#de425b", alpha = .15, color = 'transparent') + | |
annotate("rect", xmin = (off_epa_max + off_epa_min) / 2, xmax = off_epa_max, | |
ymin = (def_epa_max + def_epa_min) / 2, ymax = def_epa_max, fill= "#E69F00", alpha = .15, color = 'transparent') + | |
annotate("rect", xmin = off_epa_min, xmax = (off_epa_max + off_epa_min) / 2, | |
ymin = def_epa_min, ymax = (def_epa_max + def_epa_min) / 2, fill= "#E69F00", alpha = .15, color = 'transparent') + | |
# add team logos | |
geom_nfl_logos(aes(team_abbr = team), width = 0.08, alpha = 0.75, angle = -1*rotation) + | |
scale_y_reverse(limits = c(.45, -.45), breaks = seq(-.45, .45, .15), labels = scales::number_format(accuracy = 0.01)) + | |
scale_x_continuous(limits = c(-.45, .45), breaks = seq(.45, -.45, -.15), labels = scales::number_format(accuracy = 0.01)) + | |
coord_equal() + | |
# add axis labels | |
labs( | |
x = "Offense EPA/play", | |
y = "Defense EPA/play" | |
) + | |
# thematic stuff | |
theme_minimal(base_family = 'Roboto') + | |
theme(axis.text.x = element_text(angle=(-1 * rotation), hjust = 0.5, margin = margin(t = -5)), | |
axis.text.y = element_text(angle=(-1 * rotation), hjust = 0.5, margin = margin(r = -5)), | |
axis.title.x = element_text(size = 12, | |
#angle=(-1 * rotation + 45), | |
vjust = 0.5, | |
margin = margin(t = 10), | |
face = 'bold', | |
color = "black"), | |
axis.title.y = element_text(size = 12, | |
angle=(-1 * rotation - 45), | |
hjust = 0.5, | |
margin = margin(r = 10), | |
color = "black", | |
face = 'bold'), | |
plot.margin = margin(1, .5, .5, 0, unit = 'in'), | |
panel.grid.minor = element_blank(), | |
plot.background = element_rect(fill = 'floralwhite', color = "floralwhite")) + | |
# hack together a title and subtitle | |
annotate(geom = 'text', x = .45, y = -.45, label = "2022 NFL Offensive and Defensive EPA per Play", angle = -1 * rotation, vjust = -2.5, fontface = 'bold', size = 4, family = 'Roboto') + | |
annotate(geom = 'text', x = .45, y = -.45, label = "Data: @nflfastR | Chart: @owenlhjphillips", angle = -1 * rotation, vjust = -1.5, size = 3, family = 'Roboto') + | |
# hack together a few chart guides (ie, 'Good D, Bad D') | |
geom_richtext(aes(x = .375, y = .375, label = "Good O, Bad D"), angle = -1 * rotation, size = 3, family = 'Roboto', fontface = 'bold', color = 'black', fill = "#f5d4ab", label.size = 0) + | |
geom_richtext(aes(x = -.375, y = -.375, label = "Bad O, Good D"), angle = -1 * rotation, size = 3, family = 'Roboto', fontface = 'bold', color = 'black', fill = "#f5d4ab", label.size = 0) + | |
geom_richtext(aes(x = -.375, y = .375, label = "Bad O, Bad D"), angle = -1 * rotation, size = 3, family = 'Roboto', fontface = 'bold', color = 'black', fill = "#f0b8b8", label.size = 0) + | |
geom_richtext(aes(x = .375, y = -.375, label = "Good O, Good D"), angle = -1 * rotation, size = 3, family = 'Roboto', fontface = 'bold', color = 'black', fill = "#aecdc2", label.size = 0) | |
# save plot | |
png("epa_diamond_plot.png", res = 300, width = 6, height = 6, units = 'in', bg = 'floralwhite') | |
print(p, vp=viewport(angle=rotation, | |
width = unit(6, "in"), | |
height = unit(6, "in"))) | |
dev.off() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Absolutely love this visualization.