Visualizing NFL Kicker Accuracy Trends (1999-2024)

Yesterday I wrote a quick blog post discussing how the distribution of field goal attempts by distance has shifted quite a bit over the last twenty five years: teams are attempting fewer short field goals and more long field goals.

In this post, I’ll look to round out my analysis, focusing not on attempts, but the actual make rate of kicks by distance, and how that has evolved over time.

Visualizations

What we find when we slice the data by time is that kickers have NFL kickers have become more accurate at every level of accuracy since 1999. We find that kickers in 2024 are:

  • 12 ppt more accurate on kicks of 30-34 yards than 1999-2003
  • 10 ppt more accurate on kicks of 40-44 yards than 1999-2003
  • 20 ppt more accurate on kicks of 50-54 yards than 1999-2003
  • 40 ppt more accurate on kicks of 55-59 yards than 1999-2003

The huge improvements in the 50-59 yard range have meaningfully changed how the game is played, allowing teams to attempt more long kicks instead of punting.

I’ve made two charts to visualize this data: one showing field goal conversion probability per year cohort as a decaying line chart, and another summarizing the accuracy per cohort and distance as a heatmap.

Kick Conversion as a Function of Distance

Field Goal Make Rate Function of Distance over Time

As Tile Chart

Field Goal Make Rate Function of Distance over Time Heatmap

Code Reference

Play-by-Play Data Acquisition

library(tidyverse)
library(lubridate)
library(nflreadr)

# Initialize an empty data frame to store results
all_kicks <- tibble()

# Loop through each year from 1999 to 2024
for (year in 1999:2024) {
  # Load play-by-play data for the specific year
  pbp <- load_pbp(year)
  
  # Filter and process the data
  kicks <- pbp %>% 
    filter(!is.na(field_goal_result)) %>% 
    select(game_date, field_goal_result, kick_distance, kicker_player_name) %>% 
    mutate(year = year(ymd(game_date)))
  
  # Append the processed kicks data to the main dataset
  all_kicks <- bind_rows(all_kicks, kicks)
  
  # Print progress to monitor the loop
  print(paste("Processed year:", year))
}

Charting Code

Line Chart

all_kicks %>% 
  mutate(kick_distance = floor(kick_distance / 5) * 5) %>%
  mutate(kick_distance = ifelse(kick_distance > 60, 60, kick_distance)) %>% 
  group_by(year_range, kick_distance) %>% 
  summarize(
    fga = n(),
    fgm_pct = sum(fgm) / n()
  ) %>% 
  ggplot(aes(x = kick_distance, y = fgm_pct, group = year_range, color = year_range)) + 
    geom_line() + 
    geom_point(size = 0.8) + 
    scale_color_manual(
      values = c("#003f5c", "#444e86", "#955196", "#dd5182", "#ff6e54", "#ffa600"),
      name = "Years"
    ) + 
    scale_x_continuous(limits = c(20, 60), breaks = seq(20, 60, 5)) + 
    scale_y_continuous(labels = scales::percent_format(), breaks = pretty_breaks(n = 5)) + 
    coord_cartesian(clip = "off") +
    theme_minimal() + 
    theme(
      panel.grid.minor.x = element_blank(),
      plot.title = element_text(size = 20, face = "bold"),
      plot.subtitle = element_text(size = 12),
      plot.caption = element_text(colour = "grey60"),
      plot.title.position = "plot"
    ) + 
    labs(
      x = "\nField Goal Distance (Rounded Down to Nearest 5 Yards)",
      y = "% of Field Goals Attempts Made\n",
      title = "NFL Kicker Accuracy as a Function of\nDistance and Year",
      subtitle = "Kickers are making significantly more kicks at every distance than they did\n25 years ago, with the most pronounced improvement visible >50 yards.",
      caption = "conormclaughlin.net"
    )

Tile Chart

all_kicks %>% 
  mutate(kick_distance = floor(kick_distance / 5) * 5) %>%
  mutate(kick_distance = ifelse(kick_distance > 60, 60, kick_distance)) %>% 
  filter(kick_distance >= 20) %>% 
  group_by(year_range, rounded_year, kick_distance) %>% 
  summarize(
    fga = n(),
    fgm_pct = sum(fgm) / n()
  ) %>% 
  ggplot(aes(x = kick_distance, y = reorder(year_range, -rounded_year), fill = fgm_pct)) + 
    geom_tile() + 
    geom_text(
      size = 3,
      fontface = "bold",
      aes(
        label = scales::percent(fgm_pct, accuracy = 1), 
        colour = ifelse(fgm_pct < 0.4, "white", "black")
      )
    ) +
    scale_colour_identity() + # Use the specified colors without a legend
    scale_fill_viridis(option = "C", guide = FALSE) + 
    scale_x_continuous(breaks = seq(20, 60, 5)) + 
    theme_minimal() + 
    theme(
      panel.grid.minor.x = element_blank(),
      plot.title = element_text(size = 20, face = "bold"),
      plot.subtitle = element_text(size = 12),
      plot.caption = element_text(colour = "grey60"),
      plot.title.position = "plot"
    ) + 
    labs(
      x = "\nField Goal Distance (Rounded Down to Nearest 5 Yards)",
      y = "",
      title = "NFL Kicker Accuracy as a Function of\nDistance and Year",
      subtitle = "Kickers are making significantly more kicks at every distance than they did\n25 years ago, with the most pronounced improvement visible >50 yards.",
      caption = "conormclaughlin.net"
    )

Contents