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 at https://perma.cc/CT3F-QN5E) and 2022 (Niles, 2022; archived at https://perma.cc/F4F5-7RQZ).
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.5
Scaled residuals:
Min 1Q Median 3Q Max
-3.1498 -0.5380 0.0910 0.5724 3.1917
Random effects:
Groups Name Variance Std.Dev.
player_id (Intercept) 112.0 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.2840 0.8412 245.2217 52.644 <2e-16 ***
contractYearyes -0.8738 1.1368 1063.7272 -0.769 0.442
---
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.354
Marginal R2: 0.000
contractYear emmean SE df lower.CL upper.CL
no 44.3 0.842 284 42.6 45.9
yes 43.4 1.250 804 41.0 45.9
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: 9943.3
Scaled residuals:
Min 1Q Median 3Q Max
-3.3056 -0.4979 0.0813 0.5508 3.2409
Random effects:
Groups Name Variance Std.Dev. Corr
player_id (Intercept) 149.4669 12.2257
ageCentered20 0.7425 0.8617 -0.54
Residual 193.8632 13.9235
Number of obs: 1186, groups: player_id, 271
Fixed effects:
Estimate Std. Error df t value Pr(>|t|)
(Intercept) 39.56847 2.20015 246.27636 17.984 < 2e-16 ***
contractYearyes -0.50460 1.17248 1049.28996 -0.430 0.6670
ageCentered20 0.28897 0.62750 355.64282 0.461 0.6454
ageCentered20Quadratic -0.08953 0.02217 178.29145 -4.038 7.99e-05 ***
years_of_experience 1.52469 0.51847 333.39962 2.941 0.0035 **
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Correlation of Fixed Effects:
(Intr) cntrcY agCn20 agC20Q
contrctYrys 0.050
ageCentrd20 -0.749 -0.060
agCntrd20Qd 0.751 0.038 -0.635
yrs_f_xprnc 0.150 -0.027 -0.683 -0.077
# R2 for Mixed Models
Conditional R2: 0.402
Marginal R2: 0.026
contractYear emmean SE df lower.CL upper.CL
no 44.3 0.879 257 42.6 46.0
yes 43.8 1.260 740 41.3 46.3
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.2
Scaled residuals:
Min 1Q Median 3Q Max
-4.6374 -0.5031 0.0901 0.5459 4.2578
Random effects:
Groups Name Variance Std.Dev.
player_id (Intercept) 2.551 1.597
Residual 4.368 2.090
Number of obs: 1192, groups: player_id, 274
Fixed effects:
Estimate Std. Error df t value Pr(>|t|)
(Intercept) -0.8380 0.1254 234.3168 -6.682 1.7e-10 ***
contractYearyes -0.2104 0.1664 1051.0003 -1.264 0.206
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Correlation of Fixed Effects:
(Intr)
contrctYrys -0.231
# R2 for Mixed Models
Conditional R2: 0.369
Marginal R2: 0.001
contractYear emmean SE df lower.CL upper.CL
no -0.838 0.125 284 -1.09 -0.591
yes -1.048 0.184 795 -1.41 -0.687
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: 5413.9
Scaled residuals:
Min 1Q Median 3Q Max
-4.8312 -0.5112 0.0876 0.5217 4.2918
Random effects:
Groups Name Variance Std.Dev. Corr
player_id (Intercept) 3.88640 1.9714
ageCentered20 0.01774 0.1332 -0.65
Residual 4.16345 2.0405
Number of obs: 1186, groups: player_id, 271
Fixed effects:
Estimate Std. Error df t value Pr(>|t|)
(Intercept) -1.540e+00 3.309e-01 2.339e+02 -4.654 5.45e-06 ***
contractYearyes -1.832e-01 1.717e-01 1.038e+03 -1.067 0.286066
ageCentered20 2.813e-02 9.314e-02 3.455e+02 0.302 0.762813
ageCentered20Quadratic -1.204e-02 3.275e-03 1.685e+02 -3.676 0.000318 ***
years_of_experience 2.341e-01 7.604e-02 3.202e+02 3.078 0.002263 **
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Correlation of Fixed Effects:
(Intr) cntrcY agCn20 agC20Q
contrctYrys 0.050
ageCentrd20 -0.753 -0.061
agCntrd20Qd 0.746 0.043 -0.643
yrs_f_xprnc 0.160 -0.028 -0.685 -0.066
# R2 for Mixed Models
Conditional R2: 0.401
Marginal R2: 0.023
contractYear emmean SE df lower.CL upper.CL
no -0.790 0.128 260 -1.04 -0.538
yes -0.973 0.185 745 -1.34 -0.611
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.1
Scaled residuals:
Min 1Q Median 3Q Max
-3.0423 -0.4972 0.0336 0.5464 4.4114
Random effects:
Groups Name Variance Std.Dev.
player_id (Intercept) 2.521 1.588
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.1342 0.1172 255.5034 9.674 <2e-16 ***
contractYearyes 0.3395 0.1386 1031.9883 2.449 0.0145 *
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Correlation of Fixed Effects:
(Intr)
contrctYrys -0.203
# 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 283 0.903 1.37
yes 1.47 0.162 733 1.155 1.79
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: 5017.5
Scaled residuals:
Min 1Q Median 3Q Max
-3.1323 -0.5224 0.0649 0.5388 4.3523
Random effects:
Groups Name Variance Std.Dev.
player_id (Intercept) 2.338 1.529
Residual 2.929 1.711
Number of obs: 1186, groups: player_id, 271
Fixed effects:
Estimate Std. Error df t value Pr(>|t|)
(Intercept) 4.918e-01 2.603e-01 1.028e+03 1.889 0.059113 .
contractYearyes 1.707e-01 1.436e-01 1.056e+03 1.189 0.234842
ageCentered20 -5.464e-02 7.594e-02 7.301e+02 -0.720 0.472050
ageCentered20Quadratic -6.021e-03 2.395e-03 1.100e+03 -2.514 0.012073 *
years_of_experience 2.600e-01 6.637e-02 4.642e+02 3.917 0.000103 ***
---
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.726 -0.062
agCntrd20Qd 0.730 0.040 -0.572
yrs_f_xprnc 0.196 -0.028 -0.731 -0.103
# R2 for Mixed Models
Conditional R2: 0.464
Marginal R2: 0.036
contractYear emmean SE df lower.CL upper.CL
no 1.28 0.117 283 1.05 1.52
yes 1.45 0.162 731 1.14 1.77
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: 14071
Scaled residuals:
Min 1Q Median 3Q Max
-3.7579 -0.5680 -0.0806 0.6294 2.7203
Random effects:
Groups Name Variance Std.Dev.
player_id (Intercept) 6150 78.42
Residual 5546 74.47
Number of obs: 1192, groups: player_id, 274
Fixed effects:
Estimate Std. Error df t value Pr(>|t|)
(Intercept) 111.575 5.580 315.462 19.997 < 2e-16 ***
contractYearyes -32.306 6.024 1048.808 -5.363 1.01e-07 ***
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Correlation of Fixed Effects:
(Intr)
contrctYrys -0.184
# R2 for Mixed Models
Conditional R2: 0.532
Marginal R2: 0.014
contractYear emmean SE df lower.CL upper.CL
no 111.6 5.58 283 100.6 122.6
yes 79.3 7.42 680 64.7 93.8
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: 13949.2
Scaled residuals:
Min 1Q Median 3Q Max
-3.8643 -0.5769 -0.0811 0.6252 2.5728
Random effects:
Groups Name Variance Std.Dev.
player_id (Intercept) 5955 77.17
Residual 5338 73.06
Number of obs: 1186, groups: player_id, 271
Fixed effects:
Estimate Std. Error df t value Pr(>|t|)
(Intercept) 140.8230 11.6423 1036.7002 12.096 < 2e-16 ***
contractYearyes -25.3706 6.1920 1059.5436 -4.097 4.50e-05 ***
ageCentered20 -14.8645 3.4566 822.9553 -4.300 1.91e-05 ***
ageCentered20Quadratic -0.1610 0.1036 1092.2642 -1.554 0.12
years_of_experience 16.4575 3.0832 585.1456 5.338 1.35e-07 ***
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Correlation of Fixed Effects:
(Intr) cntrcY agCn20 agC20Q
contrctYrys 0.063
ageCentrd20 -0.712 -0.061
agCntrd20Qd 0.705 0.037 -0.544
yrs_f_xprnc 0.228 -0.024 -0.759 -0.096
# R2 for Mixed Models
Conditional R2: 0.562
Marginal R2: 0.073
contractYear emmean SE df lower.CL upper.CL
no 113.1 5.64 285 102.0 124
yes 87.8 7.41 667 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: 6822.1
Scaled residuals:
Min 1Q Median 3Q Max
-7.9318 -0.3929 0.0089 0.4039 15.0512
Random effects:
Groups Name Variance Std.Dev.
player_id (Intercept) 0.4291 0.655
Residual 1.9194 1.385
Number of obs: 1870, groups: player_id, 560
Fixed effects:
Estimate Std. Error df t value Pr(>|t|)
(Intercept) 3.908e+00 4.871e-02 5.719e+02 80.241 <2e-16 ***
contractYearyes 1.201e-02 7.823e-02 1.825e+03 0.154 0.878
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Correlation of Fixed Effects:
(Intr)
contrctYrys -0.396
# R2 for Mixed Models
Conditional R2: 0.183
Marginal R2: 0.000
contractYear emmean SE df lower.CL upper.CL
no 3.91 0.0487 676 3.81 4.00
yes 3.92 0.0741 1307 3.77 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: 6808.2
Scaled residuals:
Min 1Q Median 3Q Max
-7.7418 -0.3806 -0.0056 0.3938 14.4738
Random effects:
Groups Name Variance Std.Dev. Corr
player_id (Intercept) 0.3187 0.5645
ageCentered20 0.0109 0.1044 -0.37
Residual 1.8481 1.3594
Number of obs: 1870, groups: player_id, 560
Fixed effects:
Estimate Std. Error df t value Pr(>|t|)
(Intercept) 4.141e+00 1.631e-01 7.770e+02 25.393 <2e-16 ***
contractYearyes 9.562e-02 8.507e-02 1.739e+03 1.124 0.261
ageCentered20 -4.734e-02 5.650e-02 8.201e+02 -0.838 0.402
ageCentered20Quadratic -5.905e-03 4.128e-03 4.357e+02 -1.430 0.153
years_of_experience 5.894e-02 3.718e-02 5.674e+02 1.585 0.113
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Correlation of Fixed Effects:
(Intr) cntrcY agCn20 agC20Q
contrctYrys 0.162
ageCentrd20 -0.872 -0.166
agCntrd20Qd 0.814 0.151 -0.804
yrs_f_xprnc -0.062 -0.131 -0.300 -0.248
# R2 for Mixed Models
Conditional R2: 0.235
Marginal R2: 0.021
contractYear emmean SE df lower.CL upper.CL
no 3.87 0.0519 579 3.77 3.97
yes 3.97 0.0776 1306 3.81 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: 5393.5
Scaled residuals:
Min 1Q Median 3Q Max
-4.6983 -0.5062 0.0782 0.5862 3.4360
Random effects:
Groups Name Variance Std.Dev.
player_id (Intercept) 0.1040 0.3224
Residual 0.9552 0.9774
Number of obs: 1870, groups: player_id, 560
Fixed effects:
Estimate Std. Error df t value Pr(>|t|)
(Intercept) -0.64512 0.03077 694.49360 -20.96 <2e-16 ***
contractYearyes 0.04076 0.05361 1867.57072 0.76 0.447
---
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.098
Marginal R2: 0.000
contractYear emmean SE df lower.CL upper.CL
no -0.645 0.0308 690 -0.706 -0.585
yes -0.604 0.0486 1239 -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: 5406.9
Scaled residuals:
Min 1Q Median 3Q Max
-4.7422 -0.5040 0.0672 0.5781 3.4174
Random effects:
Groups Name Variance Std.Dev. Corr
player_id (Intercept) 0.236649 0.48647
ageCentered20 0.003396 0.05827 -0.76
Residual 0.934249 0.96657
Number of obs: 1870, groups: player_id, 560
Fixed effects:
Estimate Std. Error df t value Pr(>|t|)
(Intercept) -6.872e-01 1.147e-01 4.765e+02 -5.992 4.09e-09 ***
contractYearyes 6.854e-02 5.739e-02 1.649e+03 1.194 0.233
ageCentered20 4.618e-02 3.838e-02 4.844e+02 1.203 0.229
ageCentered20Quadratic -2.377e-03 2.722e-03 2.580e+02 -0.873 0.383
years_of_experience -3.278e-02 2.307e-02 5.885e+02 -1.421 0.156
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Correlation of Fixed Effects:
(Intr) cntrcY agCn20 agC20Q
contrctYrys 0.160
ageCentrd20 -0.884 -0.185
agCntrd20Qd 0.825 0.177 -0.832
yrs_f_xprnc -0.049 -0.124 -0.283 -0.225
# R2 for Mixed Models
Conditional R2: 0.121
Marginal R2: 0.004
contractYear emmean SE df lower.CL upper.CL
no -0.655 0.0318 600 -0.717 -0.592
yes -0.586 0.0506 1256 -0.686 -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: 21521.1
Scaled residuals:
Min 1Q Median 3Q Max
-3.2401 -0.4903 -0.1709 0.4078 3.8531
Random effects:
Groups Name Variance Std.Dev.
player_id (Intercept) 2451 49.50
Residual 2091 45.73
Number of obs: 1973, groups: player_id, 577
Fixed effects:
Estimate Std. Error df t value Pr(>|t|)
(Intercept) 65.760 2.474 695.816 26.581 < 2e-16 ***
contractYearyes -12.571 2.745 1705.934 -4.579 5.01e-06 ***
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Correlation of Fixed Effects:
(Intr)
contrctYrys -0.240
# R2 for Mixed Models
Conditional R2: 0.543
Marginal R2: 0.007
contractYear emmean SE df lower.CL upper.CL
no 65.8 2.47 631 60.9 70.6
yes 53.2 3.23 1281 46.9 59.5
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: 21360
Scaled residuals:
Min 1Q Median 3Q Max
-3.7104 -0.4882 -0.1531 0.4265 3.5522
Random effects:
Groups Name Variance Std.Dev. Corr
player_id (Intercept) 4263.03 65.292
ageCentered20 39.38 6.275 -0.74
Residual 1813.93 42.590
Number of obs: 1973, groups: player_id, 577
Fixed effects:
Estimate Std. Error df t value Pr(>|t|)
(Intercept) 58.6423 6.8397 772.5162 8.574 < 2e-16 ***
contractYearyes -10.4358 2.8749 1686.3435 -3.630 0.000292 ***
ageCentered20 -2.3643 2.3713 1049.2986 -0.997 0.318979
ageCentered20Quadratic -1.0239 0.1466 607.5037 -6.985 7.49e-12 ***
years_of_experience 15.4261 1.6765 738.3768 9.201 < 2e-16 ***
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Correlation of Fixed Effects:
(Intr) cntrcY agCn20 agC20Q
contrctYrys 0.166
ageCentrd20 -0.855 -0.151
agCntrd20Qd 0.727 0.167 -0.737
yrs_f_xprnc 0.203 -0.115 -0.530 -0.113
# R2 for Mixed Models
Conditional R2: 0.612
Marginal R2: 0.106
contractYear emmean SE df lower.CL upper.CL
no 67.0 2.43 639 62.3 71.8
yes 56.6 3.15 1287 50.4 62.8
Degrees-of-freedom method: kenward-roger
Confidence level used: 0.95
18.2.3 WR/TE
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: 35244.1
Scaled residuals:
Min 1Q Median 3Q Max
-4.8705 -0.5237 -0.1144 0.5037 4.5875
Random effects:
Groups Name Variance Std.Dev.
player_id (Intercept) 275.1 16.59
Residual 180.4 13.43
Number of obs: 4146, groups: player_id, 1146
Fixed effects:
Estimate Std. Error df t value Pr(>|t|)
(Intercept) 24.9202 0.5698 1375.5668 43.734 < 2e-16 ***
contractYearyes -4.2752 0.5274 3524.9605 -8.106 7.14e-16 ***
---
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.607
Marginal R2: 0.008
contractYear emmean SE df lower.CL upper.CL
no 24.9 0.570 1257 23.8 26
yes 20.6 0.679 2141 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: 34706
Scaled residuals:
Min 1Q Median 3Q Max
-2.9358 -0.5189 -0.0997 0.4779 3.9916
Random effects:
Groups Name Variance Std.Dev. Corr
player_id (Intercept) 514.160 22.68
ageCentered20 5.855 2.42 -0.71
Residual 134.199 11.58
Number of obs: 4146, groups: player_id, 1146
Fixed effects:
Estimate Std. Error df t value Pr(>|t|)
(Intercept) 14.49275 1.45972 1618.24317 9.928 < 2e-16 ***
contractYearyes -3.17620 0.51635 3284.86822 -6.151 8.61e-10 ***
ageCentered20 1.50128 0.50311 2307.12056 2.984 0.00288 **
ageCentered20Quadratic -0.46762 0.02552 1766.33024 -18.323 < 2e-16 ***
years_of_experience 4.91082 0.39735 1400.69138 12.359 < 2e-16 ***
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Correlation of Fixed Effects:
(Intr) cntrcY agCn20 agC20Q
contrctYrys 0.118
ageCentrd20 -0.819 -0.137
agCntrd20Qd 0.675 0.073 -0.640
yrs_f_xprnc 0.276 0.008 -0.663 -0.077
# R2 for Mixed Models
Conditional R2: 0.747
Marginal R2: 0.155
contractYear emmean SE df lower.CL upper.CL
no 24.1 0.587 1279 23.0 25.3
yes 20.9 0.668 1984 19.6 22.2
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: 13548.4
Scaled residuals:
Min 1Q Median 3Q Max
-5.6043 -0.5669 -0.0416 0.5273 3.9029
Random effects:
Groups Name Variance Std.Dev.
player_id (Intercept) 0.5422 0.7364
Residual 1.3001 1.1402
Number of obs: 4065, groups: player_id, 1126
Fixed effects:
Estimate Std. Error df t value Pr(>|t|)
(Intercept) 0.66996 0.03231 1521.78392 20.73 < 2e-16 ***
contractYearyes -0.16120 0.04321 3868.38080 -3.73 0.000194 ***
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Correlation of Fixed Effects:
(Intr)
contrctYrys -0.362
# R2 for Mixed Models
Conditional R2: 0.296
Marginal R2: 0.003
contractYear emmean SE df lower.CL upper.CL
no 0.670 0.0323 1342 0.607 0.733
yes 0.509 0.0436 2563 0.423 0.594
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: 13492.7
Scaled residuals:
Min 1Q Median 3Q Max
-5.7529 -0.5617 -0.0371 0.5236 3.9446
Random effects:
Groups Name Variance Std.Dev. Corr
player_id (Intercept) 0.955305 0.97740
ageCentered20 0.006654 0.08157 -0.70
Residual 1.240787 1.11391
Number of obs: 4065, groups: player_id, 1126
Fixed effects:
Estimate Std. Error df t value Pr(>|t|)
(Intercept) 3.615e-01 9.962e-02 1.230e+03 3.629 0.000296 ***
contractYearyes -1.643e-01 4.563e-02 3.767e+03 -3.601 0.000321 ***
ageCentered20 2.227e-02 3.253e-02 1.404e+03 0.685 0.493555
ageCentered20Quadratic -1.106e-02 1.841e-03 5.636e+02 -6.010 3.33e-09 ***
years_of_experience 1.535e-01 2.286e-02 1.285e+03 6.713 2.86e-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.847 -0.192
agCntrd20Qd 0.775 0.122 -0.739
yrs_f_xprnc 0.116 0.011 -0.503 -0.149
# R2 for Mixed Models
Conditional R2: 0.335
Marginal R2: 0.028
contractYear emmean SE df lower.CL upper.CL
no 0.686 0.0330 1276 0.621 0.750
yes 0.521 0.0441 2603 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: 42535.3
Scaled residuals:
Min 1Q Median 3Q Max
-3.3628 -0.5293 -0.1534 0.4389 4.9251
Random effects:
Groups Name Variance Std.Dev.
player_id (Intercept) 1215 34.86
Residual 1127 33.56
Number of obs: 4146, groups: player_id, 1146
Fixed effects:
Estimate Std. Error df t value Pr(>|t|)
(Intercept) 49.427 1.256 1443.311 39.362 < 2e-16 ***
contractYearyes -10.302 1.304 3647.812 -7.901 3.62e-15 ***
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Correlation of Fixed Effects:
(Intr)
contrctYrys -0.269
# R2 for Mixed Models
Conditional R2: 0.523
Marginal R2: 0.009
contractYear emmean SE df lower.CL upper.CL
no 49.4 1.26 1284 47.0 51.9
yes 39.1 1.55 2313 36.1 42.2
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: 42115.3
Scaled residuals:
Min 1Q Median 3Q Max
-3.1207 -0.5024 -0.1214 0.4287 5.2992
Random effects:
Groups Name Variance Std.Dev. Corr
player_id (Intercept) 2426.12 49.256
ageCentered20 24.89 4.989 -0.74
Residual 902.42 30.040
Number of obs: 4146, groups: player_id, 1146
Fixed effects:
Estimate Std. Error df t value Pr(>|t|)
(Intercept) 30.92265 3.43909 1579.07395 8.992 < 2e-16 ***
contractYearyes -7.32187 1.30535 3479.25298 -5.609 2.19e-08 ***
ageCentered20 1.92954 1.16055 2233.04071 1.663 0.0965 .
ageCentered20Quadratic -0.91016 0.06143 1374.85900 -14.815 < 2e-16 ***
years_of_experience 10.53521 0.87748 1362.34115 12.006 < 2e-16 ***
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Correlation of Fixed Effects:
(Intr) cntrcY agCn20 agC20Q
contrctYrys 0.126
ageCentrd20 -0.830 -0.155
agCntrd20Qd 0.707 0.090 -0.678
yrs_f_xprnc 0.235 0.010 -0.617 -0.090
# R2 for Mixed Models
Conditional R2: 0.655
Marginal R2: 0.134
contractYear emmean SE df lower.CL upper.CL
no 48.0 1.28 1285 45.5 50.5
yes 40.7 1.53 2203 37.7 43.7
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: 73360.1
Scaled residuals:
Min 1Q Median 3Q Max
-4.8226 -0.4764 -0.1298 0.4211 4.1893
Random effects:
Groups Name Variance Std.Dev.
player_id (Intercept) 2006 44.79
Residual 1866 43.20
Number of obs: 6816, groups: player_id, 1898
Fixed effects:
Estimate Std. Error df t value Pr(>|t|)
(Intercept) 152.013 4.011 2036.243 37.90 <2e-16 ***
contractYearyes -13.799 1.335 5971.228 -10.34 <2e-16 ***
position_groupRB -85.728 4.561 2016.427 -18.80 <2e-16 ***
position_groupTE -112.436 4.810 2000.614 -23.37 <2e-16 ***
position_groupWR -96.187 4.430 2014.771 -21.71 <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.081
postn_grpRB -0.874 0.008
postn_grpTE -0.828 -0.007 0.728
postn_grpWR -0.899 -0.002 0.791 0.750
# R2 for Mixed Models
Conditional R2: 0.616
Marginal R2: 0.204
contractYear emmean SE df lower.CL upper.CL
no 78.4 1.44 2021 75.6 81.3
yes 64.6 1.73 3457 61.2 68.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: 72795.8
Scaled residuals:
Min 1Q Median 3Q Max
-4.3477 -0.4691 -0.1119 0.4138 4.0275
Random effects:
Groups Name Variance Std.Dev. Corr
player_id (Intercept) 3402.76 58.333
ageCentered20 39.54 6.288 -0.69
Residual 1561.65 39.518
Number of obs: 6816, groups: player_id, 1898
Fixed effects:
Estimate Std. Error df t value Pr(>|t|)
(Intercept) 135.86006 5.03863 3053.85830 26.964 < 2e-16 ***
contractYearyes -9.68928 1.36446 5817.88670 -7.101 1.38e-12 ***
position_groupRB -80.60227 4.50068 1957.69476 -17.909 < 2e-16 ***
position_groupTE -108.43652 4.72268 1905.41648 -22.961 < 2e-16 ***
position_groupWR -92.31773 4.36877 1949.16102 -21.131 < 2e-16 ***
ageCentered20 -1.20662 1.09148 3405.82504 -1.105 0.269
ageCentered20Quadratic -0.89417 0.05601 2019.27013 -15.966 < 2e-16 ***
years_of_experience 13.25415 0.86621 2376.66276 15.301 < 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.106
postn_grpRB -0.710 -0.032
postn_grpTE -0.656 -0.029 0.738
postn_grpWR -0.734 -0.040 0.798 0.758
ageCentrd20 -0.503 -0.135 -0.028 -0.044 -0.010
agCntrd20Qd 0.463 0.087 -0.029 -0.012 -0.032 -0.644
yrs_f_xprnc 0.070 -0.024 0.099 0.089 0.077 -0.629 -0.111
# R2 for Mixed Models
Conditional R2: 0.693
Marginal R2: 0.262
contractYear emmean SE df lower.CL upper.CL
no 75.5 1.45 1959 72.7 78.4
yes 65.9 1.72 3396 62.5 69.2
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 age and 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.6.1 (2026-06-24)
Platform: x86_64-pc-linux-gnu
Running under: Ubuntu 24.04.5 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.1
[5] purrr_1.2.2 readr_2.2.0 tidyr_1.3.2 tibble_3.3.1
[9] ggplot2_4.0.3 tidyverse_2.0.0 emmeans_2.0.4 performance_0.18.2
[13] lmerTest_3.2-1 lme4_2.0-6 Matrix_1.7-5 nflreadr_1.5.1
[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.1 sandwich_3.1-3 rlang_1.3.0
[7] magrittr_2.0.5 multcomp_1.4-32 otel_0.2.0
[10] compiler_4.6.1 vctrs_0.7.3 reshape2_1.4.5
[13] quadprog_1.5-8 pkgconfig_2.0.3 fastmap_1.2.0
[16] backports_1.5.1 pbivnorm_0.6.0 rmarkdown_2.32
[19] tzdb_0.5.0 nloptr_2.2.1 xfun_0.61
[22] cachem_1.1.0 jsonlite_2.0.0 psych_2.6.5
[25] broom_1.0.13 parallel_4.6.1 lavaan_0.7-2
[28] cluster_2.1.8.2 R6_2.6.1 stringi_1.8.9
[31] RColorBrewer_1.1-3 boot_1.3-32 rpart_4.1.27
[34] numDeriv_2016.8-1.1 estimability_2.0.0 Rcpp_1.1.2
[37] knitr_1.52 zoo_1.9-0 base64enc_0.1-6
[40] splines_4.6.1 nnet_7.3-20 timechange_0.4.0
[43] tidyselect_1.2.1 rstudioapi_0.19.0 yaml_2.3.12
[46] codetools_0.2-20 lattice_0.22-9 plyr_1.8.9
[49] withr_3.0.3 S7_0.2.2 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.6.1
[58] reformulas_0.4.4 insight_1.5.4 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.1
[67] Hmisc_5.3-0 tools_4.6.1 data.table_1.18.6.1
[70] mvtnorm_1.4-2 grid_4.6.1 mitools_2.7
[73] rbibutils_2.4.1 colorspace_2.1-3 nlme_3.1-169
[76] htmlTable_2.5.0 Formula_1.2-6 cli_3.6.6
[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