Which teams have the most (and least) efficient offenses so far in the 2025 NFL season? Buffalo holds the lead, followed by Green Bay. Tennessee is dead last in the league, just behind Cleveland. Here’s a breakdown based on the latest stats from NFLFastR:
Expected Points Added (EPA) is a metric in football that measures the impact of a play on a team’s expected points. It compares the expected points before a play to the expected points after the play, quantifying how much a play increases or decreases the team’s chance of scoring. Positive EPA indicates a successful play that improves scoring probability, while negative EPA indicates a play that reduces it. EPA is widely used to evaluate players, teams, and play decisions because it accounts for down, distance, field position, and game context.
Learn to create and publish analyses like this one by signing up for the Spring 2026 edition of JOUR 3841 Data Skills for Media Professionals with MTSU SoJSM professor Dr. Ken Blake.
R code:
Here is the R code that produced the graphic:
# ============================================================
# 0. INSTALL AND LOAD REQUIRED PACKAGES
# ============================================================
if (!require("nflfastR"))
install.packages("nflfastR")
if (!require("tidyverse"))
install.packages("tidyverse")
if (!require("plotly"))
install.packages("plotly")
library(nflfastR)
library(tidyverse)
library(plotly)
# ============================================================
# 1. LOAD PLAY-BY-PLAY DATA FOR THE 2025 SEASON SO FAR
# ============================================================
pbp_2025 <- load_pbp(2025) %>%
filter(season_type == "REG") # keep only regular season games
# ============================================================
# 2. FILTER TO OFFENSIVE RUN + PASS PLAYS WITH VALID EPA
# ============================================================
offense_plays_2025 <- pbp_2025 %>%
filter(!is.na(epa), play_type %in% c("run", "pass"))
# ============================================================
# 3. CALCULATE EPA PER PLAY BY OFFENSIVE TEAM
# ============================================================
team_epa_2025 <- offense_plays_2025 %>%
group_by(posteam) %>%
summarize(
plays = n(),
total_epa = sum(epa, na.rm = TRUE),
epa_per_play = mean(epa, na.rm = TRUE)
) %>%
arrange(desc(epa_per_play))
# ============================================================
# 4. CREATE CUSTOM HOVER TEXT
# ============================================================
team_epa_2025 <- team_epa_2025 %>%
mutate(
hovertxt = paste0(
"<b>",
posteam,
"</b><br>",
"EPA/play: ",
round(epa_per_play, 3),
"<br>",
"Plays: ",
plays,
"<br>",
"Total EPA: ",
round(total_epa, 1)
)
)
# ============================================================
# 5. PLOTLY HORIZONTAL BAR CHART (NEUTRAL GRAY BARS)
# ============================================================
Plot <- plot_ly(
team_epa_2025,
x = ~ epa_per_play,
y = ~ reorder(posteam, epa_per_play),
type = "bar",
orientation = "h",
text = ~ hovertxt,
hoverinfo = "text",
marker = list(color = "royalblue")
) %>%
layout(
xaxis = list(title = "EPA per Play (as of 11/19/2025)"),
yaxis = list(title = "Team")
)
Plot