Skip to content

Instantly share code, notes, and snippets.

@Henryjean
Last active December 14, 2023 15:34
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Henryjean/885108b96e5e556d9429e9484e072ddb to your computer and use it in GitHub Desktop.
Save Henryjean/885108b96e5e556d9429e9484e072ddb to your computer and use it in GitHub Desktop.
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()
@NotebookOFXiaoMing
Copy link

Absolutely love this visualization.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment