Assignment Description

The purpose of this project is to investigate which NFL kickers were trusted to attempt longer field goals during the 2018–2020 seasons. Rather than examining accuracy alone, I wanted to compare the total distance of a kicker’s field goal attempts, average kick distance, number of attempts, and success rate.

The main research question is: Who were the best kickers based on the distance they were trusted to attempt?

To find the answer to this question, I summarized field goal attempts by kicker, visualized the relationship between total kick distance and the number of attempts, and created a heatmap showing successful and unsuccessful attempts across distance categories. I also develop a combined score using average kick distance and success rate to produce a Top 10 ranking to not clutter the heatmap with a bunch of random low ranking kickers.


Step 1. Load the CSV files

I use the NFL Big Data Bowl data from the 2018, 2019, and 2020 seasons. The plays.csv file contains information about field goal attempts, including kick distance and result. The players.csv file identifies the kickers, while games.csv provides season information.

library(data.table)
library(dplyr)
library(ggplot2)
library(scales)
library(knitr)
library(kableExtra)

# Set the folder containing the CSV files
setwd("~/Rstudio/nfl-big-data-bowl-2022")

# Load the data
plays <- fread("plays.csv")
players <- fread("players.csv")
games <- fread("games.csv")

# Join the data so each play includes the kicker's name and season
plays_df <- plays |>
  left_join(
    players |> select(nflId, displayName),
    by = c("kickerId" = "nflId")
  ) |>
  left_join(
    games |> select(gameId, season),
    by = "gameId"
  )

# Keep completed field goal attempts with recorded results
perf_summary <- plays_df |>
  filter(
    specialTeamsPlayType == "Field Goal",
    specialTeamsResult %in% c(
      "Kick Attempt Good",
      "Kick Attempt No Good"
    ),
    !is.na(kickLength),
    !is.na(displayName)
  ) |>
  data.frame()

# Check the data
head(perf_summary)
##       gameId playId
## 1 2018090600    658
## 2 2018090600   1368
## 3 2018090600   1587
## 4 2018090900   1226
## 5 2018090900   1613
## 6 2018090900   1973
##                                                                                                         playDescription
## 1                                      (5:03) M.Bryant 21 yard field goal is GOOD, Center-J.Overbaugh, Holder-M.Bosher.
## 2                                      (6:12) J.Elliott 26 yard field goal is GOOD, Center-R.Lovato, Holder-C.Johnston.
## 3                                      (2:13) M.Bryant 52 yard field goal is GOOD, Center-J.Overbaugh, Holder-M.Bosher.
## 4                      (13:18) (Field Goal formation) J.Tucker 41 yard field goal is GOOD, Center-M.Cox, Holder-S.Koch.
## 5 (8:48) (Field Goal formation) S.Hauschka 52 yard field goal is No Good, Short, Center-R.Ferguson, Holder-C.Bojorquez.
## 6                       (4:20) (Field Goal formation) J.Tucker 39 yard field goal is GOOD, Center-M.Cox, Holder-S.Koch.
##   quarter down yardsToGo possessionTeam specialTeamsPlayType
## 1       1    4         3            ATL           Field Goal
## 2       2    4         8            PHI           Field Goal
## 3       2    4        17            ATL           Field Goal
## 4       2    4        13            BAL           Field Goal
## 5       2    4         9            BUF           Field Goal
## 6       2    4         2            BAL           Field Goal
##     specialTeamsResult kickerId returnerId kickBlockerId yardlineSide
## 1    Kick Attempt Good    27091       <NA>            NA          PHI
## 2    Kick Attempt Good    44966       <NA>            NA          ATL
## 3    Kick Attempt Good    27091       <NA>            NA          PHI
## 4    Kick Attempt Good    39470       <NA>            NA          BUF
## 5 Kick Attempt No Good    33469       <NA>            NA          BAL
## 6    Kick Attempt Good    39470       <NA>            NA          BUF
##   yardlineNumber gameClock penaltyCodes penaltyJerseyNumbers penaltyYards
## 1              3  05:03:00         <NA>                 <NA>           NA
## 2              8  06:12:00         <NA>                 <NA>           NA
## 3             34  02:13:00         <NA>                 <NA>           NA
## 4             23  13:18:00         <NA>                 <NA>           NA
## 5             34  08:48:00         <NA>                 <NA>           NA
## 6             21  04:20:00         <NA>                 <NA>           NA
##   preSnapHomeScore preSnapVisitorScore passResult kickLength kickReturnYardage
## 1                0                   0       <NA>         21                NA
## 2                0                   3       <NA>         26                NA
## 3                3                   3       <NA>         52                NA
## 4               14                   0       <NA>         41                NA
## 5               17                   0       <NA>         52                NA
## 6               17                   0       <NA>         39                NA
##   playResult absoluteYardlineNumber      displayName season
## 1          0                     13      Matt Bryant   2018
## 2          0                     18     Jake Elliott   2018
## 3          0                     76      Matt Bryant   2018
## 4          0                     33    Justin Tucker   2018
## 5         -8                     76 Stephen Hauschka   2018
## 6          0                     31    Justin Tucker   2018

Step 2. Summarize performance by kicker

I calculate the number of field goal attempts, total kick distance, average kick distance, number of successful attempts, number of missed attempts, and success rate for each kicker.

To avoid ranking kickers with very few attempts, I include only kickers with at least 10 recorded field goal attempts.

# Calculate performance statistics for each kicker
kicker_summary <- perf_summary |>
  group_by(displayName) |>
  summarise(
    kicks = n(),
    total_kick_length = sum(kickLength, na.rm = TRUE),
    average_kick_length = mean(kickLength, na.rm = TRUE),
    made = sum(specialTeamsResult == "Kick Attempt Good"),
    missed = sum(specialTeamsResult == "Kick Attempt No Good"),
    success_rate = made / kicks,
    .groups = "drop"
  ) |>
  filter(kicks >= 10)

# Calculate standardized scores for average distance and success rate
kicker_summary <- kicker_summary |>
  mutate(
    distance_score = as.numeric(scale(average_kick_length)),
    success_score = as.numeric(scale(success_rate)),

    # Give equal weight to distance and success rate
    kicker_score = distance_score + success_score
  ) |>
  arrange(desc(kicker_score))

# Display the summary
kicker_summary
## # A tibble: 48 × 10
##    displayName          kicks total_kick_length average_kick_length  made missed
##    <chr>                <int>             <int>               <dbl> <int>  <int>
##  1 Graham Gano             45              1796                39.9    42      3
##  2 Brandon McManus         82              3344                40.8    72     10
##  3 Jason Myers             80              3149                39.4    73      7
##  4 Justin Tucker           92              3493                38.0    87      5
##  5 Matt Bryant             34              1406                41.4    28      6
##  6 Josh Lambo              57              2153                37.8    54      3
##  7 Mason Crosby            71              2803                39.5    63      8
##  8 Sebastian Janikowski    24               967                40.3    20      4
##  9 Dustin Hopkins          85              3321                39.1    73     12
## 10 Randy Bullock           77              3012                39.1    66     11
## # ℹ 38 more rows
## # ℹ 4 more variables: success_rate <dbl>, distance_score <dbl>,
## #   success_score <dbl>, kicker_score <dbl>

The combined score gives equal weight to average kick distance and success rate after standardizing both measures. A higher score indicates a combination of longer average attempts and a higher success rate. This is a custom scoring method for this project rather than an official NFL statistic.

Step 3. Compare total kick distance with number of attempts

Total kick distance can be influenced by how many opportunities a kicker receives. So I compare total kick distance with the number of attempts and also consider average kick distance and success rate in the subsequent analysis.

# Plot total kick distance against number of attempts
ggplot(
  kicker_summary,
  aes(
    x = kicks,
    y = total_kick_length
  )
) +
  geom_point(alpha = 0.7) +
  geom_smooth(method = "lm", se = FALSE) +
  labs(
    title = "Total Kick Distance vs. Number of Attempts",
    x = "Number of Field Goal Attempts",
    y = "Total Kick Distance (yards)"
  ) +
  theme_minimal() +
  theme(
    plot.title = element_text(hjust = 0.5)
  )

This scatterplot helps show whether kickers with more attempts also accumulate greater total kick distance. The number of attempts is important when interpreting total distance because a kicker with more opportunities may accumulate a larger total even if their average attempts are shorter.

Step 4. Examine field goal attempts by distance

I group field goal attempts into five distance categories. The heatmap displays the number of successful and unsuccessful attempts for each kicker across these categories. For readability, it focuses on the Top 10 kickers identified by the combined score.

# Create categories for field goal distances
perf_summary2 <- perf_summary |>
  mutate(
    kick_range = cut(
      kickLength,
      breaks = c(0, 30, 40, 50, 60, Inf),
      labels = c(
        "Under 30",
        "30-39",
        "40-49",
        "50-59",
        "60+"
      ),
      right = FALSE
    )
  ) |>
  filter(!is.na(kick_range))

# Get the names of the Top 10 kickers
top_10_kickers <- kicker_summary |>
  slice_head(n = 10)

# Keep only the Top 10 kickers for the heatmap
heatmap_data <- perf_summary2 |>
  filter(displayName %in% top_10_kickers$displayName) |>
  group_by(displayName, kick_range, specialTeamsResult) |>
  summarise(
    kicks = n(),
    .groups = "drop"
  )

# Create the heatmap
ggplot(
  heatmap_data,
  aes(
    x = kick_range,
    y = displayName,
    fill = kicks
  )
) +
  geom_tile(color = "white") +
  facet_wrap(~specialTeamsResult) +
  labs(
    title = "Top 10 Kickers: Attempts by Distance and Result",
    x = "Field Goal Distance (yards)",
    y = "Kicker",
    fill = "Number of Kicks"
  ) +
  theme_minimal() +
  theme(
    plot.title = element_text(hjust = 0.5),
    axis.text.y = element_text(size = 8)
  )

The heatmap helps identify the distance ranges in which these kickers attempted the most field goals. Comparing the panels also shows how successful and unsuccessful attempts are distributed across distance categories.

Step 5. Display the Top 10 kickers

The following table ranks kickers by the combined score. It also shows the number of attempts, total distance, average distance, and success rate so that the ranking can be interpreted alongside the underlying statistics.

# Prepare the Top 10 table
top_10_table <- top_10_kickers |>
  transmute(
    Rank = row_number(),
    Kicker = displayName,
    Attempts = kicks,
    `Total Distance (yards)` = round(total_kick_length, 1),
    `Average Distance (yards)` = round(average_kick_length, 1),
    `Successful Kicks` = made,
    `Missed Kicks` = missed,
    `Success Rate` = percent(success_rate, accuracy = 0.1),
    `Combined Score` = round(kicker_score, 2)
  )

# Display the table
kable(
  top_10_table,
  format = "html",
  caption = "Top 10 NFL Kickers by Combined Score",
  align = "c"
) |>
  kable_styling(
    bootstrap_options = c("striped", "hover", "condensed"),
    full_width = TRUE,
    position = "center"
  )
Top 10 NFL Kickers by Combined Score
Rank Kicker Attempts Total Distance (yards) Average Distance (yards) Successful Kicks Missed Kicks Success Rate Combined Score
1 Graham Gano 45 1796 39.9 42 3 93.3% 2.64
2 Brandon McManus 82 3344 40.8 72 10 87.8% 2.21
3 Jason Myers 80 3149 39.4 73 7 91.2% 1.96
4 Justin Tucker 92 3493 38.0 87 5 94.6% 1.69
5 Matt Bryant 34 1406 41.4 28 6 82.4% 1.62
6 Josh Lambo 57 2153 37.8 54 3 94.7% 1.60
7 Mason Crosby 71 2803 39.5 63 8 88.7% 1.60
8 Sebastian Janikowski 24 967 40.3 20 4 83.3% 1.16
9 Dustin Hopkins 85 3321 39.1 73 12 85.9% 0.87
10 Randy Bullock 77 3012 39.1 66 11 85.7% 0.87

Step 6. Visualize the Top 10 ranking

# Compare the combined scores of the Top 10 kickers
ggplot(
  top_10_kickers,
  aes(
    x = reorder(displayName, kicker_score),
    y = kicker_score
  )
) +
  geom_col() +
  coord_flip() +
  labs(
    title = "Top 10 NFL Kickers by Combined Score",
    x = "Kicker",
    y = "Combined Score"
  ) +
  theme_minimal() +
  theme(
    plot.title = element_text(hjust = 0.5)
  )

Step 7. Interpret the results

The table and ranking graph identify the kickers with the highest combined scores among those with at least 10 recorded attempts. The scatterplot provides context about the relationship between total kick distance and number of opportunities, while the heatmap shows the distribution of attempts by distance and result.

Conclusion

This project examines NFL field goal kickers from 2018 through 2020 by considering the distances they attempted, their number of attempts, and their success rates. The visualizations provide different perspectives on kicker performance: the scatterplot compares total distance with opportunities, the heatmap compares attempts across distance categories, and the Top 10 table and graph summarize the combined ranking.

The main takeaway is that identifying the best kickers depends on how performance is measured. A kicker who attempts longer field goals may be trusted with more difficult opportunities, but distance alone does not guarantee success. Considering attempt volume, average distance, and success rate together provides a broader basis for comparing kickers.

I have came to the conclusion that Graham Gano is the best Kicker based on my analysis on longest field goals.

Loyola University Maryland logo