Defining the “Best” Kicker

When evaluating the best kickers in the NFL, looking only at raw field goal percentage can be misleading. A kicker who attempts mostly short extra points will naturally post a higher percentage than one who is trusted with 55-yard field goals, even if the second kicker is more valuable to his team. To account for this, I defined “best” using a Distance-Adjusted Accuracy Score (DA-Score).

The DA-Score multiplies a kicker’s raw accuracy by the ratio of his average made-kick distance to the league-wide average made distance (36.7 yards). This rewards kickers who stay accurate while making longer, more difficult kicks. For example, Justin Tucker leads the group in raw accuracy (95.2%), but Mason Crosby ranks first by DA-Score because his average make stretches to 38.6 yards. That gap shows why distance has to count just as much as consistency when judging who the best kickers really are.

Data Import and Sanity Checks

The code below imports the four 2022 NFL Big Data Bowl CSVs and merges them into one dataframe. The sanity checks replicate the instructor’s reference counts (Extra Point: 3488, Field Goal: 2657, Kickoff: 7843, Punt: 5991) to confirm the data loaded correctly.

# Import datasets (CSVs must be in your RStudio working directory)
games   <- read_csv("games.csv", show_col_types = FALSE)
plays   <- read_csv("plays.csv", show_col_types = FALSE)
players <- read_csv("players.csv", show_col_types = FALSE)
pff     <- read_csv("PFFScoutingData.csv", show_col_types = FALSE)

# Merge into a single dataframe
kick_df <- plays %>%
  left_join(games, by = "gameId") %>%
  left_join(pff, by = c("gameId", "playId")) %>%
  left_join(players, by = c("kickerId" = "nflId"))
# Confirm play-type counts match the instructor's reference numbers
table(kick_df$specialTeamsPlayType)
## 
## Extra Point  Field Goal     Kickoff        Punt 
##        3488        2657        7843        5991
table(kick_df$specialTeamsResult)
## 
##     Blocked Kick Attempt             Blocked Punt                   Downed 
##                       61                       39                      834 
##               Fair Catch        Kick Attempt Good     Kick Attempt No Good 
##                     1645                     5470                      585 
##    Kickoff Team Recovery                   Muffed Non-Special Teams Result 
##                       16                      214                      101 
##            Out of Bounds                   Return                Touchback 
##                      651                     5207                     5156

Analysis: The Distance-Adjusted Accuracy Score

Using dplyr, I filter to field goals and extra points, group by kicker and team, and keep multi-season kickers with at least 30 attempts. The DA-Score multiplies raw accuracy by the ratio of the kicker’s average made distance to the league-wide average made distance.

# League-wide average distance of made kicks (the DA-Score baseline)
league_avg_dist <- kick_df %>%
  filter(specialTeamsPlayType %in% c("Field Goal", "Extra Point"),
         specialTeamsResult == "Kick Attempt Good") %>%
  summarise(avg_dist = mean(kickLength, na.rm = TRUE)) %>%
  pull(avg_dist)

# Per-kicker summary with the custom DA-Score
top_kickers <- kick_df %>%
  filter(specialTeamsPlayType %in% c("Field Goal", "Extra Point")) %>%
  filter(specialTeamsResult %in% c("Kick Attempt Good", "Kick Attempt No Good",
                                   "Blocked Kick Attempt")) %>%
  mutate(
    is_made   = ifelse(specialTeamsResult == "Kick Attempt Good", 1, 0),
    is_missed = ifelse(specialTeamsResult != "Kick Attempt Good", 1, 0)
  ) %>%
  group_by(displayName, possessionTeam) %>%
  summarise(
    seasons        = n_distinct(season),
    total_attempts = n(),
    total_made     = sum(is_made),
    accuracy       = total_made / total_attempts,
    avg_made_dist   = mean(kickLength[is_made == 1], na.rm = TRUE),
    min_made_dist   = min(kickLength[is_made == 1], na.rm = TRUE),
    max_made_dist   = max(kickLength[is_made == 1], na.rm = TRUE),
    avg_missed_dist = mean(kickLength[is_missed == 1], na.rm = TRUE),
    min_missed_dist = min(kickLength[is_missed == 1], na.rm = TRUE),
    max_missed_dist = max(kickLength[is_missed == 1], na.rm = TRUE),
    .groups = "drop"
  ) %>%
  filter(seasons > 1, total_attempts >= 30) %>%
  mutate(DA_Score = accuracy * (avg_made_dist / league_avg_dist)) %>%
  arrange(desc(DA_Score)) %>%
  head(10)

top_kickers
## # A tibble: 10 × 13
##    displayName     possessionTeam seasons total_attempts total_made accuracy
##    <chr>           <chr>            <int>          <int>      <dbl>    <dbl>
##  1 Mason Crosby    GB                   3            201        187    0.930
##  2 Matt Bryant     ATL                  2             84         75    0.893
##  3 Justin Tucker   BAL                  3            231        220    0.952
##  4 Brandon McManus DEN                  3            166        150    0.904
##  5 Wil Lutz        NO                   3            232        221    0.953
##  6 Josh Lambo      JAX                  3            106         99    0.934
##  7 Jason Sanders   MIA                  3            173        159    0.919
##  8 Randy Bullock   CIN                  3            160        145    0.906
##  9 Jason Myers     SEA                  2            128        116    0.906
## 10 Dustin Hopkins  WAS                  3            153        136    0.889
## # ℹ 7 more variables: avg_made_dist <dbl>, min_made_dist <dbl>,
## #   max_made_dist <dbl>, avg_missed_dist <dbl>, min_missed_dist <dbl>,
## #   max_missed_dist <dbl>, DA_Score <dbl>

Why Distance Has to Count

Before ranking anyone, look at the problem with raw percentages: the longer the kick, the lower the make rate. Short kicks are nearly automatic; 50+ yarders are a different sport. Any “best kicker” ranking that ignores distance is really ranking kick selection, not kickers — which is why the DA-Score adjusts for it.

dist_bins <- kick_df %>%
  filter(specialTeamsPlayType %in% c("Field Goal", "Extra Point"),
         specialTeamsResult %in% c("Kick Attempt Good", "Kick Attempt No Good",
                                    "Blocked Kick Attempt"),
         !is.na(kickLength)) %>%
  mutate(dist_bin = case_when(
    kickLength < 30 ~ "<30 yds",
    kickLength < 40 ~ "30-39 yds",
    kickLength < 50 ~ "40-49 yds",
    TRUE ~ "50+ yds"
  ),
  dist_bin = factor(dist_bin,
                    levels = c("<30 yds", "30-39 yds", "40-49 yds", "50+ yds"))) %>%
  group_by(dist_bin) %>%
  summarise(make_rate = mean(specialTeamsResult == "Kick Attempt Good"),
            n = n(), .groups = "drop")

ggplot(dist_bins, aes(x = dist_bin, y = make_rate)) +
  geom_col(fill = "#2a9d8f") +
  geom_text(aes(label = paste0(round(make_rate * 100, 1), "%\n(n = ", n, ")")),
            vjust = -0.4, size = 4.5, fontface = "bold") +
  scale_y_continuous(labels = function(x) paste0(round(x * 100), "%"),
                     limits = c(0, 1.12)) +
  labs(title = "Make rate collapses as kicks get longer",
       subtitle = "2022 NFL Big Data Bowl",
       x = NULL, y = "Make rate") +
  theme_minimal(base_size = 13) +
  theme(plot.title = element_text(face = "bold"))

Visualizing the Best Kickers

Green segments show each kicker’s range of made-kick distances; red segments show the range of misses. The numbers inside the bars are the average make/miss distances.

kicker_labels <- paste(top_kickers$displayName, "-", top_kickers$possessionTeam)

ggplot(top_kickers, aes(y = reorder(kicker_labels, DA_Score))) +
  geom_segment(aes(x = min_missed_dist, xend = max_missed_dist,
                   yend = reorder(kicker_labels, DA_Score)),
               color = "#e63946", linewidth = 6, alpha = 0.8) +
  geom_segment(aes(x = min_made_dist, xend = max_made_dist,
                   yend = reorder(kicker_labels, DA_Score)),
               color = "#2a9d8f", linewidth = 6, alpha = 0.9) +
  geom_text(aes(x = avg_made_dist, label = round(avg_made_dist, 1)),
            color = "black", size = 3.5, fontface = "bold") +
  geom_text(aes(x = avg_missed_dist, label = round(avg_missed_dist, 1)),
            color = "black", size = 3.5, fontface = "bold") +
  theme_minimal(base_size = 14) +
  labs(
    title = "Top 10 NFL Kickers by Distance-Adjusted Accuracy",
    subtitle = "Green = range of made kicks | Red = range of missed kicks\nNumbers = average make/miss yardage",
    x = "Kick Distance (Yards)",
    y = NULL,
    caption = "Data: 2022 NFL Big Data Bowl"
  ) +
  theme(
    plot.title = element_text(face = "bold", size = 18, margin = margin(b = 10)),
    plot.subtitle = element_text(color = "grey40", size = 13, margin = margin(b = 20)),
    panel.grid.major.y = element_blank(),
    axis.text.y = element_text(face = "bold", size = 12),
    plot.margin = margin(20, 20, 20, 20)
  )

The Ranking in One Chart

The headline result in a single chart. DA-Score multiplies accuracy by a distance factor, so the kickers at the top are the ones who stayed accurate while taking on the longest kicks.

ggplot(top_kickers, aes(x = DA_Score,
                        y = reorder(paste(displayName, "-", possessionTeam),
                                    DA_Score))) +
  geom_col(fill = "#2a9d8f") +
  geom_text(aes(label = round(DA_Score, 3)), hjust = -0.15,
            size = 4, fontface = "bold") +
  scale_x_continuous(expand = expansion(mult = c(0, 0.18))) +
  labs(title = "Top 10 kickers by Distance-Adjusted Accuracy Score",
       subtitle = "Higher = more accurate from longer distance",
       x = "DA-Score", y = NULL) +
  theme_minimal(base_size = 13) +
  theme(plot.title = element_text(face = "bold"),
        panel.grid.major.y = element_blank())

Interactive: Accuracy vs. Distance

For an interactive view, the chart below plots each top-10 kicker’s accuracy against his average made distance. Hover over any dot for the full line: DA-Score, accuracy, distance, and attempts. It tells the same story as the chart above — the best kickers live in the top-right, accurate from deep.

# NOTE: run install.packages("plotly") once in the Console before knitting
library(plotly)

interactive_plot <- top_kickers %>%
  mutate(
    hover_label = paste0(
      "<b>", displayName, " (", possessionTeam, ")</b><br>",
      "DA-Score: ", round(DA_Score, 3), "<br>",
      "Accuracy: ", round(accuracy * 100, 1), "%<br>",
      "Avg made distance: ", round(avg_made_dist, 1), " yds<br>",
      "Attempts: ", total_attempts
    )
  ) %>%
  plot_ly(
    x = ~avg_made_dist, y = ~accuracy,
    text = ~hover_label, hoverinfo = "text",
    type = "scatter", mode = "markers",
    marker = list(size = 16, color = ~DA_Score, colorscale = "Blues",
                  showscale = TRUE,
                  colorbar = list(title = "DA-Score"))
  ) %>%
  plotly::layout(
    title = "Top 10 Kickers: Accuracy vs. Distance (hover a dot)",
    xaxis = list(title = "Average made-kick distance (yards)"),
    yaxis = list(title = "Accuracy", tickformat = ".0%")
  )

interactive_plot

Bonus: Does Weather Actually Matter?

As a bonus check, I used an optional weather-enriched kick dataset (2018–2020 seasons) to test something every football fan argues about: does bad weather actually hurt kickers? I looked at 1,844 outdoor field goals. Indoor kicks were excluded because their temperature is recorded as a flat 70°F, which would fake the comparison.

# Optional bonus data: 2018-2020 weather-enriched kicks (kick_model_data.csv
# must be in your RStudio working directory, alongside the other CSVs)
wx <- read_csv("kick_model_data.csv", show_col_types = FALSE) %>%
  filter(indoor == 0, is_xp == 0)   # outdoor field goals only

wx <- wx %>%
  mutate(temp_bucket = case_when(
    temp < 32 ~ "Freezing (<32F)",
    temp < 50 ~ "Cold (32-50F)",
    TRUE      ~ "Mild (50F+)"
  )) %>%
  mutate(temp_bucket = factor(temp_bucket,
                              levels = c("Freezing (<32F)",
                                         "Cold (32-50F)",
                                         "Mild (50F+)")))

wx_summary <- wx %>%
  group_by(temp_bucket) %>%
  summarise(make_rate = mean(made), n = n(), .groups = "drop") %>%
  mutate(se = sqrt(make_rate * (1 - make_rate) / n))

The result is surprising: make rates are basically flat across temperatures. Kickers made 87.9% in freezing weather, 85.6% in the cold, and 84.6% in mild weather. Crosswind tells the same story: 84.5% when calm, 86.1% when breezy, 86.9% when windy. If bad weather hurt kickers, those bars would slope down. They don’t.

ggplot(wx_summary, aes(x = temp_bucket, y = make_rate, fill = temp_bucket)) +
  geom_col() +
  geom_errorbar(aes(ymin = make_rate - 1.96 * se,
                    ymax = make_rate + 1.96 * se), width = 0.2) +
  geom_text(aes(y = make_rate + 1.96 * se + 0.025,
                label = paste0(round(make_rate * 100, 1), "% (n = ", n, ")")),
            color = "black", size = 4, fontface = "bold") +
  scale_y_continuous(labels = function(x) paste0(round(x * 100), "%"),
                     limits = c(0, 1)) +
  scale_fill_brewer(palette = "Blues") +
  labs(title = "Cold weather does not hurt NFL kickers",
       subtitle = "Outdoor field goals, 2018-2020 (n = 1,844)",
       x = NULL, y = "Field goal make rate") +
  theme_minimal(base_size = 13) +
  theme(legend.position = "none",
        plot.title = element_text(face = "bold"))

To make sure distance wasn’t hiding a weather effect (for example, if coaches only attempted short kicks in bad weather), I ran a logistic regression predicting makes from kick distance plus cold, crosswind, and rain. Distance is hugely significant: every extra 10 yards cuts the odds of a make to about one-third. Cold, crosswind, and rain are all statistically insignificant. The honest conclusion is that at the NFL level, kickers are so good that weather is noise, and distance is nearly everything — which is exactly why ranking kickers by the Distance-Adjusted Accuracy Score, instead of raw percentage, is the right call.

wx_model <- glm(made ~ D + cold + abs(crosswind) + rain,
                data = wx, family = binomial)
summary(wx_model)$coefficients[, c("Estimate", "Pr(>|z|)")]
##                    Estimate     Pr(>|z|)
## (Intercept)     6.480874814 8.044854e-59
## D              -0.113378914 6.406438e-40
## cold            0.006022111 6.258454e-01
## abs(crosswind) -0.005640422 7.572918e-01
## rain           -0.106370869 6.752105e-01

Bonus: Where Kicks Crossed the Goal Line

One more way to see what separates makes from misses: where the ball actually crossed the goal line. Using the goal-post technique from class — the uprights sit 18’6” apart, about 3.08 yards on each side of center — each dot below is one kick’s crossing point, colored by result. The picture is stark: almost everything between the uprights went in, and almost everything outside them missed. NFL kicking really is a game of inches.

# Crossing point of each kick at the goal line (e = yards from center)
cross <- wx %>% filter(!is.na(e), abs(e) < 12)

ggplot(cross, aes(x = e, y = 0,
                  color = factor(made, labels = c("Missed", "Made")))) +
  geom_hline(yintercept = 0, color = "grey75") +
  geom_vline(xintercept = c(-3.083, 3.083), linetype = "dashed", linewidth = 1) +
  geom_point(alpha = 0.35, size = 2,
             position = position_jitter(height = 0.3, width = 0)) +
  scale_color_manual(values = c("Missed" = "#e63946", "Made" = "#2a9d8f"),
                     name = NULL) +
  annotate("text", x = 0, y = 0.55, label = "Between the uprights",
           size = 4.5, color = "grey30") +
  coord_cartesian(ylim = c(-0.5, 0.8)) +
  labs(title = "Where kicks crossed the goal line",
       subtitle = "Each dot is one kick's crossing point (2018-2020 outdoor FGs); dashed lines are the uprights, 18'6\" apart",
       x = "Yards from center (+ = right of center)", y = NULL) +
  theme_minimal(base_size = 13) +
  theme(axis.text.y = element_blank(),
        axis.ticks.y = element_blank(),
        panel.grid.major.y = element_blank(),
        plot.title = element_text(face = "bold"))

Conclusion

So who are the best kickers in the NFL? By raw accuracy, Justin Tucker leads the group at 95.2%. But raw accuracy rewards kickers who attempt mostly short kicks. The Distance-Adjusted Accuracy Score fixes that by multiplying accuracy by a distance factor — and by that measure, Mason Crosby comes out on top, with his average make stretching to 38.6 yards against a league average of 36.7.

The rest of the analysis backs the metric up. Make rate collapses as kicks get longer, which is why distance has to count just as much as consistency. Weather, surprisingly, does not matter: across 1,844 outdoor field goals, cold, wind, and rain had no significant effect on makes once distance was accounted for. And the goal-line plot shows how fine the margins are — nearly everything between the uprights went in, and nearly everything outside them missed.

The takeaway: the best kicker is not the one with the prettiest percentage. It is the one who stays accurate when the kick gets long.