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.
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
##
## 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
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>
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"))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 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())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_plotAs 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
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"))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.