library(nflreadr)
library(dplyr)
library(ggplot2)
library(pROC)
library(tidyverse)
library(patchwork)
#Player directory: draft info + birth date, one row per player
player_info <- load_players() |>
#just pulling biographical info about a player
#id, birth date, draft info, and year of their rookie season (undrafted players don't have a draft year)
select(gsis_id, birth_date, draft_year, draft_round, draft_pick, rookie_season) |>
#keeping only one instance of each player
distinct(gsis_id, .keep_all = TRUE) |>
#change the birth_date variable to a datetime variable
mutate(birth_date = as.Date(birth_date))
#want to get juset wr stats over the last 20 years, and just pulling offense players for the sake of memory and data processing time
wr_stats <- load_player_stats(seasons = 2006:2025) |>
#just want regular season stats from WR
filter(season_type == "REG", position == "WR") |>
#grouping by season, id, name, display name, in order to add other stats to individual player season totals (probably only need to group by season and player_id but this is fine too)
group_by(season, player_id, player_name, player_display_name) |>
#for each unqique player and unique season, find number games, targets, reception, receiving yards, receiving tds, air yards, rec_epa
summarise(
games = n_distinct(week),
targets = sum(targets, na.rm = TRUE),
receptions = sum(receptions, na.rm = TRUE),
rec_yards = sum(receiving_yards, na.rm = TRUE),
rec_tds = sum(receiving_tds, na.rm = TRUE),
air_yards = sum(receiving_air_yards, na.rm = TRUE), #reveiving_air_yards is a weird stat, its air yards of the ball on each TARGET including incompletions, this number is frequently bigger that rec_yrds because of that
rec_epa = sum(receiving_epa, na.rm = TRUE),
.groups = "drop" #use .groups = "drop" to ungroup after getting the necessary stats
) |>
#we want receivers that see some field action, 20 total targets in any given season feels like a good amount
filter(targets >= 20) |>
#adding yards, targets, yards per target, for each season
mutate(
yards_per_game = rec_yards / games,
targets_per_game = targets / games,
yards_per_target = rec_yards / targets
) |>
#joining the player_info df (contains draft info, birth day) onto the main stats df
#player_id from main stats df equals gsis_id from player_info df
left_join(player_info, by = c("player_id" = "gsis_id")) |>
#grouping by player_id -- add info for each player in each season
group_by(player_id) |>
#add the first season for a player with min(season) (the df is grouped by player id)
mutate(first_season_in_data = min(season)) |>
#ungroup after pulling a player's first season
ungroup() |>
#addding draft info abount each player
mutate(
#drafted binary logical variable needs to be made BEFORE adding dummy variables to undrafted players
#if a player has a draft_pick, drafted is TRUE, else FALSE
drafted = !is.na(draft_pick),
#coalese() returns the FIRST non-NA value it finds, so draft year will essentially just end up being the first year a player has any recorded stats
draft_year = coalesce(draft_year, rookie_season, first_season_in_data),
draft_pick = if_else(drafted, as.numeric(draft_pick), 263), #assign number for undrafted players
draft_round = if_else(drafted, as.numeric(draft_round), 8), #assign round for undrafted players
age = as.numeric(difftime(as.Date(paste0(season, "-09-01")),
birth_date, units = "days")) / 365.25,
#age floor rounds age down, so that you can group players by age number later on (age is in decimal for some reason)
age_floor = floor(age),
#find number of years in the league for each player each season
yrs_in_league = season - draft_year
) |>
#the rookie_season and first_season_in_data columns were just use to create the draft_year column, they're not needed anymore
select(-rookie_season, -first_season_in_data) |>
#put all these columns after player_id, makes it cleanere
relocate(player_display_name, player_name, birth_date, age, age_floor,
draft_year, draft_round, draft_pick, drafted,
.after = player_id)
#general production stats grouped by age
age_curve <- wr_stats |>
filter(!is.na(age_floor)) |>
group_by(age_floor) |>
summarise(
player_seasons = n(),
avg_targets_pg = mean(targets_per_game),
avg_yards_pg = mean(yards_per_game),
avg_ypt = mean(yards_per_target),
avg_tds = mean(rec_tds),
avg_epa = mean(rec_epa, na.rm = TRUE),
.groups = "drop"
)
age_curve
wr_theme <- theme_minimal(base_family = 'mono', base_size = 14) +
theme(
plot.margin = margin(20, 20, 20, 20),
text = element_text(color = 'black', face = 'bold'),
plot.title = element_text(color = 'black', face = 'bold', size = 16),
axis.title = element_text(color = 'black', face = 'bold'),
axis.text = element_text(color = 'black', face = 'bold')
)
#the goal is to see at what point in a WR's career are they at their best, making extremely quick plots about age and number of years in the league
#how many instances of each age are there in the dataset
age <- wr_stats %>% ggplot(aes(x = age)) +
geom_histogram(binwidth = 1, fill = 'royalblue3', color = 'white') +
wr_theme +
labs(title = 'Counts of WR by Age', x = 'Age', y = 'Counts')
age

#how many instances of career years in the league
years <- wr_stats %>% ggplot(aes(x = yrs_in_league)) +
geom_histogram(binwidth = 1, fill = 'royalblue3', color = 'white') +
wr_theme +
labs(title = 'Counts of WR by Career Years', x = 'Years in League', y = 'Counts')
years

#age for a season vs yards per game
age_v_yards <- wr_stats %>% ggplot(aes(x = age, y = yards_per_game)) +
geom_point(color = 'royalblue3', alpha = .5) +
wr_theme +
labs(title = 'Rec Yards Per Game vs Age', x = 'Age', y = 'Receiving Yards Per Game')
age_v_yards

#number of years spent in the league for a given seaseon vs yards per game
years_v_yards <- wr_stats %>% ggplot(aes(x = yrs_in_league, y = yards_per_game)) +
geom_point(color = 'royalblue3', alpha = .5) +
wr_theme +
labs(title = 'Rec Yards Per Game vs Career Years', x = 'Years in the League', y = 'Receiving Yards per Game')
years_v_yards

combined <- (age | years) / (age_v_yards | years_v_yards)
ggsave(filename = "~/Documents/elite_wr/wr_plot.png", combined,
width = 12, height = 9, dpi = 300, bg = "white")
combined

#elite wrs
#defining an "elite" seaon as having either 1000 receiving yards or 100 catches (rarely is it one or the other however)
elite_wr_stats <- wr_stats %>% filter((rec_yards >= 1000) | (receptions >= 100), drafted == TRUE)
#all recievers who never had either 1000 yards or 100 catches are non-elite
non_elite_wr_stats <- wr_stats %>% filter((rec_yards < 1000) & (receptions < 100), drafted == TRUE)
#making the same quick plots but for elite receivers only
age <- elite_wr_stats %>% ggplot(aes(x=age)) +
geom_histogram(binwidth=1,fill = 'royalblue3', color = 'white') +
wr_theme +
labs(title='Counts of Elite WR by Age', x='Age', y='Counts')
age

years <- elite_wr_stats %>% ggplot(aes(x=yrs_in_league)) +
geom_histogram(binwidth=1, fill = 'royalblue3', color = 'white') +
wr_theme +
labs(title = 'Counts of Elite WR by Career Years', x ='Years in League', y='Counts')
years

age_v_yards <- elite_wr_stats %>% ggplot(aes(x=age, y=yards_per_game)) +
geom_point(color = 'royalblue3', alpha = .5) +
wr_theme +
labs(title = 'Elite WR Rec Yards Per Game vs Age', x ='Age', y='Receiving Yards Per Game')
age_v_yards

years_v_yards <- elite_wr_stats %>% ggplot(aes(x=yrs_in_league, y=yards_per_game)) +
geom_point(color = 'royalblue3', alpha = .5) +
wr_theme +
labs(title = str_wrap('Elite WR Rec Yards Per Game vs Career Years', width=40), x ='Years in the League', y='Receiving Yards per Game')
years_v_yards

combined <- (age | years) / (age_v_yards | years_v_yards)
ggsave(filename = "~/Documents/elite_wr/elite_wr_plot.png", combined, width = 12, height = 9, dpi = 300, bg = "white")
combined

#going to make a histogram that combines elite wrs and non-elite wrs
overlay_df <- bind_rows(
non_elite_wr_stats |> transmute(age = age, group = "Non-Elite"),
elite_wr_stats |> transmute(age = age, group = "Elite")
)
#getting the means ages of elite and non-elite for visualization
#making a df that will contain the mean, the group, and a label that will go on the final graph
means <- overlay_df |>
#the group column is "Elite" or "Non-Elite"
group_by(group) |>
summarise(mean_val = mean(age, na.rm = TRUE), .groups = "drop") |>
#adding a label column that will read "[group name] mean: [value]"
mutate(label = paste0(group, " mean age: ", round(mean_val, 1)))
#(new) pick a color for each group here, any R color name or hex code works
group_colors <- c("Elite" = "firebrick3", "Non-Elite" = "royalblue3")
#using the overlay_df data for making the overlayed histogram
#in aes() x is age, and fill color will be by group
p <- ggplot(overlay_df, aes(x = age, fill = group)) +
#after_stat(density) tells ggplot to maek the y-axis density (within group) after it's done the binning
#position = "identity" tells ggplot to keep bars overlapped, "stack" would make them go on top of each other
geom_histogram(aes(y = after_stat(density)), # density so unequal group sizes are comparable
#color "white" makes the outlines white so that bars ear easy to see
position = "identity", alpha = 0.6, bins = 30, color = "white") +
#add the vertical line with the means of each group, use the means df from before
geom_vline(data = means, aes(xintercept = mean_val, color = group),
#show.legend = FALSE because we're labeling the lines directly
linewidth = 1, linetype = "dashed", show.legend = FALSE) +
#add the labels to the mean lines
geom_label(data = means,
#y = Inf so that the line goes up to the top of the graph
#use data = means and pull the label from the label column
aes(x = mean_val, y = Inf, label = label, color = group),
vjust = c(1.5, 3.2), hjust = -0.05, # staggered so labels don't collide
fill = "white", show.legend = FALSE) +
#(new) fill colors the bars, color colors the mean lines and label text
scale_fill_manual(values = group_colors) +
scale_color_manual(values = group_colors) +
labs(title = 'Proportion of Elite and Non-Elite WR Seasons by Age', x = "Age", y = "Density", fill = NULL) +
#using the mono font, looks better
wr_theme
p

ggsave(filename = "~/Documents/elite_wr/elite_v_non-e_age.png", p, width = 12, height = 9, dpi = 300, bg = "white")
#welch's T-test compare 2 means from different populations without assuming they have equal variances
t.test(age ~ group, data = overlay_df)
##
## Welch Two Sample t-test
##
## data: age by group
## t = 1.7881, df = 497.11, p-value = 0.07437
## alternative hypothesis: true difference in means between group Elite and group Non-Elite is not equal to 0
## 95 percent confidence interval:
## -0.03274627 0.69556750
## sample estimates:
## mean in group Elite mean in group Non-Elite
## 26.41623 26.08482
#primary output: t = 1.6944, df = 489.97, p-value = 0.09082
#the p-value is low but not low enough to be significant, fails to reject the null hypothesis that the mean ages between elite and non-elite wr seasons are equal
#doing everything again but instead of age, using yrs_in_league
overlay_df <- bind_rows(
non_elite_wr_stats |> transmute(yrs_in_league = yrs_in_league, group = "Non-Elite"),
elite_wr_stats |> transmute(yrs_in_league = yrs_in_league, group = "Elite")
)
means <- overlay_df |>
group_by(group) |>
summarise(mean_val = mean(yrs_in_league, na.rm = TRUE), .groups = "drop") |>
mutate(label = paste0(group, " mean years in league: ", round(mean_val, 1)))
group_colors <- c("Elite" = "firebrick3", "Non-Elite" = "royalblue3")
p <- ggplot(overlay_df, aes(x = yrs_in_league, fill = group)) +
geom_histogram(aes(y = after_stat(density)), # density so unequal group sizes are comparable
position = "identity", alpha = 0.5, binwidth = 1, color = "white") +
geom_vline(data = means, aes(xintercept = mean_val, color = group),
linewidth = 1, linetype = "dashed", show.legend = FALSE) +
geom_label(data = means,
aes(x = mean_val, y = Inf, label = label, color = group),
vjust = c(1.5, 3.2), hjust = -0.05, # staggered so labels don't collide
fill = "white", show.legend = FALSE) +
labs(title = str_wrap('Proportion of Elite and Non-Elite WR Seasons by Career Years', width=40), x = "Years in League", y = "Density", fill = NULL) +
scale_fill_manual(values = group_colors) +
scale_color_manual(values = group_colors) +
wr_theme
p

ggsave(filename = "~/Documents/elite_wr/elite_v_non-e_years.png", p, width = 12, height = 9, dpi = 300, bg = "white")
t.test(yrs_in_league ~ group, data = overlay_df)
##
## Welch Two Sample t-test
##
## data: yrs_in_league by group
## t = 3.779, df = 501.6, p-value = 0.0001764
## alternative hypothesis: true difference in means between group Elite and group Non-Elite is not equal to 0
## 95 percent confidence interval:
## 0.3252914 1.0297958
## sample estimates:
## mean in group Elite mean in group Non-Elite
## 4.203125 3.525581
#primary results: t = 3.6503, df = 494.75, p-value = 0.0002898
#the p-value is significant and rejects the null hypothesis that the mean years in the league between elite and non-elite WR seasons are equal
#now compare elite vs non-elite draft position
overlay_df <- bind_rows(
non_elite_wr_stats |> transmute(draft_pick = draft_pick, group = "Non-Elite"),
elite_wr_stats |> transmute(draft_pick = draft_pick, group = "Elite")
)
means <- overlay_df |>
group_by(group) |>
summarise(mean_val = mean(draft_pick, na.rm = TRUE), .groups = "drop") |>
mutate(label = paste0(group, " mean draft position: ", round(mean_val, 1)))
group_colors <- c("Elite" = "firebrick3", "Non-Elite" = "royalblue3")
p <- ggplot(overlay_df, aes(x = draft_pick, fill = group)) +
geom_histogram(aes(y = after_stat(density)), # density so unequal group sizes are comparable
position = "identity", alpha = 0.5, binwidth = 10, color = "white") +
geom_vline(data = means, aes(xintercept = mean_val, color = group),
linewidth = 1, linetype = "dashed", show.legend = FALSE) +
geom_label(data = means,
aes(x = mean_val, y = Inf, label = label, color = group),
vjust = c(1.5, 3.2), hjust = 0.03, # staggered so labels don't collide
fill = "white", show.legend = FALSE) +
labs(title = str_wrap('Proportion of Elite and Non-Elite WR Seasons by Draft Pick', width=40), x = "Draft Pick", y = "Density", fill = NULL) +
scale_fill_manual(values = group_colors) +
scale_color_manual(values = group_colors) +
wr_theme
p

ggsave(filename = "~/Documents/elite_wr/elite_v_non-e_pick.png", p, width = 12, height = 9, dpi = 300, bg = "white")
t.test(draft_pick ~ group, data = overlay_df)
##
## Welch Two Sample t-test
##
## data: draft_pick by group
## t = -8.5347, df = 505.41, p-value < 2.2e-16
## alternative hypothesis: true difference in means between group Elite and group Non-Elite is not equal to 0
## 95 percent confidence interval:
## -38.35059 -23.99812
## sample estimates:
## mean in group Elite mean in group Non-Elite
## 57.59375 88.76811
#pimrary results: t = -8.341, df = 498.54, p-value = 7.257e-16
#this is the most significant of the three, rejects the null hypothesis
#now compare elite vs non-elite draft position
overlay_df <- bind_rows(
non_elite_wr_stats |> transmute(draft_round = draft_round, group = "Non-Elite"),
elite_wr_stats |> transmute(draft_round = draft_round, group = "Elite")
)
means <- overlay_df |>
group_by(group) |>
summarise(mean_val = mean(draft_round, na.rm = TRUE), .groups = "drop") |>
mutate(label = paste0(group, " mean draft round: ", round(mean_val, 1)))
group_colors <- c("Elite" = "firebrick3", "Non-Elite" = "royalblue3")
p <- ggplot(overlay_df, aes(x = draft_round, fill = group)) +
geom_histogram(aes(y = after_stat(density)), # density so unequal group sizes are comparable
position = "identity", alpha = 0.5, binwidth = 1, color = "white") +
geom_vline(data = means, aes(xintercept = mean_val, color = group),
linewidth = 1, linetype = "dashed", show.legend = FALSE) +
geom_label(data = means,
aes(x = mean_val, y = Inf, label = label, color = group),
vjust = c(1.5, 3.2), hjust = -0.05, # staggered so labels don't collide
fill = "white", show.legend = FALSE) +
labs(title = str_wrap('Proportion of Elite and Non-Elite WR Seasons by Draft Round', width=40), x = "Draft Round", y = "Density", fill = NULL) +
scale_fill_manual(values = group_colors) +
scale_color_manual(values = group_colors) +
wr_theme
p

ggsave(filename = "~/Documents/elite_wr/elite_v_non-e_round.png", p, width = 12, height = 9, dpi = 300, bg = "white")
t.test(draft_round ~ group, data = overlay_df)
##
## Welch Two Sample t-test
##
## data: draft_round by group
## t = -8.7989, df = 507.15, p-value < 2.2e-16
## alternative hypothesis: true difference in means between group Elite and group Non-Elite is not equal to 0
## 95 percent confidence interval:
## -1.0783374 -0.6846817
## sample estimates:
## mean in group Elite mean in group Non-Elite
## 2.228125 3.109635
#Modeling and inference
#what is the probability of an elite season given draft position and years in the league
#making a df that will be used to build regression models of elite wrs
model_df <- wr_stats |>
#only using wrs that were draft, all the undrafted players skew the data
filter(drafted, !is.na(yrs_in_league), !is.na(draft_pick)) |>
mutate(
#adds binary logical variable for whether a wr is elite or not base on the definition we've been using
elite = as.integer(rec_yards >= 1000 | receptions >= 100),
#effect of draft pick isn't linear (as shown by the graphs of elite WR by draft pick/round)
#massive drop off afer round 1, then after round 2/3, flattens in later rounds; difference between pick 1 and 10 is important, difference between pick 210 and 220 is negligible
#will be doing LOGISTIC regression as a result of this and compare to a standard linear regression
log_pick = log(draft_pick)
)
#Linear version, regressing elite (TRUE/FALSE) on yrs_in_league, and draft_pick (family=binomial b/c elite is a binomial variable)
m1 <- glm(elite ~ yrs_in_league + draft_pick, data = model_df, family = binomial)
#Better version: career arcs are curved, and pick value is steep early then flat
#I(yrs_in_league^2) creates a squared term for years in the league, fits a curved relationship (WR careers usually follow a parabolic curve, not a straight line)
m2 <- glm(elite ~ yrs_in_league + I(yrs_in_league^2) + log_pick,
data = model_df, family = binomial)
summary(m2)
##
## Call:
## glm(formula = elite ~ yrs_in_league + I(yrs_in_league^2) + log_pick,
## family = binomial, data = model_df)
##
## Coefficients:
## Estimate Std. Error z value Pr(>|z|)
## (Intercept) -0.09209 0.25639 -0.359 0.719
## yrs_in_league 0.37513 0.06556 5.722 1.05e-08 ***
## I(yrs_in_league^2) -0.03227 0.00622 -5.187 2.14e-07 ***
## log_pick -0.55743 0.05760 -9.677 < 2e-16 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## (Dispersion parameter for binomial family taken to be 1)
##
## Null deviance: 1694.5 on 1824 degrees of freedom
## Residual deviance: 1560.4 on 1821 degrees of freedom
## AIC: 1568.4
##
## Number of Fisher Scoring iterations: 5
#yrs_in_league has positive correlation (.3642), and I(yrs_in_league^2) has a slight negative correlation (-.03), this means the curve of this is an inverted U (what we expect)
#log_pick correlation is -.539, doubling of draft pick number multiplies odds of an elite season: exp(-0.538 * log(2)) = exp(-0.373) ≈ 0.69; double the draft pick means likelihood of elite season is cut by 30% (from the original draft position, so if pick 50 is 15%, then pick 100 is 10%)
#each predictor is significant
out <- capture.output(summary(m2))
txt <- paste(out, collapse = "\n")
p <- ggplot() +
annotate("text", x = 0, y = 1, label = txt, hjust = 0, vjust = 1,
family = "mono", size = 3.5, color = "#d4d4d4") +
xlim(0, 1) + ylim(0, 1) +
theme_void() +
theme(plot.background = element_rect(fill = "#1e1e1e", color = NA),
plot.margin = margin(20, 20, 20, 20))
ggsave(filename = "~/Documents/elite_wr/m2_summary.png", p, width = 8, height = 6, dpi = 300)
AIC(m1, m2) # lower is better
#using the second model to predict an elite season from a 3rd year wr that was drafted 15 overall
predict(m2,
newdata = data.frame(yrs_in_league = 3, log_pick = log(15)),
type = "response") #type = "response" makes the output a normal probability instead of a log-odds ('link' returns the log-odds from glm)
## 1
## 0.3172072
#expand.grid() makes a df of every combination of values you give
#in this case, every combination of yrs_in_league (0 to 12) and draft_pick(5, 15, 32, 64, 100, 150, 220), then for each combination of year in the league and draft pick we calculate probability of an elite season with the model
grid <- expand.grid(yrs_in_league = 0:12, draft_pick = c(5, 15, 32, 64, 100, 150, 220)) |>
mutate(log_pick = log(draft_pick))
#add probability of an elite season to the grid df
grid$p_elite <- predict(m2, grid, type = "response")
p <- ggplot(grid, aes(yrs_in_league, p_elite, color = factor(draft_pick))) +
geom_line(linewidth = 1) +
labs(title = str_wrap("Probability of Elite WR Season \nGiven Year in League and Draft Pick", width=40), x = "Years in league", y = "P(elite season)", color = "Draft pick") +
wr_theme
p

ggsave(filename = "~/Documents/elite_wr/elite_prob_m1.png", p, width = 12, height = 9, dpi = 300, bg = "white")
roc(model_df$elite, fitted(m2)) # AUC; 0.5 = coin flip, 1 = perfect
##
## Call:
## roc.default(response = model_df$elite, predictor = fitted(m2))
##
## Data: fitted(m2) in 1505 controls (model_df$elite 0) < 320 cases (model_df$elite 1).
## Area under the curve: 0.7005
#a random elite season gets a higher predicted probability than a random non-elite season .6955 of the time
#new model adds INTERACTION between draft position and career curve; shape of the years-in-league curve can differ by draft pick
#in m2 every player shares one curve, one years where career peaks the most across all draft picks (about year 6)
#earlier draft picks may get to peak earlier in career, later picks may take longer
m4 <- glm(elite ~ (yrs_in_league + I(yrs_in_league^2)) * log_pick,
data = model_df, family = binomial)
summary(m4)
##
## Call:
## glm(formula = elite ~ (yrs_in_league + I(yrs_in_league^2)) *
## log_pick, family = binomial, data = model_df)
##
## Coefficients:
## Estimate Std. Error z value Pr(>|z|)
## (Intercept) 1.661e+00 4.969e-01 3.343 0.000828 ***
## yrs_in_league -2.307e-01 1.999e-01 -1.154 0.248438
## I(yrs_in_league^2) 8.864e-05 1.715e-02 0.005 0.995875
## log_pick -1.048e+00 1.385e-01 -7.566 3.86e-14 ***
## yrs_in_league:log_pick 1.677e-01 5.572e-02 3.010 0.002616 **
## I(yrs_in_league^2):log_pick -8.737e-03 4.854e-03 -1.800 0.071836 .
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## (Dispersion parameter for binomial family taken to be 1)
##
## Null deviance: 1694.5 on 1824 degrees of freedom
## Residual deviance: 1541.3 on 1819 degrees of freedom
## AIC: 1553.3
##
## Number of Fisher Scoring iterations: 5
out <- capture.output(summary(m4))
txt <- paste(out, collapse = "\n")
p <- ggplot() +
annotate("text", x = 0, y = 1, label = txt, hjust = 0, vjust = 1,
family = "mono", size = 3.5, color = "#d4d4d4") +
xlim(0, 1) + ylim(0, 1) +
theme_void() +
theme(plot.background = element_rect(fill = "#1e1e1e", color = NA),
plot.margin = margin(20, 20, 20, 20))
ggsave(filename = "~/Documents/elite_wr/m4_summary.png", p, width = 8, height = 6, dpi = 300)
grid$p_int <- predict(m4, grid, type = "response")
grid |>
mutate(tier = cut(draft_pick, c(0, 10, 32, 64, 100, 256))) |>
group_by(tier) |>
slice_max(p_int, n = 1, with_ties = FALSE)
p <- ggplot(grid, aes(yrs_in_league, p_int, color = factor(draft_pick))) +
geom_line(linewidth = 1) +
labs(title = str_wrap("Probability of Elite WR Season \nGiven Year in League and Draft Pick (Interaction Model)", width=40), x = "Years in league", y = "P(elite season)", color = "Draft pick") +
wr_theme
ggsave(filename = "~/Documents/elite_wr/elite_prob_m2.png", p, width = 12, height = 9, dpi = 300, bg = "white")
p

#see which model performs better
AIC(m2, m4)
anova(m2, m4, test = "Chisq")
#m2 residual deviance: 1589
#m4 residual deviance: 1571
#large residual deviance drop-off
#rows of same players correlate with each other, can create bias
#m4 fits better than m2
#heat map of probability for an elite season from m2
heat_grid <- expand.grid(
yrs_in_league = 0:12,
draft_pick = 1:256
) |>
mutate(log_pick = log(draft_pick))
heat_grid$p_elite <- predict(m2, heat_grid, type = "response")
p <- ggplot(heat_grid, aes(yrs_in_league, draft_pick, fill = p_elite)) +
geom_tile() +
scale_y_reverse(breaks = seq(0, 250, 25)) +
scale_x_continuous(breaks = 0:12) +
scale_fill_gradientn(
colors = c("#08306b", "#4393c3", "#f7f7f7", "#f4a582", "#b2182b"),
labels = scales::percent, name = "P(elite)"
) +
labs(title = 'Probability of Elite Season by Draft Pick', x = "Years in league", y = "Draft pick") +
wr_theme
p

ggsave(filename = "~/Documents/elite_wr/m1_heatmap.png", p, width = 12, height = 9, dpi = 300, bg = "white")
#heat map of probability for an elite season from m4
heat_grid <- expand.grid(
yrs_in_league = 0:12,
draft_pick = 1:256
) |>
mutate(log_pick = log(draft_pick))
heat_grid$p_elite <- predict(m4, heat_grid, type = "response")
p <- ggplot(heat_grid, aes(yrs_in_league, draft_pick, fill = p_elite)) +
geom_tile() +
scale_y_reverse(breaks = seq(0, 250, 25)) +
scale_x_continuous(breaks = 0:12) +
scale_fill_gradientn(
colors = c("#08306b", "#4393c3", "#f7f7f7", "#f4a582", "#b2182b"),
labels = scales::percent, name = "P(elite)"
) +
labs(title=str_wrap('Probability of Elite Season by Draft Pick (Interaction Model)', width=40), x = "Years in league", y = "Draft pick") +
wr_theme
p

ggsave(filename = "~/Documents/elite_wr/m2_heatmap.png", p, width = 12, height = 9, dpi = 300, bg = "white")
model_df |> group_by(yrs_in_league) |> summarise(rate = mean(elite), n = n())
#raw counts of elite seasons for each draft pick and year
#based on eye test, looks like it agrees more with m4 than m2
elite_counts <- model_df |>
filter(elite == 1) |>
count(yrs_in_league, draft_pick)
p <- ggplot(elite_counts, aes(yrs_in_league, draft_pick, fill = n)) +
geom_tile() +
scale_y_reverse(breaks = seq(0, 250, 25)) +
scale_x_continuous(breaks = 0:15) +
scale_fill_gradientn(
colors = c("#f4a582", "#b2182b"),
name = "Elite seasons",
breaks = scales::breaks_pretty()
) +
labs(title='Counts of Elite Seasons by Draft Pick', x = "Years in league", y = "Draft pick") +
wr_theme
p

ggsave(filename = "~/Documents/elite_wr/counts_heatmap.png", p, width = 12, height = 9, dpi = 300, bg = "white")