I want your feedback to make the book better for you and other readers. If you find typos, errors, or places where the text may be improved, please let me know. The best ways to provide feedback are by GitHub or hypothes.is annotations.
You can leave a comment at the bottom of the page/chapter, or open an issue or submit a pull request on GitHub: https://github.com/isaactpetersen/Fantasy-Football-Analytics-Textbook
Alternatively, you can leave an annotation using hypothes.is.
To add an annotation, select some text and then click the
symbol on the pop-up menu.
To see the annotations of others, click the
symbol in the upper right-hand corner of the page.
18 Mythbusters: Putting Fantasy Football Beliefs/Anecdotes to the Test
In this chapter, we put a popular fantasy football belief to the test. We evaluate the widely held belief that players perform better during a contract year.
18.1 Getting Started
18.1.1 Load Packages
18.1.2 Specify Package Options
18.1.3 Load Data
We created the player_stats_weekly.RData and player_stats_seasonal.RData objects in Section 4.4.3.
18.2 Do Players Perform Better in their Contract Year?
Considerable speculation exists regarding whether players perform better in their last year of their contract (i.e., their “contract year”). Fantasy football talking heads and commentators frequently discuss the benefit of selecting players who are in their contract year, because it supposedly means that player has more motivation to perform well so they get a new contract and get paid more. To our knowledge, no peer-reviewed studies have examined this question for football players. One study found that National Basketball Association (NBA) players improved in field goal percentage, points, and player efficiency rating (but not other statistics: rebounds, assists, steals, or blocks) from their pre-contract year to their contract year, and that Major League Baseball (MLB) players improved in runs batted in (RBIs; but not other statistics: batting average, slugging percentage, on base percentage, home runs, fielding percentage) from their pre-contract year to their contract year (White & Sheldon, 2014). Other casual analyses have been examined contract-year performance of National Football League (NFL) players, including articles in 2012 [Bales (2012); archived here] and 2022 [Niles (2022); archived here].
Let’s examine the question empirically. Our research questions is: Do players perform better in their “contract year” (i.e., the last year of their contract)? Our hypothesis is that players are motivated to get larger contracts (more money), leading players in their contract year to try harder and perform better. If the hypothesis is true, we predict that players who are in their contract year will tend to score more fantasy points than players who are not in their contract year.
In order to test this question empirically, we have to make some assumptions/constraints. In this example, we will make the following constraints:
- We will determine a player’s contract year programmatically based on the year the contract was signed. For instance, if a player signed a 3-year contract in 2015, their contract would expire in 2018, and thus their contract year would be 2017. Note: this is a coarse way of determining a player’s contract year because it could depend on when during the year the player’s contract is signed. If we were submitting this analysis as a paper to a scientific journal, it would be important to verify each player’s contract year.
- We will examine performance in all seasons since 2011, beginning when most data for player contracts are available.
- For maximum statistical power to detect an effect if a contract year effect exists, we will examine all seasons for a player (since 2011), not just their contract year and their pre-contract year.
- To ensure a more fair, apples-to-apples comparison of the games in which players played, we will examine per-game performance (except for yards per carry, which is based on \(\frac{\text{rushing yards}}{\text{carries}}\) from the entire season).
- We will examine regular season games only (no postseason).
- To ensure we do not make generalization about a player’s performance in a season from a small sample, the player has to play at least 5 games in a given season for that player–season combination to be included in analysis.
For analysis, the same player contributes multiple observations of performance (i.e., multiple seasons) due to the longitudinal nature of the data. Inclusion of multiple data points from the same player would violate the assumption of multiple regression that all observations are independent. Thus, we use mixed-effects models that allow nonindependent observations. In our mixed-effects models, we include a random intercept for each player, to allow our model to account for players’ differing level of performance. We examine two mixed-effects models for each outcome variable: one model that accounts for the effects of age and experience, and one model that does not.
The model that does not account for the effects of age and experience includes:
- random intercepts to allow the model to estimate a different starting point for each player
- a fixed effect for whether the player is in a contract year
The model that accounts for the effects of age and experience includes:
- random intercepts to allow the model to estimate a different starting point for each player
- random linear slopes (i.e., random effect of linear age) to allow the model to estimate a different form of change for each player
- a fixed quadratic effect of age to allow for curvilinear effects
- a fixed effect of experience
- a fixed effect for whether the player is in a contract year
Code
# Subset to remove players without a year signed
nfl_playerContracts_subset <- nfl_playerContracts %>%
dplyr::filter(!is.na(year_signed) & year_signed != 0)
# Determine the contract year for a given contract
nfl_playerContracts_subset$contractYear <- nfl_playerContracts_subset$year_signed + nfl_playerContracts_subset$years - 1
# Arrange contracts by player and year_signed
nfl_playerContracts_subset <- nfl_playerContracts_subset %>%
dplyr::group_by(player, position) %>%
dplyr::arrange(player, position, -year_signed) %>%
dplyr::ungroup()
# Determine if the player played in the original contract year
nfl_playerContracts_subset <- nfl_playerContracts_subset %>%
dplyr::group_by(player, position) %>%
dplyr::mutate(
next_contract_start = lag(year_signed)) %>%
dplyr::ungroup() %>%
dplyr::mutate(
played_in_contract_year = ifelse(
is.na(next_contract_start) | contractYear < next_contract_start,
TRUE,
FALSE))
# Check individual players
#nfl_playerContracts_subset %>%
# dplyr::filter(player == "Aaron Rodgers") %>%
# dplyr::select(player:years, contractYear, next_contract_start, played_in_contract_year)
#
#nfl_playerContracts_subset %>%
# dplyr::filter(player %in% c("Jared Allen", "Aaron Rodgers")) %>%
# dplyr::select(player:years, contractYear, next_contract_start, played_in_contract_year)
# Subset data
nfl_playerContractYears <- nfl_playerContracts_subset %>%
dplyr::filter(played_in_contract_year == TRUE) %>%
dplyr::filter(position %in% c("QB","RB","WR","TE")) %>%
dplyr::select(player, position, team, contractYear) %>%
dplyr::mutate(merge_name = nflreadr::clean_player_names(player, lowercase = TRUE)) %>%
dplyr::rename(season = contractYear) %>%
dplyr::mutate(contractYear = 1)
# Merge with weekly and seasonal stats data
player_stats_weekly_offense <- player_stats_weekly %>%
dplyr::filter(position_group %in% c("QB","RB","WR","TE")) %>%
dplyr::mutate(merge_name = nflreadr::clean_player_names(player_display_name, lowercase = TRUE))
#nfl_actualStats_offense_seasonal <- nfl_actualStats_offense_seasonal %>%
# mutate(merge_name = nflreadr::clean_player_names(player_display_name, lowercase = TRUE))
player_statsContracts_offense_weekly <- dplyr::full_join(
player_stats_weekly_offense,
nfl_playerContractYears,
by = c("merge_name", "position_group" = "position", "season")
) %>%
dplyr::filter(position_group %in% c("QB","RB","WR","TE"))
#player_statsContracts_offense_seasonal <- full_join(
# player_stats_seasonal_offense,
# nfl_playerContractYears,
# by = c("merge_name", "position_group" = "position", "season")
#) %>%
# filter(position_group %in% c("QB","RB","WR","TE"))
player_statsContracts_offense_weekly$contractYear[which(is.na(player_statsContracts_offense_weekly$contractYear))] <- 0
#player_statsContracts_offense_seasonal$contractYear[which(is.na(player_statsContracts_offense_seasonal$contractYear))] <- 0
#player_statsContracts_offense_weekly$contractYear <- factor(
# player_statsContracts_offense_weekly$contractYear,
# levels = c(0, 1),
# labels = c("no", "yes"))
#player_statsContracts_offense_seasonal$contractYear <- factor(
# player_statsContracts_offense_seasonal$contractYear,
# levels = c(0, 1),
# labels = c("no", "yes"))
player_statsContracts_offense_weekly <- player_statsContracts_offense_weekly %>%
dplyr::arrange(merge_name, season, season_type, week)
#player_statsContracts_offense_seasonal <- player_statsContracts_offense_seasonal %>%
# arrange(merge_name, season)
player_statsContractsSubset_offense_weekly <- player_statsContracts_offense_weekly %>%
dplyr::filter(season_type == "REG")
#table(nfl_playerContracts$year_signed) # most contract data is available beginning in 2011
# Calculate Per Game Totals
player_statsContracts_seasonal <- player_statsContractsSubset_offense_weekly %>%
dplyr::group_by(player_id, season) %>%
dplyr::summarise(
player_display_name = petersenlab::Mode(player_display_name),
position_group = petersenlab::Mode(position_group),
age = min(age, na.rm = TRUE),
years_of_experience = min(years_of_experience, na.rm = TRUE),
rushing_yards = sum(rushing_yards, na.rm = TRUE), # season total
carries = sum(carries, na.rm = TRUE), # season total
rushing_epa = mean(rushing_epa, na.rm = TRUE),
receiving_yards = mean(receiving_yards, na.rm = TRUE),
receiving_epa = mean(receiving_epa, na.rm = TRUE),
fantasyPoints = sum(fantasyPoints, na.rm = TRUE), # season total
contractYear = mean(contractYear, na.rm = TRUE),
games = n(),
.groups = "drop_last"
) %>%
dplyr::mutate(
player_id = as.factor(player_id),
ypc = rushing_yards / carries,
contractYear = factor(
contractYear,
levels = c(0, 1),
labels = c("no", "yes")
))
player_statsContracts_seasonal[sapply(player_statsContracts_seasonal, is.infinite)] <- NA
player_statsContracts_seasonal$ageCentered20 <- player_statsContracts_seasonal$age - 20
player_statsContracts_seasonal$ageCentered20Quadratic <- player_statsContracts_seasonal$ageCentered20 ^ 2
# Merge with seasonal fantasy points data18.2.1 QB
First, we prepare the data by merging and performing additional processing:
Code
# Merge with QBR data
nfl_espnQBR_weekly$merge_name <- paste(nfl_espnQBR_weekly$name_first, nfl_espnQBR_weekly$name_last, sep = " ") %>%
nflreadr::clean_player_names(., lowercase = TRUE)
nfl_contractYearQBR_weekly <- nfl_playerContractYears %>%
dplyr::filter(position == "QB") %>%
dplyr::full_join(
.,
nfl_espnQBR_weekly,
by = c("merge_name","team","season")
)
nfl_contractYearQBR_weekly$contractYear[which(is.na(nfl_contractYearQBR_weekly$contractYear))] <- 0
#nfl_contractYearQBR_weekly$contractYear <- factor(
# nfl_contractYearQBR_weekly$contractYear,
# levels = c(0, 1),
# labels = c("no", "yes"))
nfl_contractYearQBR_weekly <- nfl_contractYearQBR_weekly %>%
dplyr::arrange(merge_name, season, season_type, game_week)
nfl_contractYearQBRsubset_weekly <- nfl_contractYearQBR_weekly %>%
dplyr::filter(season_type == "Regular") %>%
dplyr::arrange(merge_name, season, season_type, game_week) %>%
mutate(
player = coalesce(player, name_display),
position = "QB") %>%
group_by(merge_name, player_id) %>%
fill(player, .direction = "downup")
# Merge with age and experience
nfl_contractYearQBRsubset_weekly <- player_statsContractsSubset_offense_weekly %>%
dplyr::filter(position == "QB") %>%
dplyr::select(merge_name, season, week, age, years_of_experience, fantasyPoints) %>%
full_join(
nfl_contractYearQBRsubset_weekly,
by = c("merge_name","season", c("week" = "game_week"))
) %>% select(player_id, season, week, player, everything()) %>%
arrange(player_id, season, week)
#hist(nfl_contractYearQBRsubset_weekly$qb_plays) # players have at least 20 dropbacks per game
# Calculate Per Game Totals
nfl_contractYearQBR_seasonal <- nfl_contractYearQBRsubset_weekly %>%
dplyr::group_by(merge_name, season) %>%
dplyr::summarise(
age = min(age, na.rm = TRUE),
years_of_experience = min(years_of_experience, na.rm = TRUE),
qbr = mean(qbr_total, na.rm = TRUE),
pts_added = mean(pts_added, na.rm = TRUE),
epa_pass = mean(pass, na.rm = TRUE),
qb_plays = sum(qb_plays, na.rm = TRUE), # season total
fantasyPoints = sum(fantasyPoints, na.rm = TRUE), # season total
contractYear = mean(contractYear, na.rm = TRUE),
games = n(),
.groups = "drop_last"
) %>%
dplyr::mutate(
contractYear = factor(
contractYear,
levels = c(0, 1),
labels = c("no", "yes")
))
nfl_contractYearQBR_seasonal[sapply(nfl_contractYearQBR_seasonal, is.infinite)] <- NA
nfl_contractYearQBR_seasonal$ageCentered20 <- nfl_contractYearQBR_seasonal$age - 20
nfl_contractYearQBR_seasonal$ageCentered20Quadratic <- nfl_contractYearQBR_seasonal$ageCentered20 ^ 2
nfl_contractYearQBR_seasonal <- nfl_contractYearQBR_seasonal %>%
group_by(merge_name) %>%
mutate(player_id = as.factor(as.character(cur_group_id())))
nfl_contractYearQBRsubset_seasonal <- nfl_contractYearQBR_seasonal %>%
dplyr::filter(
games >= 5, # keep only player-season combinations in which QBs played at least 5 games
season >= 2011) # keep only seasons since 2011 (when most contract data are available)Then, we analyze the data.
18.2.1.1 Quarterback Rating
Below is a mixed model that examines whether a player has a higher QBR per game when they are in a contract year compared to when they are not in a contract year. The first model includes just contract year as a predictor. The second model includes additional covariates, including player age and experience. In terms of Quarterback Rating (QBR), findings from the models indicate that Quarterbacks did not perform significantly better in their contract year.
Code
Linear mixed model fit by REML. t-tests use Satterthwaite's method [
lmerModLmerTest]
Formula: qbr ~ contractYear + (1 | player_id)
Data: nfl_contractYearQBR_seasonal
Control: lmerControl(optimizer = "bobyqa")
REML criterion at convergence: 10013.4
Scaled residuals:
Min 1Q Median 3Q Max
-3.1438 -0.5379 0.0903 0.5719 3.1897
Random effects:
Groups Name Variance Std.Dev.
player_id (Intercept) 111.9 10.58
Residual 204.7 14.31
Number of obs: 1192, groups: player_id, 274
Fixed effects:
Estimate Std. Error df t value Pr(>|t|)
(Intercept) 44.3079 0.8418 245.9324 52.637 <2e-16 ***
contractYearyes -0.9822 1.1351 1070.8136 -0.865 0.387
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Correlation of Fixed Effects:
(Intr)
contrctYrys -0.241
# R2 for Mixed Models
Conditional R2: 0.354
Marginal R2: 0.000
contractYear emmean SE df lower.CL upper.CL
no 44.3 0.842 284 42.7 46.0
yes 43.3 1.240 797 40.9 45.8
Degrees-of-freedom method: kenward-roger
Confidence level used: 0.95
Code
Linear mixed model fit by REML. t-tests use Satterthwaite's method [
lmerModLmerTest]
Formula: qbr ~ contractYear + ageCentered20 + ageCentered20Quadratic +
years_of_experience + (1 + ageCentered20 | player_id)
Data: nfl_contractYearQBR_seasonal
Control: lmerControl(optimizer = "bobyqa")
REML criterion at convergence: 9958.3
Scaled residuals:
Min 1Q Median 3Q Max
-3.2952 -0.4944 0.0821 0.5502 3.2427
Random effects:
Groups Name Variance Std.Dev. Corr
player_id (Intercept) 148.060 12.1680
ageCentered20 0.754 0.8683 -0.54
Residual 193.631 13.9151
Number of obs: 1188, groups: player_id, 272
Fixed effects:
Estimate Std. Error df t value Pr(>|t|)
(Intercept) 39.48556 2.19458 245.59561 17.992 < 2e-16 ***
contractYearyes -0.66767 1.16702 1059.27994 -0.572 0.56737
ageCentered20 0.31325 0.62455 355.06653 0.502 0.61629
ageCentered20Quadratic -0.08998 0.02217 179.41581 -4.058 7.37e-05 ***
years_of_experience 1.51209 0.51312 320.38855 2.947 0.00345 **
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Correlation of Fixed Effects:
(Intr) cntrcY agCn20 agC20Q
contrctYrys 0.048
ageCentrd20 -0.747 -0.062
agCntrd20Qd 0.753 0.040 -0.641
yrs_f_xprnc 0.141 -0.027 -0.680 -0.073
# R2 for Mixed Models
Conditional R2: 0.402
Marginal R2: 0.027
contractYear emmean SE df lower.CL upper.CL
no 44.4 0.878 258 42.6 46.1
yes 43.7 1.250 733 41.2 46.2
Degrees-of-freedom method: kenward-roger
Confidence level used: 0.95
18.2.1.2 Points Added
In terms of points added, Quarterbacks did not perform better in their contract year.
Code
Linear mixed model fit by REML. t-tests use Satterthwaite's method [
lmerModLmerTest]
Formula: pts_added ~ contractYear + (1 | player_id)
Data: nfl_contractYearQBR_seasonal
Control: lmerControl(optimizer = "bobyqa")
REML criterion at convergence: 5446.1
Scaled residuals:
Min 1Q Median 3Q Max
-4.6351 -0.5035 0.0888 0.5462 4.2567
Random effects:
Groups Name Variance Std.Dev.
player_id (Intercept) 2.546 1.596
Residual 4.369 2.090
Number of obs: 1192, groups: player_id, 274
Fixed effects:
Estimate Std. Error df t value Pr(>|t|)
(Intercept) -0.8360 0.1255 234.9785 -6.662 1.9e-10 ***
contractYearyes -0.2156 0.1661 1058.3856 -1.298 0.195
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Correlation of Fixed Effects:
(Intr)
contrctYrys -0.236
# R2 for Mixed Models
Conditional R2: 0.369
Marginal R2: 0.001
contractYear emmean SE df lower.CL upper.CL
no -0.836 0.126 285 -1.08 -0.589
yes -1.052 0.183 788 -1.41 -0.692
Degrees-of-freedom method: kenward-roger
Confidence level used: 0.95
Code
Linear mixed model fit by REML. t-tests use Satterthwaite's method [
lmerModLmerTest]
Formula: pts_added ~ contractYear + ageCentered20 + ageCentered20Quadratic +
years_of_experience + (1 + ageCentered20 | player_id)
Data: nfl_contractYearQBR_seasonal
Control: lmerControl(optimizer = "bobyqa")
REML criterion at convergence: 5421.2
Scaled residuals:
Min 1Q Median 3Q Max
-4.8253 -0.5109 0.0882 0.5198 4.2928
Random effects:
Groups Name Variance Std.Dev. Corr
player_id (Intercept) 3.86471 1.966
ageCentered20 0.01821 0.135 -0.65
Residual 4.15832 2.039
Number of obs: 1188, groups: player_id, 272
Fixed effects:
Estimate Std. Error df t value Pr(>|t|)
(Intercept) -1.554e+00 3.304e-01 2.334e+02 -4.705 4.35e-06 ***
contractYearyes -1.962e-01 1.709e-01 1.048e+03 -1.148 0.251226
ageCentered20 3.257e-02 9.272e-02 3.459e+02 0.351 0.725640
ageCentered20Quadratic -1.213e-02 3.279e-03 1.705e+02 -3.699 0.000291 ***
years_of_experience 2.316e-01 7.515e-02 3.088e+02 3.082 0.002240 **
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Correlation of Fixed Effects:
(Intr) cntrcY agCn20 agC20Q
contrctYrys 0.049
ageCentrd20 -0.751 -0.063
agCntrd20Qd 0.749 0.045 -0.650
yrs_f_xprnc 0.152 -0.028 -0.681 -0.060
# R2 for Mixed Models
Conditional R2: 0.401
Marginal R2: 0.024
contractYear emmean SE df lower.CL upper.CL
no -0.783 0.128 261 -1.03 -0.531
yes -0.979 0.183 738 -1.34 -0.619
Degrees-of-freedom method: kenward-roger
Confidence level used: 0.95
18.2.1.3 Expected Points Added
In terms of expected points added (EPA) from passing plays, when not controlling for player age and experience, Quarterbacks performed better in their contract year. However, when controlling for player age and experience, Quarterbacks did not perform significantly better in their contract year.
Code
Linear mixed model fit by REML. t-tests use Satterthwaite's method [
lmerModLmerTest]
Formula: epa_pass ~ contractYear + (1 | player_id)
Data: nfl_contractYearQBR_seasonal
Control: lmerControl(optimizer = "bobyqa")
REML criterion at convergence: 5056.5
Scaled residuals:
Min 1Q Median 3Q Max
-3.0398 -0.4989 0.0322 0.5476 4.4112
Random effects:
Groups Name Variance Std.Dev.
player_id (Intercept) 2.527 1.590
Residual 2.974 1.724
Number of obs: 1192, groups: player_id, 274
Fixed effects:
Estimate Std. Error df t value Pr(>|t|)
(Intercept) 1.1346 0.1174 256.0275 9.662 <2e-16 ***
contractYearyes 0.3280 0.1385 1037.7967 2.369 0.018 *
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Correlation of Fixed Effects:
(Intr)
contrctYrys -0.207
# R2 for Mixed Models
Conditional R2: 0.461
Marginal R2: 0.003
contractYear emmean SE df lower.CL upper.CL
no 1.13 0.117 284 0.903 1.37
yes 1.46 0.162 727 1.144 1.78
Degrees-of-freedom method: kenward-roger
Confidence level used: 0.95
Code
mixedModelAge_epaPass <- lmerTest::lmer(
epa_pass ~ contractYear + ageCentered20 + ageCentered20Quadratic + years_of_experience + (1 | player_id), # removed random slopes to address convergence issue
data = nfl_contractYearQBR_seasonal,
control = lmerControl(optimizer = "bobyqa")
)
summary(mixedModelAge_epaPass)Linear mixed model fit by REML. t-tests use Satterthwaite's method [
lmerModLmerTest]
Formula: epa_pass ~ contractYear + ageCentered20 + ageCentered20Quadratic +
years_of_experience + (1 | player_id)
Data: nfl_contractYearQBR_seasonal
Control: lmerControl(optimizer = "bobyqa")
REML criterion at convergence: 5024.9
Scaled residuals:
Min 1Q Median 3Q Max
-3.1342 -0.5234 0.0635 0.5388 4.3611
Random effects:
Groups Name Variance Std.Dev.
player_id (Intercept) 2.313 1.521
Residual 2.930 1.712
Number of obs: 1188, groups: player_id, 272
Fixed effects:
Estimate Std. Error df t value Pr(>|t|)
(Intercept) 4.877e-01 2.595e-01 1.036e+03 1.879 0.0605 .
contractYearyes 1.500e-01 1.431e-01 1.063e+03 1.049 0.2945
ageCentered20 -5.822e-02 7.542e-02 7.323e+02 -0.772 0.4404
ageCentered20Quadratic -6.047e-03 2.393e-03 1.104e+03 -2.527 0.0116 *
years_of_experience 2.656e-01 6.555e-02 4.552e+02 4.052 5.97e-05 ***
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Correlation of Fixed Effects:
(Intr) cntrcY agCn20 agC20Q
contrctYrys 0.059
ageCentrd20 -0.724 -0.063
agCntrd20Qd 0.734 0.043 -0.579
yrs_f_xprnc 0.187 -0.029 -0.727 -0.100
# R2 for Mixed Models
Conditional R2: 0.462
Marginal R2: 0.038
contractYear emmean SE df lower.CL upper.CL
no 1.30 0.117 284 1.07 1.53
yes 1.45 0.161 727 1.13 1.76
Degrees-of-freedom method: kenward-roger
Confidence level used: 0.95
18.2.1.4 Fantasy Points
In terms of fantasy points, Quarterbacks performed significantly worse in their contract year, even controlling for player age and experience.
Code
Linear mixed model fit by REML. t-tests use Satterthwaite's method [
lmerModLmerTest]
Formula: fantasyPoints ~ contractYear + (1 | player_id)
Data: nfl_contractYearQBR_seasonal
Control: lmerControl(optimizer = "bobyqa")
REML criterion at convergence: 14070.4
Scaled residuals:
Min 1Q Median 3Q Max
-3.7607 -0.5646 -0.0765 0.6277 2.7171
Random effects:
Groups Name Variance Std.Dev.
player_id (Intercept) 6117 78.21
Residual 5549 74.49
Number of obs: 1192, groups: player_id, 274
Fixed effects:
Estimate Std. Error df t value Pr(>|t|)
(Intercept) 112.435 5.573 316.134 20.175 < 2e-16 ***
contractYearyes -32.921 6.021 1053.698 -5.468 5.68e-08 ***
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Correlation of Fixed Effects:
(Intr)
contrctYrys -0.188
# R2 for Mixed Models
Conditional R2: 0.531
Marginal R2: 0.015
contractYear emmean SE df lower.CL upper.CL
no 112.4 5.57 283 101 123
yes 79.5 7.40 676 65 94
Degrees-of-freedom method: kenward-roger
Confidence level used: 0.95
Code
mixedModelAge_fantasyPtsPass <- lmerTest::lmer(
fantasyPoints ~ contractYear + ageCentered20 + ageCentered20Quadratic + years_of_experience + (1 | player_id), # removed random slopes to address convergence issue
data = nfl_contractYearQBR_seasonal,
control = lmerControl(optimizer = "bobyqa")
)
summary(mixedModelAge_fantasyPtsPass)Linear mixed model fit by REML. t-tests use Satterthwaite's method [
lmerModLmerTest]
Formula:
fantasyPoints ~ contractYear + ageCentered20 + ageCentered20Quadratic +
years_of_experience + (1 | player_id)
Data: nfl_contractYearQBR_seasonal
Control: lmerControl(optimizer = "bobyqa")
REML criterion at convergence: 13972.5
Scaled residuals:
Min 1Q Median 3Q Max
-3.8688 -0.5757 -0.0819 0.6272 2.5759
Random effects:
Groups Name Variance Std.Dev.
player_id (Intercept) 5940 77.07
Residual 5337 73.05
Number of obs: 1188, groups: player_id, 272
Fixed effects:
Estimate Std. Error df t value Pr(>|t|)
(Intercept) 140.3003 11.6134 1042.8569 12.081 < 2e-16 ***
contractYearyes -26.1750 6.1694 1064.3229 -4.243 2.40e-05 ***
ageCentered20 -14.5261 3.4373 824.1128 -4.226 2.64e-05 ***
ageCentered20Quadratic -0.1617 0.1035 1095.0343 -1.562 0.119
years_of_experience 16.1450 3.0537 576.8851 5.287 1.77e-07 ***
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Correlation of Fixed Effects:
(Intr) cntrcY agCn20 agC20Q
contrctYrys 0.061
ageCentrd20 -0.710 -0.062
agCntrd20Qd 0.709 0.039 -0.550
yrs_f_xprnc 0.220 -0.024 -0.757 -0.093
# R2 for Mixed Models
Conditional R2: 0.561
Marginal R2: 0.073
contractYear emmean SE df lower.CL upper.CL
no 113.9 5.64 287 102.8 125
yes 87.7 7.37 662 73.2 102
Degrees-of-freedom method: kenward-roger
Confidence level used: 0.95
18.2.2 RB
18.2.2.1 Yards Per Carry
In terms of yards per carry (YPC), Running Backs did not perform significantly better in their contract year.
Code
Linear mixed model fit by REML. t-tests use Satterthwaite's method [
lmerModLmerTest]
Formula: ypc ~ contractYear + (1 | player_id)
Data: player_statsContractsRB_seasonal
Control: lmerControl(optimizer = "bobyqa")
REML criterion at convergence: 6749.2
Scaled residuals:
Min 1Q Median 3Q Max
-8.0616 -0.3988 0.0101 0.4104 15.1586
Random effects:
Groups Name Variance Std.Dev.
player_id (Intercept) 0.4509 0.6715
Residual 1.8445 1.3581
Number of obs: 1865, groups: player_id, 558
Fixed effects:
Estimate Std. Error df t value Pr(>|t|)
(Intercept) 3.906e+00 4.865e-02 5.636e+02 80.281 <2e-16 ***
contractYearyes 5.948e-03 7.699e-02 1.809e+03 0.077 0.938
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Correlation of Fixed Effects:
(Intr)
contrctYrys -0.389
# R2 for Mixed Models
Conditional R2: 0.196
Marginal R2: 0.000
contractYear emmean SE df lower.CL upper.CL
no 3.91 0.0487 673 3.81 4.00
yes 3.91 0.0734 1304 3.77 4.06
Degrees-of-freedom method: kenward-roger
Confidence level used: 0.95
Code
Linear mixed model fit by REML. t-tests use Satterthwaite's method [
lmerModLmerTest]
Formula: ypc ~ contractYear + ageCentered20 + ageCentered20Quadratic +
years_of_experience + (1 + ageCentered20 | player_id)
Data: player_statsContractsRB_seasonal
Control: lmerControl(optimizer = "bobyqa")
REML criterion at convergence: 6733.3
Scaled residuals:
Min 1Q Median 3Q Max
-7.8722 -0.3818 -0.0044 0.4022 14.5651
Random effects:
Groups Name Variance Std.Dev. Corr
player_id (Intercept) 0.34352 0.5861
ageCentered20 0.01178 0.1085 -0.39
Residual 1.76996 1.3304
Number of obs: 1865, groups: player_id, 558
Fixed effects:
Estimate Std. Error df t value Pr(>|t|)
(Intercept) 4.114e+00 1.610e-01 7.960e+02 25.545 <2e-16 ***
contractYearyes 8.517e-02 8.381e-02 1.737e+03 1.016 0.3097
ageCentered20 -4.103e-02 5.577e-02 8.521e+02 -0.736 0.4621
ageCentered20Quadratic -6.475e-03 4.070e-03 4.695e+02 -1.591 0.1122
years_of_experience 6.152e-02 3.672e-02 5.558e+02 1.675 0.0944 .
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Correlation of Fixed Effects:
(Intr) cntrcY agCn20 agC20Q
contrctYrys 0.165
ageCentrd20 -0.867 -0.165
agCntrd20Qd 0.814 0.153 -0.803
yrs_f_xprnc -0.071 -0.136 -0.301 -0.246
# R2 for Mixed Models
Conditional R2: 0.252
Marginal R2: 0.022
contractYear emmean SE df lower.CL upper.CL
no 3.87 0.0520 577 3.77 3.97
yes 3.95 0.0768 1300 3.80 4.11
Degrees-of-freedom method: kenward-roger
Confidence level used: 0.95
18.2.2.2 Expected Points Added
In terms of expected points added (EPA) from rushing plays, Running Backs did not perform significantly better in their contract year.
Code
Linear mixed model fit by REML. t-tests use Satterthwaite's method [
lmerModLmerTest]
Formula: rushing_epa ~ contractYear + (1 | player_id)
Data: player_statsContractsRB_seasonal
Control: lmerControl(optimizer = "bobyqa")
REML criterion at convergence: 5373.8
Scaled residuals:
Min 1Q Median 3Q Max
-4.7029 -0.5050 0.0776 0.5840 3.4426
Random effects:
Groups Name Variance Std.Dev.
player_id (Intercept) 0.1040 0.3225
Residual 0.9523 0.9759
Number of obs: 1865, groups: player_id, 558
Fixed effects:
Estimate Std. Error df t value Pr(>|t|)
(Intercept) -0.64807 0.03080 694.42627 -21.044 <2e-16 ***
contractYearyes 0.04345 0.05353 1862.57554 0.812 0.417
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Correlation of Fixed Effects:
(Intr)
contrctYrys -0.445
# R2 for Mixed Models
Conditional R2: 0.099
Marginal R2: 0.000
contractYear emmean SE df lower.CL upper.CL
no -0.648 0.0308 690 -0.709 -0.588
yes -0.605 0.0485 1229 -0.700 -0.509
Degrees-of-freedom method: kenward-roger
Confidence level used: 0.95
Code
Linear mixed model fit by REML. t-tests use Satterthwaite's method [
lmerModLmerTest]
Formula: rushing_epa ~ contractYear + ageCentered20 + ageCentered20Quadratic +
years_of_experience + (1 + ageCentered20 | player_id)
Data: player_statsContractsRB_seasonal
Control: lmerControl(optimizer = "bobyqa")
REML criterion at convergence: 5387.9
Scaled residuals:
Min 1Q Median 3Q Max
-4.7451 -0.5008 0.0678 0.5769 3.4337
Random effects:
Groups Name Variance Std.Dev. Corr
player_id (Intercept) 0.243750 0.49371
ageCentered20 0.003432 0.05858 -0.77
Residual 0.930638 0.96470
Number of obs: 1865, groups: player_id, 558
Fixed effects:
Estimate Std. Error df t value Pr(>|t|)
(Intercept) -6.789e-01 1.147e-01 4.841e+02 -5.917 6.22e-09 ***
contractYearyes 7.143e-02 5.732e-02 1.647e+03 1.246 0.213
ageCentered20 4.053e-02 3.825e-02 4.933e+02 1.060 0.290
ageCentered20Quadratic -2.185e-03 2.710e-03 2.656e+02 -0.806 0.421
years_of_experience -2.905e-02 2.278e-02 5.775e+02 -1.275 0.203
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Correlation of Fixed Effects:
(Intr) cntrcY agCn20 agC20Q
contrctYrys 0.164
ageCentrd20 -0.882 -0.188
agCntrd20Qd 0.827 0.182 -0.835
yrs_f_xprnc -0.060 -0.129 -0.278 -0.226
# R2 for Mixed Models
Conditional R2: 0.122
Marginal R2: 0.004
contractYear emmean SE df lower.CL upper.CL
no -0.658 0.0319 599 -0.720 -0.595
yes -0.586 0.0505 1251 -0.685 -0.487
Degrees-of-freedom method: kenward-roger
Confidence level used: 0.95
18.2.2.3 Fantasy Points
In terms of fantasy points, Running Backs performed significantly worse in their contract year, even controlling for player age and experience.
Code
Linear mixed model fit by REML. t-tests use Satterthwaite's method [
lmerModLmerTest]
Formula: fantasyPoints ~ contractYear + (1 | player_id)
Data: player_statsContractsRB_seasonal
Control: lmerControl(optimizer = "bobyqa")
REML criterion at convergence: 22311.1
Scaled residuals:
Min 1Q Median 3Q Max
-3.4809 -0.4980 -0.1645 0.4193 3.9107
Random effects:
Groups Name Variance Std.Dev.
player_id (Intercept) 3683 60.69
Residual 2990 54.68
Number of obs: 1979, groups: player_id, 575
Fixed effects:
Estimate Std. Error df t value Pr(>|t|)
(Intercept) 82.021 3.016 688.292 27.195 < 2e-16 ***
contractYearyes -14.658 3.289 1704.577 -4.457 8.85e-06 ***
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Correlation of Fixed Effects:
(Intr)
contrctYrys -0.236
# R2 for Mixed Models
Conditional R2: 0.555
Marginal R2: 0.006
contractYear emmean SE df lower.CL upper.CL
no 82.0 3.02 627 76.1 87.9
yes 67.4 3.90 1264 59.7 75.0
Degrees-of-freedom method: kenward-roger
Confidence level used: 0.95
Code
Linear mixed model fit by REML. t-tests use Satterthwaite's method [
lmerModLmerTest]
Formula:
fantasyPoints ~ contractYear + ageCentered20 + ageCentered20Quadratic +
years_of_experience + (1 + ageCentered20 | player_id)
Data: player_statsContractsRB_seasonal
Control: lmerControl(optimizer = "bobyqa")
REML criterion at convergence: 22151.3
Scaled residuals:
Min 1Q Median 3Q Max
-3.9394 -0.4940 -0.1411 0.4195 3.6298
Random effects:
Groups Name Variance Std.Dev. Corr
player_id (Intercept) 6347.82 79.673
ageCentered20 54.47 7.381 -0.74
Residual 2607.45 51.063
Number of obs: 1979, groups: player_id, 575
Fixed effects:
Estimate Std. Error df t value Pr(>|t|)
(Intercept) 72.1729 8.1190 785.9451 8.889 < 2e-16 ***
contractYearyes -12.5928 3.4473 1689.6301 -3.653 0.000267 ***
ageCentered20 -2.7877 2.8056 1051.6854 -0.994 0.320636
ageCentered20Quadratic -1.2008 0.1696 587.6050 -7.080 4.12e-12 ***
years_of_experience 18.3685 2.0084 712.1883 9.146 < 2e-16 ***
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Correlation of Fixed Effects:
(Intr) cntrcY agCn20 agC20Q
contrctYrys 0.168
ageCentrd20 -0.846 -0.150
agCntrd20Qd 0.720 0.171 -0.727
yrs_f_xprnc 0.203 -0.116 -0.545 -0.109
# R2 for Mixed Models
Conditional R2: 0.618
Marginal R2: 0.103
contractYear emmean SE df lower.CL upper.CL
no 84.0 2.96 636 78.2 89.8
yes 71.4 3.80 1271 64.0 78.9
Degrees-of-freedom method: kenward-roger
Confidence level used: 0.95
18.2.3 WR/TE
Code
18.2.3.1 Receiving Yards
In terms of receiving yards, Wide Receivers/Tight Ends performed significantly worse in their contract year, even controlling for player age and experience.
Code
Linear mixed model fit by REML. t-tests use Satterthwaite's method [
lmerModLmerTest]
Formula: receiving_yards ~ contractYear + (1 | player_id)
Data: player_statsContractsWRTE_seasonal
Control: lmerControl(optimizer = "bobyqa")
REML criterion at convergence: 35228.1
Scaled residuals:
Min 1Q Median 3Q Max
-4.8693 -0.5238 -0.1139 0.5045 4.5870
Random effects:
Groups Name Variance Std.Dev.
player_id (Intercept) 274.9 16.58
Residual 180.4 13.43
Number of obs: 4144, groups: player_id, 1147
Fixed effects:
Estimate Std. Error df t value Pr(>|t|)
(Intercept) 24.9219 0.5696 1378.0369 43.755 < 2e-16 ***
contractYearyes -4.2902 0.5270 3522.5864 -8.141 5.38e-16 ***
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Correlation of Fixed Effects:
(Intr)
contrctYrys -0.237
# R2 for Mixed Models
Conditional R2: 0.607
Marginal R2: 0.008
contractYear emmean SE df lower.CL upper.CL
no 24.9 0.570 1259 23.8 26
yes 20.6 0.678 2140 19.3 22
Degrees-of-freedom method: kenward-roger
Confidence level used: 0.95
Code
mixedModelAge_receivingYards <- lmerTest::lmer(
receiving_yards ~ contractYear + ageCentered20 + ageCentered20Quadratic + years_of_experience + (1 + ageCentered20 | player_id),
data = player_statsContractsWRTE_seasonal,
control = lmerControl(optimizer = "bobyqa")
)
summary(mixedModelAge_receivingYards)Linear mixed model fit by REML. t-tests use Satterthwaite's method [
lmerModLmerTest]
Formula:
receiving_yards ~ contractYear + ageCentered20 + ageCentered20Quadratic +
years_of_experience + (1 + ageCentered20 | player_id)
Data: player_statsContractsWRTE_seasonal
Control: lmerControl(optimizer = "bobyqa")
REML criterion at convergence: 34689.1
Scaled residuals:
Min 1Q Median 3Q Max
-2.9367 -0.5202 -0.0991 0.4775 3.9966
Random effects:
Groups Name Variance Std.Dev. Corr
player_id (Intercept) 511.936 22.626
ageCentered20 5.859 2.421 -0.70
Residual 134.019 11.577
Number of obs: 4144, groups: player_id, 1147
Fixed effects:
Estimate Std. Error df t value Pr(>|t|)
(Intercept) 14.2374 1.4562 1622.8865 9.777 < 2e-16 ***
contractYearyes -3.1918 0.5156 3280.7026 -6.190 6.76e-10 ***
ageCentered20 1.5577 0.5061 2311.5831 3.078 0.00211 **
ageCentered20Quadratic -0.4722 0.0256 1776.3575 -18.447 < 2e-16 ***
years_of_experience 4.8877 0.4008 1398.6380 12.196 < 2e-16 ***
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Correlation of Fixed Effects:
(Intr) cntrcY agCn20 agC20Q
contrctYrys 0.117
ageCentrd20 -0.814 -0.136
agCntrd20Qd 0.677 0.073 -0.638
yrs_f_xprnc 0.271 0.008 -0.667 -0.075
# R2 for Mixed Models
Conditional R2: 0.747
Marginal R2: 0.154
contractYear emmean SE df lower.CL upper.CL
no 24.2 0.588 1279 23.0 25.3
yes 21.0 0.669 1975 19.7 22.3
Degrees-of-freedom method: kenward-roger
Confidence level used: 0.95
18.2.3.2 Expected Points Added
In terms of expected points added (EPA) from receiving plays, Wide Receivers/Tight Ends performed significantly worse in their contract year, even controlling for player age and experience.
Code
Linear mixed model fit by REML. t-tests use Satterthwaite's method [
lmerModLmerTest]
Formula: receiving_epa ~ contractYear + (1 | player_id)
Data: player_statsContractsWRTE_seasonal
Control: lmerControl(optimizer = "bobyqa")
REML criterion at convergence: 13543.6
Scaled residuals:
Min 1Q Median 3Q Max
-5.6074 -0.5669 -0.0418 0.5273 3.9027
Random effects:
Groups Name Variance Std.Dev.
player_id (Intercept) 0.541 0.7356
Residual 1.300 1.1401
Number of obs: 4064, groups: player_id, 1127
Fixed effects:
Estimate Std. Error df t value Pr(>|t|)
(Intercept) 0.67104 0.03231 1526.48873 20.772 < 2e-16 ***
contractYearyes -0.16380 0.04314 3866.10064 -3.796 0.000149 ***
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Correlation of Fixed Effects:
(Intr)
contrctYrys -0.364
# R2 for Mixed Models
Conditional R2: 0.296
Marginal R2: 0.003
contractYear emmean SE df lower.CL upper.CL
no 0.671 0.0323 1345 0.608 0.734
yes 0.507 0.0435 2562 0.422 0.593
Degrees-of-freedom method: kenward-roger
Confidence level used: 0.95
Code
Linear mixed model fit by REML. t-tests use Satterthwaite's method [
lmerModLmerTest]
Formula:
receiving_epa ~ contractYear + ageCentered20 + ageCentered20Quadratic +
years_of_experience + (1 + ageCentered20 | player_id)
Data: player_statsContractsWRTE_seasonal
Control: lmerControl(optimizer = "bobyqa")
REML criterion at convergence: 13486.4
Scaled residuals:
Min 1Q Median 3Q Max
-5.7547 -0.5629 -0.0370 0.5211 3.9493
Random effects:
Groups Name Variance Std.Dev. Corr
player_id (Intercept) 0.953838 0.97665
ageCentered20 0.006845 0.08273 -0.70
Residual 1.239792 1.11346
Number of obs: 4064, groups: player_id, 1127
Fixed effects:
Estimate Std. Error df t value Pr(>|t|)
(Intercept) 3.556e-01 9.968e-02 1.234e+03 3.567 0.000375 ***
contractYearyes -1.677e-01 4.556e-02 3.766e+03 -3.681 0.000235 ***
ageCentered20 2.095e-02 3.271e-02 1.417e+03 0.641 0.521900
ageCentered20Quadratic -1.125e-02 1.853e-03 5.724e+02 -6.071 2.32e-09 ***
years_of_experience 1.572e-01 2.304e-02 1.294e+03 6.821 1.38e-11 ***
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Correlation of Fixed Effects:
(Intr) cntrcY agCn20 agC20Q
contrctYrys 0.139
ageCentrd20 -0.844 -0.191
agCntrd20Qd 0.777 0.122 -0.737
yrs_f_xprnc 0.110 0.011 -0.506 -0.149
# R2 for Mixed Models
Conditional R2: 0.335
Marginal R2: 0.029
contractYear emmean SE df lower.CL upper.CL
no 0.689 0.0330 1272 0.624 0.754
yes 0.521 0.0441 2592 0.435 0.608
Degrees-of-freedom method: kenward-roger
Confidence level used: 0.95
18.2.3.3 Fantasy Points
In terms of fantasy points, Wide Receivers/Tight Ends performed significantly worse in their contract year, even controlling for player age and experience.
Code
Linear mixed model fit by REML. t-tests use Satterthwaite's method [
lmerModLmerTest]
Formula: fantasyPoints ~ contractYear + (1 | player_id)
Data: player_statsContractsWRTE_seasonal
Control: lmerControl(optimizer = "bobyqa")
REML criterion at convergence: 45799.7
Scaled residuals:
Min 1Q Median 3Q Max
-3.2650 -0.5274 -0.1468 0.4768 4.8082
Random effects:
Groups Name Variance Std.Dev.
player_id (Intercept) 2873 53.60
Residual 2447 49.47
Number of obs: 4144, groups: player_id, 1147
Fixed effects:
Estimate Std. Error df t value Pr(>|t|)
(Intercept) 75.622 1.906 1425.543 39.666 < 2e-16 ***
contractYearyes -14.811 1.925 3613.082 -7.693 1.85e-14 ***
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Correlation of Fixed Effects:
(Intr)
contrctYrys -0.262
# R2 for Mixed Models
Conditional R2: 0.544
Marginal R2: 0.009
contractYear emmean SE df lower.CL upper.CL
no 75.6 1.91 1279 71.9 79.4
yes 60.8 2.33 2271 56.2 65.4
Degrees-of-freedom method: kenward-roger
Confidence level used: 0.95
Code
mixedModelAge_fantasyPtsReceiving <- lmerTest::lmer(
fantasyPoints ~ contractYear + ageCentered20 + ageCentered20Quadratic + years_of_experience + (1 + ageCentered20 | player_id),
data = player_statsContractsWRTE_seasonal,
control = lmerControl(optimizer = "bobyqa")
)
summary(mixedModelAge_fantasyPtsReceiving)Linear mixed model fit by REML. t-tests use Satterthwaite's method [
lmerModLmerTest]
Formula:
fantasyPoints ~ contractYear + ageCentered20 + ageCentered20Quadratic +
years_of_experience + (1 + ageCentered20 | player_id)
Data: player_statsContractsWRTE_seasonal
Control: lmerControl(optimizer = "bobyqa")
REML criterion at convergence: 45348.3
Scaled residuals:
Min 1Q Median 3Q Max
-3.0392 -0.4987 -0.1230 0.4545 5.1688
Random effects:
Groups Name Variance Std.Dev. Corr
player_id (Intercept) 5408.09 73.540
ageCentered20 60.18 7.757 -0.72
Residual 1940.67 44.053
Number of obs: 4144, groups: player_id, 1147
Fixed effects:
Estimate Std. Error df t value Pr(>|t|)
(Intercept) 43.27625 5.10293 1574.12949 8.481 < 2e-16 ***
contractYearyes -11.00339 1.92378 3440.86901 -5.720 1.16e-08 ***
ageCentered20 3.44886 1.74445 2260.96144 1.977 0.0482 *
ageCentered20Quadratic -1.48995 0.09213 1489.09822 -16.173 < 2e-16 ***
years_of_experience 17.18091 1.33446 1360.63480 12.875 < 2e-16 ***
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Correlation of Fixed Effects:
(Intr) cntrcY agCn20 agC20Q
contrctYrys 0.123
ageCentrd20 -0.824 -0.149
agCntrd20Qd 0.705 0.084 -0.669
yrs_f_xprnc 0.232 0.008 -0.625 -0.090
# R2 for Mixed Models
Conditional R2: 0.677
Marginal R2: 0.147
contractYear emmean SE df lower.CL upper.CL
no 73.8 1.95 1281 70.0 77.6
yes 62.8 2.29 2150 58.3 67.3
Degrees-of-freedom method: kenward-roger
Confidence level used: 0.95
18.2.4 QB/RB/WR/TE
Code
player_statsContractsQBRBWRTE_seasonal <- player_statsContracts_seasonal %>%
dplyr::filter(
position_group %in% c("QB","RB","WR","TE"),
games >= 5, # keep only player-season combinations in which QBs played at least 5 games
season >= 2011) # keep only seasons since 2011 (when most contract data are available)18.2.4.1 Fantasy Points
In terms of fantasy points, Quarterbacks/Running Backs/Wide Receivers/Tight Ends performed significantly worse in their contract year, even controlling for player age and experience.
Code
Linear mixed model fit by REML. t-tests use Satterthwaite's method [
lmerModLmerTest]
Formula: fantasyPoints ~ contractYear + position_group + (1 | player_id)
Data: player_statsContractsQBRBWRTE_seasonal
Control: lmerControl(optimizer = "bobyqa")
REML criterion at convergence: 76673
Scaled residuals:
Min 1Q Median 3Q Max
-3.8195 -0.5134 -0.1384 0.4742 4.3861
Random effects:
Groups Name Variance Std.Dev.
player_id (Intercept) 3360 57.97
Residual 2922 54.06
Number of obs: 6831, groups: player_id, 1899
Fixed effects:
Estimate Std. Error df t value Pr(>|t|)
(Intercept) 151.285 5.114 2041.058 29.58 <2e-16 ***
contractYearyes -17.117 1.672 5950.928 -10.24 <2e-16 ***
position_groupRB -68.481 5.827 2021.976 -11.75 <2e-16 ***
position_groupTE -90.000 6.150 2008.195 -14.63 <2e-16 ***
position_groupWR -67.387 5.655 2020.986 -11.92 <2e-16 ***
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Correlation of Fixed Effects:
(Intr) cntrcY pst_RB pst_TE
contrctYrys -0.080
postn_grpRB -0.873 0.008
postn_grpTE -0.826 -0.007 0.725
postn_grpWR -0.898 -0.003 0.789 0.747
# R2 for Mixed Models
Conditional R2: 0.578
Marginal R2: 0.093
contractYear emmean SE df lower.CL upper.CL
no 94.8 1.85 2018 91.2 98.4
yes 77.7 2.20 3417 73.4 82.0
Results are averaged over the levels of: position_group
Degrees-of-freedom method: kenward-roger
Confidence level used: 0.95
Code
mixedModelAge_fantasyPts <- lmerTest::lmer(
fantasyPoints ~ contractYear + position_group + ageCentered20 + ageCentered20Quadratic + years_of_experience + (1 + ageCentered20 | player_id),
data = player_statsContractsQBRBWRTE_seasonal,
control = lmerControl(optimizer = "bobyqa")
)
summary(mixedModelAge_fantasyPts)Linear mixed model fit by REML. t-tests use Satterthwaite's method [
lmerModLmerTest]
Formula: fantasyPoints ~ contractYear + position_group + ageCentered20 +
ageCentered20Quadratic + years_of_experience + (1 + ageCentered20 |
player_id)
Data: player_statsContractsQBRBWRTE_seasonal
Control: lmerControl(optimizer = "bobyqa")
REML criterion at convergence: 76069.7
Scaled residuals:
Min 1Q Median 3Q Max
-4.1103 -0.4994 -0.1206 0.4520 4.6667
Random effects:
Groups Name Variance Std.Dev. Corr
player_id (Intercept) 5812.12 76.24
ageCentered20 64.33 8.02 -0.71
Residual 2435.53 49.35
Number of obs: 6831, groups: player_id, 1899
Fixed effects:
Estimate Std. Error df t value Pr(>|t|)
(Intercept) 127.04591 6.36402 3026.89259 19.963 < 2e-16 ***
contractYearyes -12.78391 1.70337 5804.11965 -7.505 7.07e-14 ***
position_groupRB -61.12324 5.68295 1938.46685 -10.756 < 2e-16 ***
position_groupTE -83.88260 5.96603 1885.35762 -14.060 < 2e-16 ***
position_groupWR -61.59645 5.51095 1925.81620 -11.177 < 2e-16 ***
ageCentered20 -1.77536 1.38388 3444.05994 -1.283 0.2
ageCentered20Quadratic -1.17681 0.07018 2010.75855 -16.769 < 2e-16 ***
years_of_experience 18.21178 1.09693 2327.31784 16.602 < 2e-16 ***
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Correlation of Fixed Effects:
(Intr) cntrcY pst_RB pst_TE pst_WR agCn20 agC20Q
contrctYrys 0.107
postn_grpRB -0.707 -0.032
postn_grpTE -0.653 -0.029 0.733
postn_grpWR -0.731 -0.041 0.794 0.754
ageCentrd20 -0.504 -0.134 -0.026 -0.042 -0.009
agCntrd20Qd 0.460 0.091 -0.025 -0.009 -0.028 -0.642
yrs_f_xprnc 0.077 -0.025 0.092 0.084 0.071 -0.637 -0.103
# R2 for Mixed Models
Conditional R2: 0.670
Marginal R2: 0.188
contractYear emmean SE df lower.CL upper.CL
no 92.1 1.84 1956 88.5 95.7
yes 79.3 2.16 3364 75.1 83.5
Results are averaged over the levels of: position_group
Degrees-of-freedom method: kenward-roger
Confidence level used: 0.95
18.3 Conclusion
There is a widely held belief that NFL players perform better in the last year of the contract because they are motivated to gain another contract. There is some evidence in the NBA and MLB that players tend to perform better in their contract year. We evaluated this possibility among NFL players who were Quarterbacks, Running Backs, Wide Receivers, or Tight Ends. We evaluated a wide range of performance indexes, including Quarterback Rating, yards per carry, points added, expected points added, receiving yards, and fantasy points. None of the positions showed significantly better performance in their contract year for any of the performance indexes. By contrast, if anything, players tended to perform more poorly during their contract year, as operationalized by fantasy points, receiving yards (WR/TE), and EPA from receiving plays (WR/TE), even when controlling for player and age experience. In sum, we did not find evidence in support of the contract year hypothesis and consider this myth debunked. However, we are open to this possibility being reexamined in new ways or with additional performance metrics.
18.4 Session Info
R version 4.5.3 (2026-03-11)
Platform: x86_64-pc-linux-gnu
Running under: Ubuntu 24.04.3 LTS
Matrix products: default
BLAS: /usr/lib/x86_64-linux-gnu/openblas-pthread/libblas.so.3
LAPACK: /usr/lib/x86_64-linux-gnu/openblas-pthread/libopenblasp-r0.3.26.so; LAPACK version 3.12.0
locale:
[1] LC_CTYPE=C.UTF-8 LC_NUMERIC=C LC_TIME=C.UTF-8
[4] LC_COLLATE=C.UTF-8 LC_MONETARY=C.UTF-8 LC_MESSAGES=C.UTF-8
[7] LC_PAPER=C.UTF-8 LC_NAME=C LC_ADDRESS=C
[10] LC_TELEPHONE=C LC_MEASUREMENT=C.UTF-8 LC_IDENTIFICATION=C
time zone: UTC
tzcode source: system (glibc)
attached base packages:
[1] stats graphics grDevices utils datasets methods base
other attached packages:
[1] lubridate_1.9.5 forcats_1.0.1 stringr_1.6.0 dplyr_1.2.0
[5] purrr_1.2.1 readr_2.2.0 tidyr_1.3.2 tibble_3.3.1
[9] ggplot2_4.0.2 tidyverse_2.0.0 emmeans_2.0.2 performance_0.16.0
[13] lmerTest_3.2-1 lme4_2.0-1 Matrix_1.7-4 nflreadr_1.5.0
[17] petersenlab_1.2.3
loaded via a namespace (and not attached):
[1] Rdpack_2.6.6 DBI_1.3.0 mnormt_2.1.2
[4] gridExtra_2.3 sandwich_3.1-1 rlang_1.1.7
[7] magrittr_2.0.4 multcomp_1.4-30 otel_0.2.0
[10] compiler_4.5.3 vctrs_0.7.1 reshape2_1.4.5
[13] quadprog_1.5-8 pkgconfig_2.0.3 fastmap_1.2.0
[16] backports_1.5.0 pbivnorm_0.6.0 rmarkdown_2.30
[19] tzdb_0.5.0 nloptr_2.2.1 xfun_0.57
[22] cachem_1.1.0 jsonlite_2.0.0 psych_2.6.1
[25] broom_1.0.12 parallel_4.5.3 lavaan_0.6-21
[28] cluster_2.1.8.2 R6_2.6.1 stringi_1.8.7
[31] RColorBrewer_1.1-3 boot_1.3-32 rpart_4.1.24
[34] numDeriv_2016.8-1.1 estimability_1.5.1 Rcpp_1.1.1
[37] knitr_1.51 zoo_1.8-15 base64enc_0.1-6
[40] splines_4.5.3 nnet_7.3-20 timechange_0.4.0
[43] tidyselect_1.2.1 rstudioapi_0.18.0 yaml_2.3.12
[46] codetools_0.2-20 lattice_0.22-9 plyr_1.8.9
[49] withr_3.0.2 S7_0.2.1 coda_0.19-4.1
[52] evaluate_1.0.5 foreign_0.8-91 survival_3.8-6
[55] pillar_1.11.1 checkmate_2.3.4 stats4_4.5.3
[58] reformulas_0.4.4 insight_1.4.6 generics_0.1.4
[61] mix_1.0-13 hms_1.1.4 scales_1.4.0
[64] minqa_1.2.8 xtable_1.8-8 glue_1.8.0
[67] Hmisc_5.2-5 tools_4.5.3 data.table_1.18.2.1
[70] mvtnorm_1.3-6 grid_4.5.3 mitools_2.4
[73] rbibutils_2.4.1 colorspace_2.1-2 nlme_3.1-168
[76] htmlTable_2.4.3 Formula_1.2-5 cli_3.6.5
[79] viridisLite_0.4.3 gtable_0.3.6 digest_0.6.39
[82] pbkrtest_0.5.5 TH.data_1.1-5 htmlwidgets_1.6.4
[85] farver_2.1.2 memoise_2.0.1 htmltools_0.5.9
[88] lifecycle_1.0.5 MASS_7.3-65