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 data
18.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: 9438.2
Scaled residuals:
Min 1Q Median 3Q Max
-3.2357 -0.5458 0.0707 0.5740 3.2417
Random effects:
Groups Name Variance Std.Dev.
player_id (Intercept) 109.4 10.46
Residual 199.0 14.11
Number of obs: 1127, groups: player_id, 262
Fixed effects:
Estimate Std. Error df t value Pr(>|t|)
(Intercept) 44.4546 0.8505 239.3701 52.270 <2e-16 ***
contractYearyes -0.2558 1.1552 1010.5146 -0.221 0.825
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Correlation of Fixed Effects:
(Intr)
contrctYrys -0.244
# R2 for Mixed Models
Conditional R2: 0.355
Marginal R2: 0.000
contractYear emmean SE df lower.CL upper.CL
no 44.5 0.851 273 42.8 46.1
yes 44.2 1.260 771 41.7 46.7
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: 9377.5
Scaled residuals:
Min 1Q Median 3Q Max
-3.3590 -0.5087 0.0868 0.5523 3.2740
Random effects:
Groups Name Variance Std.Dev. Corr
player_id (Intercept) 138.3788 11.7635
ageCentered20 0.5743 0.7578 -0.49
Residual 190.8198 13.8138
Number of obs: 1121, groups: player_id, 259
Fixed effects:
Estimate Std. Error df t value Pr(>|t|)
(Intercept) 39.96803 2.24564 197.39593 17.798 < 2e-16 ***
contractYearyes -0.15679 1.19582 1002.68967 -0.131 0.895711
ageCentered20 0.50198 0.64265 274.84098 0.781 0.435414
ageCentered20Quadratic -0.08134 0.02316 125.87449 -3.512 0.000619 ***
years_of_experience 1.11470 0.54597 298.60485 2.042 0.042062 *
---
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.742 -0.062
agCntrd20Qd 0.757 0.056 -0.616
yrs_f_xprnc 0.122 -0.040 -0.672 -0.121
# R2 for Mixed Models
Conditional R2: 0.393
Marginal R2: 0.018
contractYear emmean SE df lower.CL upper.CL
no 44.4 0.89 248 42.7 46.2
yes 44.3 1.28 718 41.8 46.8
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: 5131.2
Scaled residuals:
Min 1Q Median 3Q Max
-4.7207 -0.4956 0.0890 0.5453 4.2919
Random effects:
Groups Name Variance Std.Dev.
player_id (Intercept) 2.542 1.594
Residual 4.277 2.068
Number of obs: 1127, groups: player_id, 262
Fixed effects:
Estimate Std. Error df t value Pr(>|t|)
(Intercept) -0.8204 0.1278 225.7574 -6.419 7.96e-10 ***
contractYearyes -0.1319 0.1698 995.7377 -0.777 0.437
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Correlation of Fixed Effects:
(Intr)
contrctYrys -0.238
# R2 for Mixed Models
Conditional R2: 0.373
Marginal R2: 0.000
contractYear emmean SE df lower.CL upper.CL
no -0.820 0.128 273 -1.07 -0.569
yes -0.952 0.187 760 -1.32 -0.586
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: 5105.9
Scaled residuals:
Min 1Q Median 3Q Max
-4.8843 -0.5004 0.0933 0.5278 4.2890
Random effects:
Groups Name Variance Std.Dev. Corr
player_id (Intercept) 3.76625 1.9407
ageCentered20 0.01592 0.1262 -0.62
Residual 4.10248 2.0255
Number of obs: 1121, groups: player_id, 259
Fixed effects:
Estimate Std. Error df t value Pr(>|t|)
(Intercept) -1.516112 0.341012 190.293484 -4.446 1.49e-05 ***
contractYearyes -0.135518 0.175633 986.952187 -0.772 0.440539
ageCentered20 0.058746 0.096431 276.886585 0.609 0.542891
ageCentered20Quadratic -0.011709 0.003465 130.048786 -3.379 0.000959 ***
years_of_experience 0.189310 0.080823 288.058048 2.342 0.019848 *
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Correlation of Fixed Effects:
(Intr) cntrcY agCn20 agC20Q
contrctYrys 0.058
ageCentrd20 -0.746 -0.063
agCntrd20Qd 0.752 0.060 -0.626
yrs_f_xprnc 0.133 -0.041 -0.674 -0.106
# R2 for Mixed Models
Conditional R2: 0.403
Marginal R2: 0.017
contractYear emmean SE df lower.CL upper.CL
no -0.786 0.131 251 -1.04 -0.527
yes -0.921 0.188 717 -1.29 -0.551
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: 4785.4
Scaled residuals:
Min 1Q Median 3Q Max
-3.0580 -0.5112 0.0380 0.5455 4.4075
Random effects:
Groups Name Variance Std.Dev.
player_id (Intercept) 2.514 1.586
Residual 2.980 1.726
Number of obs: 1127, groups: player_id, 262
Fixed effects:
Estimate Std. Error df t value Pr(>|t|)
(Intercept) 1.1296 0.1199 243.1854 9.418 < 2e-16 ***
contractYearyes 0.3820 0.1431 977.2984 2.670 0.00772 **
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Correlation of Fixed Effects:
(Intr)
contrctYrys -0.211
# R2 for Mixed Models
Conditional R2: 0.460
Marginal R2: 0.004
contractYear emmean SE df lower.CL upper.CL
no 1.13 0.120 272 0.893 1.37
yes 1.51 0.166 703 1.185 1.84
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: 4751.3
Scaled residuals:
Min 1Q Median 3Q Max
-3.1335 -0.5073 0.0557 0.5457 4.3347
Random effects:
Groups Name Variance Std.Dev.
player_id (Intercept) 2.373 1.541
Residual 2.936 1.713
Number of obs: 1121, groups: player_id, 259
Fixed effects:
Estimate Std. Error df t value Pr(>|t|)
(Intercept) 4.747e-01 2.713e-01 9.855e+02 1.750 0.080400 .
contractYearyes 2.031e-01 1.482e-01 1.001e+03 1.370 0.170964
ageCentered20 -4.304e-02 7.955e-02 6.794e+02 -0.541 0.588666
ageCentered20Quadratic -5.726e-03 2.592e-03 1.056e+03 -2.209 0.027363 *
years_of_experience 2.426e-01 7.140e-02 4.137e+02 3.398 0.000744 ***
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Correlation of Fixed Effects:
(Intr) cntrcY agCn20 agC20Q
contrctYrys 0.068
ageCentrd20 -0.718 -0.060
agCntrd20Qd 0.729 0.061 -0.547
yrs_f_xprnc 0.168 -0.045 -0.721 -0.148
# R2 for Mixed Models
Conditional R2: 0.466
Marginal R2: 0.034
contractYear emmean SE df lower.CL upper.CL
no 1.27 0.121 272 1.03 1.51
yes 1.47 0.166 700 1.15 1.80
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: 13299.5
Scaled residuals:
Min 1Q Median 3Q Max
-3.7895 -0.5641 -0.0842 0.6337 2.7383
Random effects:
Groups Name Variance Std.Dev.
player_id (Intercept) 6240 78.99
Residual 5485 74.06
Number of obs: 1127, groups: player_id, 262
Fixed effects:
Estimate Std. Error df t value Pr(>|t|)
(Intercept) 112.130 5.733 299.833 19.557 < 2e-16 ***
contractYearyes -29.924 6.184 990.455 -4.839 1.51e-06 ***
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Correlation of Fixed Effects:
(Intr)
contrctYrys -0.189
# R2 for Mixed Models
Conditional R2: 0.538
Marginal R2: 0.012
contractYear emmean SE df lower.CL upper.CL
no 112.1 5.74 272 100.8 123.4
yes 82.2 7.60 647 67.3 97.1
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: 13186.6
Scaled residuals:
Min 1Q Median 3Q Max
-3.8469 -0.5734 -0.0786 0.6368 2.5779
Random effects:
Groups Name Variance Std.Dev.
player_id (Intercept) 6015 77.55
Residual 5324 72.96
Number of obs: 1121, groups: player_id, 259
Fixed effects:
Estimate Std. Error df t value Pr(>|t|)
(Intercept) 141.6572 12.0931 994.9825 11.714 < 2e-16 ***
contractYearyes -24.5306 6.3788 1003.5089 -3.846 0.000128 ***
ageCentered20 -15.2109 3.6158 767.1802 -4.207 2.90e-05 ***
ageCentered20Quadratic -0.1379 0.1120 1045.5987 -1.230 0.218796
years_of_experience 16.5905 3.3191 525.5976 4.998 7.88e-07 ***
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Correlation of Fixed Effects:
(Intr) cntrcY agCn20 agC20Q
contrctYrys 0.069
ageCentrd20 -0.703 -0.057
agCntrd20Qd 0.702 0.057 -0.514
yrs_f_xprnc 0.200 -0.042 -0.751 -0.146
# R2 for Mixed Models
Conditional R2: 0.562
Marginal R2: 0.066
contractYear emmean SE df lower.CL upper.CL
no 114.0 5.80 274 102.6 125
yes 89.5 7.58 638 74.6 104
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: 6379.2
Scaled residuals:
Min 1Q Median 3Q Max
-7.9730 -0.3966 0.0041 0.4011 14.7698
Random effects:
Groups Name Variance Std.Dev.
player_id (Intercept) 0.5188 0.7203
Residual 1.8697 1.3674
Number of obs: 1748, groups: player_id, 532
Fixed effects:
Estimate Std. Error df t value Pr(>|t|)
(Intercept) 3.906e+00 5.146e-02 5.066e+02 75.90 <2e-16 ***
contractYearyes 9.797e-03 8.165e-02 1.678e+03 0.12 0.905
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Correlation of Fixed Effects:
(Intr)
contrctYrys -0.370
# R2 for Mixed Models
Conditional R2: 0.217
Marginal R2: 0.000
contractYear emmean SE df lower.CL upper.CL
no 3.91 0.0515 631 3.80 4.01
yes 3.92 0.0788 1264 3.76 4.07
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: 6366.7
Scaled residuals:
Min 1Q Median 3Q Max
-7.7819 -0.3857 -0.0069 0.3862 14.2027
Random effects:
Groups Name Variance Std.Dev. Corr
player_id (Intercept) 0.37836 0.6151
ageCentered20 0.01157 0.1075 -0.31
Residual 1.79523 1.3399
Number of obs: 1748, groups: player_id, 532
Fixed effects:
Estimate Std. Error df t value Pr(>|t|)
(Intercept) 4.194e+00 1.696e-01 7.658e+02 24.727 <2e-16 ***
contractYearyes 9.504e-02 8.862e-02 1.609e+03 1.072 0.2837
ageCentered20 -7.278e-02 5.913e-02 8.178e+02 -1.231 0.2187
ageCentered20Quadratic -4.509e-03 4.281e-03 4.368e+02 -1.053 0.2928
years_of_experience 6.632e-02 3.962e-02 5.394e+02 1.674 0.0947 .
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Correlation of Fixed Effects:
(Intr) cntrcY agCn20 agC20Q
contrctYrys 0.158
ageCentrd20 -0.866 -0.153
agCntrd20Qd 0.811 0.138 -0.797
yrs_f_xprnc -0.053 -0.128 -0.320 -0.238
# R2 for Mixed Models
Conditional R2: 0.272
Marginal R2: 0.021
contractYear emmean SE df lower.CL upper.CL
no 3.87 0.0549 551 3.76 3.97
yes 3.96 0.0825 1248 3.80 4.12
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: 5070.9
Scaled residuals:
Min 1Q Median 3Q Max
-4.5468 -0.5055 0.0777 0.5809 3.4198
Random effects:
Groups Name Variance Std.Dev.
player_id (Intercept) 0.1022 0.3197
Residual 0.9735 0.9867
Number of obs: 1748, groups: player_id, 532
Fixed effects:
Estimate Std. Error df t value Pr(>|t|)
(Intercept) -0.64999 0.03175 649.74193 -20.472 <2e-16 ***
contractYearyes 0.03814 0.05655 1745.99999 0.674 0.5
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Correlation of Fixed Effects:
(Intr)
contrctYrys -0.438
# R2 for Mixed Models
Conditional R2: 0.095
Marginal R2: 0.000
contractYear emmean SE df lower.CL upper.CL
no -0.650 0.0318 651 -0.712 -0.588
yes -0.612 0.0514 1167 -0.713 -0.511
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: 5084.9
Scaled residuals:
Min 1Q Median 3Q Max
-4.5730 -0.4972 0.0720 0.5760 3.3878
Random effects:
Groups Name Variance Std.Dev. Corr
player_id (Intercept) 0.190478 0.43644
ageCentered20 0.002303 0.04799 -0.69
Residual 0.956386 0.97795
Number of obs: 1748, groups: player_id, 532
Fixed effects:
Estimate Std. Error df t value Pr(>|t|)
(Intercept) -6.471e-01 1.183e-01 4.179e+02 -5.468 7.84e-08 ***
contractYearyes 7.658e-02 6.042e-02 1.545e+03 1.267 0.2052
ageCentered20 3.829e-02 3.954e-02 4.009e+02 0.968 0.3335
ageCentered20Quadratic -1.627e-03 2.788e-03 2.072e+02 -0.584 0.5600
years_of_experience -4.061e-02 2.405e-02 5.630e+02 -1.689 0.0918 .
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Correlation of Fixed Effects:
(Intr) cntrcY agCn20 agC20Q
contrctYrys 0.156
ageCentrd20 -0.880 -0.176
agCntrd20Qd 0.829 0.169 -0.832
yrs_f_xprnc -0.061 -0.125 -0.284 -0.226
# R2 for Mixed Models
Conditional R2: 0.114
Marginal R2: 0.005
contractYear emmean SE df lower.CL upper.CL
no -0.663 0.0328 571 -0.728 -0.599
yes -0.587 0.0536 1186 -0.692 -0.482
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: 20834.3
Scaled residuals:
Min 1Q Median 3Q Max
-3.1357 -0.5059 -0.1723 0.4153 3.8941
Random effects:
Groups Name Variance Std.Dev.
player_id (Intercept) 3557 59.64
Residual 3044 55.18
Number of obs: 1846, groups: player_id, 549
Fixed effects:
Estimate Std. Error df t value Pr(>|t|)
(Intercept) 81.736 3.056 654.624 26.747 < 2e-16 ***
contractYearyes -14.037 3.476 1592.046 -4.038 5.64e-05 ***
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Correlation of Fixed Effects:
(Intr)
contrctYrys -0.234
# R2 for Mixed Models
Conditional R2: 0.541
Marginal R2: 0.006
contractYear emmean SE df lower.CL upper.CL
no 81.7 3.06 597 75.7 87.7
yes 67.7 4.06 1252 59.7 75.7
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: 20676.1
Scaled residuals:
Min 1Q Median 3Q Max
-3.4826 -0.4970 -0.1465 0.4146 3.6037
Random effects:
Groups Name Variance Std.Dev. Corr
player_id (Intercept) 6477.91 80.49
ageCentered20 58.84 7.67 -0.77
Residual 2644.97 51.43
Number of obs: 1846, groups: player_id, 549
Fixed effects:
Estimate Std. Error df t value Pr(>|t|)
(Intercept) 72.7332 8.6156 711.9018 8.442 < 2e-16 ***
contractYearyes -11.5548 3.6113 1570.7539 -3.200 0.0014 **
ageCentered20 -3.3029 2.9726 950.1386 -1.111 0.2668
ageCentered20Quadratic -1.1888 0.1833 521.2083 -6.487 2.04e-10 ***
years_of_experience 18.8737 2.0589 691.5663 9.167 < 2e-16 ***
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Correlation of Fixed Effects:
(Intr) cntrcY agCn20 agC20Q
contrctYrys 0.159
ageCentrd20 -0.856 -0.141
agCntrd20Qd 0.735 0.160 -0.749
yrs_f_xprnc 0.198 -0.117 -0.528 -0.098
# R2 for Mixed Models
Conditional R2: 0.606
Marginal R2: 0.105
contractYear emmean SE df lower.CL upper.CL
no 83.5 2.97 606 77.7 89.3
yes 71.9 3.93 1256 64.2 79.7
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: 32793.8
Scaled residuals:
Min 1Q Median 3Q Max
-4.8547 -0.5285 -0.1103 0.5071 4.5642
Random effects:
Groups Name Variance Std.Dev.
player_id (Intercept) 280.4 16.74
Residual 182.5 13.51
Number of obs: 3849, groups: player_id, 1088
Fixed effects:
Estimate Std. Error df t value Pr(>|t|)
(Intercept) 25.1074 0.5904 1296.8371 42.526 < 2e-16 ***
contractYearyes -4.0835 0.5560 3258.2826 -7.345 2.59e-13 ***
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Correlation of Fixed Effects:
(Intr)
contrctYrys -0.234
# R2 for Mixed Models
Conditional R2: 0.609
Marginal R2: 0.007
contractYear emmean SE df lower.CL upper.CL
no 25.1 0.59 1190 23.9 26.3
yes 21.0 0.71 2076 19.6 22.4
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: 32298
Scaled residuals:
Min 1Q Median 3Q Max
-2.9029 -0.5276 -0.0953 0.4800 3.9583
Random effects:
Groups Name Variance Std.Dev. Corr
player_id (Intercept) 520.008 22.804
ageCentered20 5.898 2.429 -0.71
Residual 136.761 11.694
Number of obs: 3849, groups: player_id, 1088
Fixed effects:
Estimate Std. Error df t value Pr(>|t|)
(Intercept) 15.03683 1.51901 1488.09149 9.899 < 2e-16 ***
contractYearyes -3.24593 0.54450 3060.78317 -5.961 2.79e-09 ***
ageCentered20 1.18353 0.52352 2137.46690 2.261 0.0239 *
ageCentered20Quadratic -0.44276 0.02687 1555.27042 -16.477 < 2e-16 ***
years_of_experience 5.03286 0.41161 1324.28706 12.227 < 2e-16 ***
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Correlation of Fixed Effects:
(Intr) cntrcY agCn20 agC20Q
contrctYrys 0.115
ageCentrd20 -0.817 -0.132
agCntrd20Qd 0.682 0.073 -0.646
yrs_f_xprnc 0.259 0.001 -0.654 -0.081
# R2 for Mixed Models
Conditional R2: 0.741
Marginal R2: 0.146
contractYear emmean SE df lower.CL upper.CL
no 24.6 0.605 1210 23.5 25.8
yes 21.4 0.696 1926 20.0 22.8
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: 12598.2
Scaled residuals:
Min 1Q Median 3Q Max
-5.5816 -0.5693 -0.0361 0.5369 3.8982
Random effects:
Groups Name Variance Std.Dev.
player_id (Intercept) 0.5522 0.7431
Residual 1.3004 1.1404
Number of obs: 3774, groups: player_id, 1071
Fixed effects:
Estimate Std. Error df t value Pr(>|t|)
(Intercept) 0.66056 0.03340 1444.06144 19.778 < 2e-16 ***
contractYearyes -0.16409 0.04521 3588.40814 -3.629 0.000288 ***
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Correlation of Fixed Effects:
(Intr)
contrctYrys -0.358
# R2 for Mixed Models
Conditional R2: 0.300
Marginal R2: 0.003
contractYear emmean SE df lower.CL upper.CL
no 0.661 0.0334 1270 0.595 0.726
yes 0.496 0.0456 2457 0.407 0.586
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: 12545.2
Scaled residuals:
Min 1Q Median 3Q Max
-5.4597 -0.5652 -0.0294 0.5305 3.8994
Random effects:
Groups Name Variance Std.Dev. Corr
player_id (Intercept) 0.911124 0.95453
ageCentered20 0.006103 0.07812 -0.68
Residual 1.246288 1.11637
Number of obs: 3774, groups: player_id, 1071
Fixed effects:
Estimate Std. Error df t value Pr(>|t|)
(Intercept) 3.575e-01 1.023e-01 1.130e+03 3.494 0.000494 ***
contractYearyes -1.735e-01 4.766e-02 3.500e+03 -3.640 0.000276 ***
ageCentered20 9.670e-03 3.352e-02 1.258e+03 0.288 0.773025
ageCentered20Quadratic -1.093e-02 1.898e-03 4.972e+02 -5.757 1.50e-08 ***
years_of_experience 1.678e-01 2.380e-02 1.222e+03 7.053 2.93e-12 ***
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Correlation of Fixed Effects:
(Intr) cntrcY agCn20 agC20Q
contrctYrys 0.137
ageCentrd20 -0.842 -0.186
agCntrd20Qd 0.778 0.123 -0.736
yrs_f_xprnc 0.102 0.001 -0.502 -0.155
# R2 for Mixed Models
Conditional R2: 0.335
Marginal R2: 0.031
contractYear emmean SE df lower.CL upper.CL
no 0.683 0.0340 1198 0.616 0.75
yes 0.509 0.0461 2486 0.419 0.60
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: 42596.4
Scaled residuals:
Min 1Q Median 3Q Max
-3.2569 -0.5264 -0.1439 0.4776 4.6031
Random effects:
Groups Name Variance Std.Dev.
player_id (Intercept) 2938 54.20
Residual 2464 49.64
Number of obs: 3849, groups: player_id, 1088
Fixed effects:
Estimate Std. Error df t value Pr(>|t|)
(Intercept) 76.082 1.977 1334.416 38.491 < 2e-16 ***
contractYearyes -14.174 2.027 3339.944 -6.994 3.21e-12 ***
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Correlation of Fixed Effects:
(Intr)
contrctYrys -0.257
# R2 for Mixed Models
Conditional R2: 0.547
Marginal R2: 0.008
contractYear emmean SE df lower.CL upper.CL
no 76.1 1.98 1208 72.2 80.0
yes 61.9 2.44 2199 57.1 66.7
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: 42173.2
Scaled residuals:
Min 1Q Median 3Q Max
-3.0232 -0.5011 -0.1202 0.4586 4.9595
Random effects:
Groups Name Variance Std.Dev. Corr
player_id (Intercept) 5542.78 74.450
ageCentered20 62.56 7.909 -0.73
Residual 1958.83 44.259
Number of obs: 3849, groups: player_id, 1088
Fixed effects:
Estimate Std. Error df t value Pr(>|t|)
(Intercept) 43.92080 5.32048 1446.59427 8.255 3.38e-16 ***
contractYearyes -11.38928 2.02175 3200.81182 -5.633 1.92e-08 ***
ageCentered20 2.81433 1.80559 2093.49796 1.559 0.119
ageCentered20Quadratic -1.43481 0.09652 1345.60860 -14.865 < 2e-16 ***
years_of_experience 17.66607 1.37263 1289.05418 12.870 < 2e-16 ***
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Correlation of Fixed Effects:
(Intr) cntrcY agCn20 agC20Q
contrctYrys 0.120
ageCentrd20 -0.826 -0.143
agCntrd20Qd 0.708 0.083 -0.676
yrs_f_xprnc 0.221 0.000 -0.614 -0.094
# R2 for Mixed Models
Conditional R2: 0.674
Marginal R2: 0.143
contractYear emmean SE df lower.CL upper.CL
no 75.1 2.01 1210 71.1 79.0
yes 63.7 2.38 2086 59.0 68.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: 71246.5
Scaled residuals:
Min 1Q Median 3Q Max
-3.7607 -0.5148 -0.1425 0.4754 4.2032
Random effects:
Groups Name Variance Std.Dev.
player_id (Intercept) 3360 57.97
Residual 2940 54.22
Number of obs: 6342, groups: player_id, 1803
Fixed effects:
Estimate Std. Error df t value Pr(>|t|)
(Intercept) 154.301 5.296 1926.481 29.14 <2e-16 ***
contractYearyes -16.377 1.757 5508.788 -9.32 <2e-16 ***
position_groupRB -72.000 6.020 1907.095 -11.96 <2e-16 ***
position_groupTE -93.584 6.345 1893.898 -14.75 <2e-16 ***
position_groupWR -69.240 5.851 1906.016 -11.83 <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.084
postn_grpRB -0.875 0.013
postn_grpTE -0.829 -0.003 0.729
postn_grpWR -0.899 0.002 0.791 0.750
# R2 for Mixed Models
Conditional R2: 0.579
Marginal R2: 0.097
contractYear emmean SE df lower.CL upper.CL
no 95.6 1.90 1914 91.9 99.3
yes 79.2 2.28 3308 74.7 83.7
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: 70667.6
Scaled residuals:
Min 1Q Median 3Q Max
-3.7078 -0.5030 -0.1205 0.4497 4.4839
Random effects:
Groups Name Variance Std.Dev. Corr
player_id (Intercept) 5990.83 77.400
ageCentered20 70.11 8.373 -0.73
Residual 2435.30 49.349
Number of obs: 6342, groups: player_id, 1803
Fixed effects:
Estimate Std. Error df t value Pr(>|t|)
(Intercept) 128.08019 6.62585 2845.54840 19.330 < 2e-16 ***
contractYearyes -12.64221 1.78301 5365.14944 -7.090 1.51e-12 ***
position_groupRB -63.76276 5.86196 1835.98555 -10.877 < 2e-16 ***
position_groupTE -86.12368 6.14762 1788.80770 -14.009 < 2e-16 ***
position_groupWR -63.06668 5.69118 1823.91009 -11.081 < 2e-16 ***
ageCentered20 -1.67954 1.44430 3188.30661 -1.163 0.245
ageCentered20Quadratic -1.18708 0.07448 1895.71369 -15.938 < 2e-16 ***
years_of_experience 18.59106 1.13641 2221.16240 16.359 < 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.100
postn_grpRB -0.705 -0.027
postn_grpTE -0.651 -0.026 0.738
postn_grpWR -0.729 -0.037 0.797 0.757
ageCentrd20 -0.510 -0.126 -0.027 -0.046 -0.004
agCntrd20Qd 0.468 0.085 -0.026 -0.011 -0.030 -0.651
yrs_f_xprnc 0.071 -0.031 0.097 0.093 0.067 -0.626 -0.104
# R2 for Mixed Models
Conditional R2: 0.672
Marginal R2: 0.190
contractYear emmean SE df lower.CL upper.CL
no 92.9 1.89 1865 89.2 96.7
yes 80.3 2.25 3250 75.9 84.7
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.1 (2025-06-13)
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.4 forcats_1.0.0 stringr_1.5.1 dplyr_1.1.4
[5] purrr_1.1.0 readr_2.1.5 tidyr_1.3.1 tibble_3.3.0
[9] ggplot2_3.5.2 tidyverse_2.0.0 emmeans_1.11.2-8 performance_0.15.1
[13] lmerTest_3.1-3 lme4_1.1-37 Matrix_1.7-3 nflreadr_1.5.0
[17] petersenlab_1.2.0
loaded via a namespace (and not attached):
[1] tidyselect_1.2.1 psych_2.5.6 viridisLite_0.4.2
[4] farver_2.1.2 fastmap_1.2.0 TH.data_1.1-4
[7] digest_0.6.37 rpart_4.1.24 timechange_0.3.0
[10] estimability_1.5.1 lifecycle_1.0.4 cluster_2.1.8.1
[13] survival_3.8-3 magrittr_2.0.3 compiler_4.5.1
[16] rlang_1.1.6 Hmisc_5.2-3 tools_4.5.1
[19] yaml_2.3.10 data.table_1.17.8 knitr_1.50
[22] htmlwidgets_1.6.4 mnormt_2.1.1 plyr_1.8.9
[25] RColorBrewer_1.1-3 multcomp_1.4-28 withr_3.0.2
[28] foreign_0.8-90 numDeriv_2016.8-1.1 nnet_7.3-20
[31] grid_4.5.1 stats4_4.5.1 lavaan_0.6-19
[34] xtable_1.8-4 colorspace_2.1-1 scales_1.4.0
[37] MASS_7.3-65 insight_1.4.2 cli_3.6.5
[40] mvtnorm_1.3-3 rmarkdown_2.29 reformulas_0.4.1
[43] generics_0.1.4 rstudioapi_0.17.1 tzdb_0.5.0
[46] reshape2_1.4.4 minqa_1.2.8 DBI_1.2.3
[49] cachem_1.1.0 splines_4.5.1 parallel_4.5.1
[52] base64enc_0.1-3 mitools_2.4 vctrs_0.6.5
[55] sandwich_3.1-1 boot_1.3-31 jsonlite_2.0.0
[58] hms_1.1.3 pbkrtest_0.5.5 Formula_1.2-5
[61] htmlTable_2.4.3 glue_1.8.0 nloptr_2.2.1
[64] codetools_0.2-20 stringi_1.8.7 gtable_0.3.6
[67] quadprog_1.5-8 pillar_1.11.0 htmltools_0.5.8.1
[70] R6_2.6.1 Rdpack_2.6.4 mix_1.0-13
[73] evaluate_1.0.5 pbivnorm_0.6.0 lattice_0.22-7
[76] rbibutils_2.3 backports_1.5.0 broom_1.0.9
[79] memoise_2.0.1 Rcpp_1.1.0 coda_0.19-4.1
[82] gridExtra_2.3 nlme_3.1-168 checkmate_2.3.3
[85] xfun_0.53 zoo_1.8-14 pkgconfig_2.0.3