I need your help!

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

Hypothesis 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

18.1 Getting Started

18.1.1 Load Packages

Code
library("petersenlab")
library("nflreadr")
library("lme4")
library("lmerTest")
library("MuMIn")
library("emmeans")
library("tidyverse")

18.1.2 Specify Package Options

Code
emm_options(lmerTest.limit = 100000)
emm_options(pbkrtest.limit = 100000)

18.1.3 Load Data

Code
load(file = "./data/nfl_playerContracts.RData")
load(file = "./data/player_stats_weekly.RData")
load(file = "./data/player_stats_seasonal.RData")
load(file = "./data/nfl_espnQBR_seasonal.RData")
load(file = "./data/nfl_espnQBR_weekly.RData")

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. In order to do that, 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:

  1. random intercepts to allow the model to estimate a different starting point for each player
  2. a fixed effect for whether the player is in a contract year

The model that accounts for the effects of age and experience includes:

  1. random intercepts to allow the model to estimate a different starting point for each player
  2. random linear slopes (i.e., random effect of linear age) to allow the model to estimate a different form of change for each player
  3. a fixed quadratic effect of age to allow for curvilinear effects
  4. a fixed effect of experience
  5. 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. 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.

Code
mixedModel_qbr <- lmerTest::lmer(
  qbr ~ contractYear + (1 | player_id),
  data = nfl_contractYearQBR_seasonal,
  control = lmerControl(optimizer = "bobyqa")
)

summary(mixedModel_qbr)
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: 9440.1

Scaled residuals: 
    Min      1Q  Median      3Q     Max 
-3.2458 -0.5402  0.0769  0.5701  3.2401 

Random effects:
 Groups    Name        Variance Std.Dev.
 player_id (Intercept) 110.5    10.51   
 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.6138     0.8532  237.9610  52.290   <2e-16 ***
contractYearyes   -0.1586     1.1569 1010.8162  -0.137    0.891    
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Correlation of Fixed Effects:
            (Intr)
contrctYrys -0.242
Code
MuMIn::r.squaredGLMM(mixedModel_qbr)
              R2m       R2c
[1,] 1.272846e-05 0.3571013
Code
emmeans::emmeans(mixedModel_qbr, "contractYear")
 contractYear emmean    SE  df lower.CL upper.CL
 no             44.6 0.854 273     42.9     46.3
 yes            44.5 1.260 769     42.0     46.9

Degrees-of-freedom method: kenward-roger 
Confidence level used: 0.95 
Code
mixedModelAge_qbr <- lmerTest::lmer(
  qbr ~ contractYear + ageCentered20 + ageCentered20Quadratic + years_of_experience + (1 + ageCentered20 | player_id),
  data = nfl_contractYearQBR_seasonal,
  control = lmerControl(optimizer = "bobyqa")
)

summary(mixedModelAge_qbr)
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: 9367.1

Scaled residuals: 
    Min      1Q  Median      3Q     Max 
-3.3664 -0.5127  0.0841  0.5489  3.2657 

Random effects:
 Groups    Name          Variance Std.Dev. Corr 
 player_id (Intercept)   126.9074 11.2653       
           ageCentered20   0.3854  0.6208  -0.32
 Residual                191.2280 13.8285       
Number of obs: 1119, groups:  player_id, 258

Fixed effects:
                         Estimate Std. Error         df t value Pr(>|t|)    
(Intercept)             3.941e+01  2.259e+00  1.940e+02  17.445  < 2e-16 ***
contractYearyes         8.814e-03  1.197e+00  1.005e+03   0.007  0.99413    
ageCentered20           1.530e+00  6.493e-01  2.886e+02   2.357  0.01911 *  
ageCentered20Quadratic -7.464e-02  2.254e-02  1.060e+02  -3.311  0.00127 ** 
years_of_experience    -1.954e-01  5.397e-01  3.237e+02  -0.362  0.71748    
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Correlation of Fixed Effects:
            (Intr) cntrcY agCn20 agC20Q
contrctYrys  0.060                     
ageCentrd20 -0.790 -0.075              
agCntrd20Qd  0.736  0.053 -0.616       
yrs_f_xprnc  0.238 -0.020 -0.693 -0.094
Code
MuMIn::r.squaredGLMM(mixedModelAge_qbr)
            R2m       R2c
[1,] 0.01244643 0.4032188
Code
emmeans::emmeans(mixedModelAge_qbr, "contractYear")
 contractYear emmean    SE  df lower.CL upper.CL
 no             44.2 0.905 251     42.5     46.0
 yes            44.2 1.290 713     41.7     46.8

Degrees-of-freedom method: kenward-roger 
Confidence level used: 0.95 
Code
mixedModel_ptsAdded <- lmerTest::lmer(
  pts_added ~ contractYear + (1 | player_id),
  data = nfl_contractYearQBR_seasonal,
  control = lmerControl(optimizer = "bobyqa")
)

summary(mixedModel_ptsAdded)
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: 5132.9

Scaled residuals: 
    Min      1Q  Median      3Q     Max 
-4.7318 -0.4972  0.0857  0.5439  4.2752 

Random effects:
 Groups    Name        Variance Std.Dev.
 player_id (Intercept) 2.574    1.604   
 Residual              4.276    2.068   
Number of obs: 1127, groups:  player_id, 262

Fixed effects:
                Estimate Std. Error       df t value Pr(>|t|)    
(Intercept)      -0.7982     0.1283 224.1668  -6.222 2.38e-09 ***
contractYearyes  -0.1155     0.1700 995.5919  -0.679    0.497    
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Correlation of Fixed Effects:
            (Intr)
contrctYrys -0.236
Code
MuMIn::r.squaredGLMM(mixedModel_ptsAdded)
              R2m       R2c
[1,] 0.0003049317 0.3759501
Code
emmeans::emmeans(mixedModel_ptsAdded, "contractYear")
 contractYear emmean    SE  df lower.CL upper.CL
 no           -0.798 0.128 273    -1.05   -0.545
 yes          -0.914 0.187 758    -1.28   -0.546

Degrees-of-freedom method: kenward-roger 
Confidence level used: 0.95 
Code
mixedModelAge_ptsAdded <- lmerTest::lmer(
  pts_added ~ contractYear + ageCentered20 + ageCentered20Quadratic + years_of_experience + (1 + ageCentered20 | player_id),
  data = nfl_contractYearQBR_seasonal,
  control = lmerControl(optimizer = "bobyqa")
)

summary(mixedModelAge_ptsAdded)
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: 5103.5

Scaled residuals: 
    Min      1Q  Median      3Q     Max 
-4.8880 -0.4961  0.0894  0.5303  4.2769 

Random effects:
 Groups    Name          Variance Std.Dev. Corr 
 player_id (Intercept)   3.57351  1.8904        
           ageCentered20 0.01127  0.1061   -0.52
 Residual                4.09939  2.0247        
Number of obs: 1119, groups:  player_id, 258

Fixed effects:
                         Estimate Std. Error         df t value Pr(>|t|)    
(Intercept)             -1.580138   0.343810 185.033479  -4.596 7.96e-06 ***
contractYearyes         -0.104459   0.175743 988.551064  -0.594  0.55239    
ageCentered20            0.198359   0.097581 286.002874   2.033  0.04300 *  
ageCentered20Quadratic  -0.010753   0.003375 106.612781  -3.186  0.00189 ** 
years_of_experience      0.009459   0.080374 306.565256   0.118  0.90639    
---
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.790 -0.076              
agCntrd20Qd  0.731  0.058 -0.624       
yrs_f_xprnc  0.244 -0.020 -0.696 -0.081
Code
MuMIn::r.squaredGLMM(mixedModelAge_ptsAdded)
            R2m      R2c
[1,] 0.01003626 0.415973
Code
emmeans::emmeans(mixedModelAge_ptsAdded, "contractYear")
 contractYear emmean    SE  df lower.CL upper.CL
 no           -0.823 0.134 254    -1.09   -0.558
 yes          -0.927 0.191 710    -1.30   -0.552

Degrees-of-freedom method: kenward-roger 
Confidence level used: 0.95 
Code
mixedModel_epaPass <- lmerTest::lmer(
  epa_pass ~ contractYear + (1 | player_id),
  data = nfl_contractYearQBR_seasonal,
  control = lmerControl(optimizer = "bobyqa")
)

summary(mixedModel_epaPass)
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.2

Scaled residuals: 
    Min      1Q  Median      3Q     Max 
-3.0595 -0.5107  0.0386  0.5465  4.4071 

Random effects:
 Groups    Name        Variance Std.Dev.
 player_id (Intercept) 2.508    1.584   
 Residual              2.981    1.727   
Number of obs: 1127, groups:  player_id, 262

Fixed effects:
                Estimate Std. Error       df t value Pr(>|t|)    
(Intercept)       1.1294     0.1198 243.0749   9.425  < 2e-16 ***
contractYearyes   0.3867     0.1433 979.1048   2.699  0.00707 ** 
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Correlation of Fixed Effects:
            (Intr)
contrctYrys -0.211
Code
MuMIn::r.squaredGLMM(mixedModel_epaPass)
            R2m       R2c
[1,] 0.00425061 0.4592752
Code
emmeans::emmeans(mixedModel_epaPass, "contractYear")
 contractYear emmean    SE  df lower.CL upper.CL
 no             1.13 0.120 272    0.893     1.37
 yes            1.52 0.166 704    1.189     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: 4755.4

Scaled residuals: 
    Min      1Q  Median      3Q     Max 
-3.2004 -0.4933  0.0484  0.5390  4.3305 

Random effects:
 Groups    Name        Variance Std.Dev.
 player_id (Intercept) 2.559    1.600   
 Residual              2.934    1.713   
Number of obs: 1119, groups:  player_id, 258

Fixed effects:
                         Estimate Std. Error         df t value Pr(>|t|)  
(Intercept)             3.313e-01  2.812e-01  9.569e+02   1.178   0.2390  
contractYearyes         2.285e-01  1.485e-01  9.983e+02   1.539   0.1241  
ageCentered20           1.372e-01  8.193e-02  7.395e+02   1.675   0.0944 .
ageCentered20Quadratic -4.517e-03  2.588e-03  1.057e+03  -1.746   0.0812 .
years_of_experience     1.785e-02  7.150e-02  4.128e+02   0.250   0.8030  
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Correlation of Fixed Effects:
            (Intr) cntrcY agCn20 agC20Q
contrctYrys  0.067                     
ageCentrd20 -0.770 -0.072              
agCntrd20Qd  0.704  0.057 -0.562       
yrs_f_xprnc  0.279 -0.025 -0.737 -0.108
Code
MuMIn::r.squaredGLMM(mixedModelAge_epaPass)
            R2m       R2c
[1,] 0.01876087 0.4759311
Code
emmeans::emmeans(mixedModelAge_epaPass, "contractYear")
 contractYear emmean    SE  df lower.CL upper.CL
 no             1.21 0.123 271    0.966     1.45
 yes            1.44 0.168 688    1.107     1.77

Degrees-of-freedom method: kenward-roger 
Confidence level used: 0.95 
Code
mixedModel_fantasyPtsPass <- lmerTest::lmer(
  fantasyPoints ~ contractYear + (1 | player_id),
  data = nfl_contractYearQBR_seasonal,
  control = lmerControl(optimizer = "bobyqa")
)

summary(mixedModel_fantasyPtsPass)
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.3

Scaled residuals: 
    Min      1Q  Median      3Q     Max 
-3.7943 -0.5637 -0.0847  0.6323  2.7416 

Random effects:
 Groups    Name        Variance Std.Dev.
 player_id (Intercept) 6286     79.29   
 Residual              5475     74.00   
Number of obs: 1127, groups:  player_id, 262

Fixed effects:
                Estimate Std. Error      df t value Pr(>|t|)    
(Intercept)      111.494      5.747 299.608  19.399  < 2e-16 ***
contractYearyes  -29.946      6.188 990.937  -4.839 1.51e-06 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Correlation of Fixed Effects:
            (Intr)
contrctYrys -0.188
Code
MuMIn::r.squaredGLMM(mixedModel_fantasyPtsPass)
           R2m       R2c
[1,] 0.0118063 0.5399733
Code
emmeans::emmeans(mixedModel_fantasyPtsPass, "contractYear")
 contractYear emmean   SE  df lower.CL upper.CL
 no            111.5 5.75 271    100.2    122.8
 yes            81.5 7.62 646     66.6     96.5

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: 13180.9

Scaled residuals: 
    Min      1Q  Median      3Q     Max 
-3.9486 -0.5690 -0.0741  0.6231  2.6086 

Random effects:
 Groups    Name        Variance Std.Dev.
 player_id (Intercept) 6564     81.02   
 Residual              5331     73.01   
Number of obs: 1119, groups:  player_id, 258

Fixed effects:
                        Estimate Std. Error        df t value Pr(>|t|)    
(Intercept)             138.6743    12.6049  971.2085  11.002  < 2e-16 ***
contractYearyes         -23.5326     6.3995  998.9610  -3.677 0.000248 ***
ageCentered20            -8.7711     3.7243  818.5205  -2.355 0.018752 *  
ageCentered20Quadratic   -0.0936     0.1121 1044.3790  -0.835 0.403818    
years_of_experience       8.4189     3.3429  523.0562   2.518 0.012085 *  
---
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.754 -0.068              
agCntrd20Qd  0.675  0.054 -0.526       
yrs_f_xprnc  0.305 -0.024 -0.764 -0.111
Code
MuMIn::r.squaredGLMM(mixedModelAge_fantasyPtsPass)
            R2m       R2c
[1,] 0.03683421 0.5683428
Code
emmeans::emmeans(mixedModelAge_fantasyPtsPass, "contractYear")
 contractYear emmean   SE  df lower.CL upper.CL
 no            110.4 5.96 272     98.7      122
 yes            86.9 7.73 623     71.7      102

Degrees-of-freedom method: kenward-roger 
Confidence level used: 0.95 

18.2.2 RB

Code
player_statsContractsRB_seasonal <- player_statsContracts_seasonal %>% 
  dplyr::filter(
    position_group == "RB",
    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)
Code
mixedModel_ypc <- lmerTest::lmer(
  ypc ~ contractYear + (1 | player_id),
  data = player_statsContractsRB_seasonal,
  control = lmerControl(optimizer = "bobyqa")
)

summary(mixedModel_ypc)
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: 6361.6

Scaled residuals: 
    Min      1Q  Median      3Q     Max 
-7.9753 -0.3998  0.0048  0.4031 14.8057 

Random effects:
 Groups    Name        Variance Std.Dev.
 player_id (Intercept) 0.5088   0.7133  
 Residual              1.8716   1.3681  
Number of obs: 1744, groups:  player_id, 531

Fixed effects:
                 Estimate Std. Error        df t value Pr(>|t|)    
(Intercept)     3.914e+00  5.134e-02 5.083e+02  76.229   <2e-16 ***
contractYearyes 6.210e-03  8.163e-02 1.677e+03   0.076    0.939    
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Correlation of Fixed Effects:
            (Intr)
contrctYrys -0.373
Code
MuMIn::r.squaredGLMM(mixedModel_ypc)
             R2m       R2c
[1,] 3.13029e-06 0.2137492
Code
emmeans::emmeans(mixedModel_ypc, "contractYear")
 contractYear emmean     SE   df lower.CL upper.CL
 no             3.91 0.0514  631     3.81     4.01
 yes            3.92 0.0787 1258     3.77     4.07

Degrees-of-freedom method: kenward-roger 
Confidence level used: 0.95 
Code
mixedModelAge_ypc <- lmerTest::lmer(
  ypc ~ contractYear + ageCentered20 + ageCentered20Quadratic + years_of_experience + (1 + ageCentered20 | player_id),
  data = player_statsContractsRB_seasonal,
  control = lmerControl(optimizer = "bobyqa")
)

summary(mixedModelAge_ypc)
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: 6346.5

Scaled residuals: 
    Min      1Q  Median      3Q     Max 
-7.7920 -0.3853 -0.0068  0.3840 14.2122 

Random effects:
 Groups    Name          Variance Std.Dev. Corr 
 player_id (Intercept)   0.39629  0.6295        
           ageCentered20 0.01044  0.1022   -0.28
 Residual                1.79728  1.3406        
Number of obs: 1742, groups:  player_id, 529

Fixed effects:
                         Estimate Std. Error         df t value Pr(>|t|)    
(Intercept)             4.222e+00  1.705e-01  7.175e+02  24.767   <2e-16 ***
contractYearyes         1.063e-01  8.808e-02  1.602e+03   1.207    0.228    
ageCentered20          -5.096e-02  5.995e-02  8.152e+02  -0.850    0.396    
ageCentered20Quadratic -2.844e-03  4.225e-03  4.258e+02  -0.673    0.501    
years_of_experience     1.446e-02  3.780e-02  4.723e+02   0.382    0.702    
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Correlation of Fixed Effects:
            (Intr) cntrcY agCn20 agC20Q
contrctYrys  0.150                     
ageCentrd20 -0.891 -0.172              
agCntrd20Qd  0.795  0.126 -0.800       
yrs_f_xprnc  0.057 -0.067 -0.355 -0.198
Code
MuMIn::r.squaredGLMM(mixedModelAge_ypc)
            R2m       R2c
[1,] 0.01710798 0.2675485
Code
emmeans::emmeans(mixedModelAge_ypc, "contractYear")
 contractYear emmean     SE   df lower.CL upper.CL
 no             3.87 0.0547  555     3.76     3.97
 yes            3.97 0.0824 1231     3.81     4.13

Degrees-of-freedom method: kenward-roger 
Confidence level used: 0.95 
Code
mixedModel_epaRush <- lmerTest::lmer(
  rushing_epa ~ contractYear + (1 | player_id),
  data = player_statsContractsRB_seasonal,
  control = lmerControl(optimizer = "bobyqa")
)

summary(mixedModel_epaRush)
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: 5057.5

Scaled residuals: 
    Min      1Q  Median      3Q     Max 
-4.5550 -0.5026  0.0814  0.5818  3.4230 

Random effects:
 Groups    Name        Variance Std.Dev.
 player_id (Intercept) 0.09939  0.3153  
 Residual              0.97446  0.9871  
Number of obs: 1744, groups:  player_id, 531

Fixed effects:
                  Estimate Std. Error         df t value Pr(>|t|)    
(Intercept)       -0.64471    0.03171  652.00671  -20.33   <2e-16 ***
contractYearyes    0.03333    0.05654 1741.98241    0.59    0.556    
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Correlation of Fixed Effects:
            (Intr)
contrctYrys -0.441
Code
MuMIn::r.squaredGLMM(mixedModel_epaRush)
              R2m        R2c
[1,] 0.0001998691 0.09273524
Code
emmeans::emmeans(mixedModel_epaRush, "contractYear")
 contractYear emmean     SE   df lower.CL upper.CL
 no           -0.645 0.0317  650   -0.707   -0.582
 yes          -0.611 0.0513 1161   -0.712   -0.511

Degrees-of-freedom method: kenward-roger 
Confidence level used: 0.95 
Code
mixedModelAge_epaRush <- lmerTest::lmer(
  rushing_epa ~ contractYear + ageCentered20 + ageCentered20Quadratic + years_of_experience + (1 + ageCentered20 | player_id),
  data = player_statsContractsRB_seasonal,
  control = lmerControl(optimizer = "bobyqa")
)

summary(mixedModelAge_epaRush)
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: 5062.4

Scaled residuals: 
    Min      1Q  Median      3Q     Max 
-4.6144 -0.4961  0.0697  0.5833  3.4213 

Random effects:
 Groups    Name          Variance Std.Dev. Corr 
 player_id (Intercept)   0.172837 0.41574       
           ageCentered20 0.002041 0.04517  -0.67
 Residual                0.958451 0.97901       
Number of obs: 1742, groups:  player_id, 529

Fixed effects:
                         Estimate Std. Error         df t value Pr(>|t|)    
(Intercept)            -6.614e-01  1.179e-01  4.017e+02  -5.608 3.81e-08 ***
contractYearyes         6.785e-02  5.998e-02  1.544e+03   1.131  0.25816    
ageCentered20           5.107e-02  3.973e-02  4.080e+02   1.286  0.19930    
ageCentered20Quadratic -1.261e-03  2.753e-03  2.096e+02  -0.458  0.64726    
years_of_experience    -5.985e-02  2.252e-02  4.867e+02  -2.657  0.00814 ** 
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Correlation of Fixed Effects:
            (Intr) cntrcY agCn20 agC20Q
contrctYrys  0.148                     
ageCentrd20 -0.902 -0.196              
agCntrd20Qd  0.818  0.153 -0.838       
yrs_f_xprnc  0.030 -0.054 -0.307 -0.191
Code
MuMIn::r.squaredGLMM(mixedModelAge_epaRush)
             R2m       R2c
[1,] 0.008101641 0.1113651
Code
emmeans::emmeans(mixedModelAge_epaRush, "contractYear")
 contractYear emmean     SE   df lower.CL upper.CL
 no           -0.657 0.0325  581   -0.721   -0.593
 yes          -0.589 0.0532 1158   -0.694   -0.485

Degrees-of-freedom method: kenward-roger 
Confidence level used: 0.95 
Code
mixedModel_fantasyPtsRush <- lmerTest::lmer(
  fantasyPoints ~ contractYear + (1 | player_id),
  data = player_statsContractsRB_seasonal,
  control = lmerControl(optimizer = "bobyqa")
)

summary(mixedModel_fantasyPtsRush)
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: 20808.1

Scaled residuals: 
    Min      1Q  Median      3Q     Max 
-3.1381 -0.5051 -0.1721  0.4160  3.8976 

Random effects:
 Groups    Name        Variance Std.Dev.
 player_id (Intercept) 3550     59.58   
 Residual              3039     55.13   
Number of obs: 1844, groups:  player_id, 548

Fixed effects:
                Estimate Std. Error       df t value Pr(>|t|)    
(Intercept)       81.576      3.056  654.334   26.69  < 2e-16 ***
contractYearyes  -14.047      3.469 1590.032   -4.05 5.37e-05 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Correlation of Fixed Effects:
            (Intr)
contrctYrys -0.235
Code
MuMIn::r.squaredGLMM(mixedModel_fantasyPtsRush)
             R2m       R2c
[1,] 0.005650508 0.5413615
Code
emmeans::emmeans(mixedModel_fantasyPtsRush, "contractYear")
 contractYear emmean   SE   df lower.CL upper.CL
 no             81.6 3.06  597     75.6     87.6
 yes            67.5 4.05 1247     59.6     75.5

Degrees-of-freedom method: kenward-roger 
Confidence level used: 0.95 
Code
mixedModelAge_fantasyPtsRush <- lmerTest::lmer(
  fantasyPoints ~ contractYear + ageCentered20 + ageCentered20Quadratic + years_of_experience + (1 + ageCentered20 | player_id),
  data = player_statsContractsRB_seasonal,
  control = lmerControl(optimizer = "bobyqa")
)

summary(mixedModelAge_fantasyPtsRush)
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: 20671.1

Scaled residuals: 
    Min      1Q  Median      3Q     Max 
-3.4383 -0.4918 -0.1585  0.4016  3.6466 

Random effects:
 Groups    Name          Variance Std.Dev. Corr 
 player_id (Intercept)   6826.30  82.621        
           ageCentered20   54.19   7.361   -0.75
 Residual                2642.05  51.401        
Number of obs: 1842, groups:  player_id, 546

Fixed effects:
                        Estimate Std. Error        df t value Pr(>|t|)    
(Intercept)              70.0164     8.8102  728.0439   7.947 7.26e-15 ***
contractYearyes          -9.6185     3.6077 1566.8723  -2.666  0.00775 ** 
ageCentered20             1.5209     3.0252  967.0698   0.503  0.61527    
ageCentered20Quadratic   -1.1202     0.1821  511.8087  -6.151 1.56e-09 ***
years_of_experience      12.0523     2.0881  659.5743   5.772 1.21e-08 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Correlation of Fixed Effects:
            (Intr) cntrcY agCn20 agC20Q
contrctYrys  0.168                     
ageCentrd20 -0.868 -0.169              
agCntrd20Qd  0.716  0.158 -0.740       
yrs_f_xprnc  0.264 -0.065 -0.552 -0.086
Code
MuMIn::r.squaredGLMM(mixedModelAge_fantasyPtsRush)
            R2m       R2c
[1,] 0.06212416 0.6124686
Code
emmeans::emmeans(mixedModelAge_fantasyPtsRush, "contractYear")
 contractYear emmean   SE   df lower.CL upper.CL
 no             80.7 3.08  598     74.7     86.8
 yes            71.1 4.04 1227     63.2     79.0

Degrees-of-freedom method: kenward-roger 
Confidence level used: 0.95 

18.2.3 WR/TE

Code
player_statsContractsWRTE_seasonal <- player_statsContracts_seasonal %>% 
  dplyr::filter(
    position_group %in% c("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)
Code
mixedModel_receivingYards <- lmerTest::lmer(
  receiving_yards ~ contractYear + (1 | player_id),
  data = player_statsContractsWRTE_seasonal,
  control = lmerControl(optimizer = "bobyqa")
)

summary(mixedModel_receivingYards)
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: 32762.4

Scaled residuals: 
    Min      1Q  Median      3Q     Max 
-4.8523 -0.5266 -0.1105  0.5070  4.5621 

Random effects:
 Groups    Name        Variance Std.Dev.
 player_id (Intercept) 280.4    16.74   
 Residual              182.7    13.52   
Number of obs: 3845, groups:  player_id, 1087

Fixed effects:
                 Estimate Std. Error        df t value Pr(>|t|)    
(Intercept)       25.1265     0.5907 1295.7691   42.53  < 2e-16 ***
contractYearyes   -4.0877     0.5569 3257.3755   -7.34 2.69e-13 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Correlation of Fixed Effects:
            (Intr)
contrctYrys -0.234
Code
MuMIn::r.squaredGLMM(mixedModel_receivingYards)
             R2m       R2c
[1,] 0.007389053 0.6084049
Code
emmeans::emmeans(mixedModel_receivingYards, "contractYear")
 contractYear emmean    SE   df lower.CL upper.CL
 no             25.1 0.591 1189     24.0     26.3
 yes            21.0 0.711 2077     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: 32324.3

Scaled residuals: 
    Min      1Q  Median      3Q     Max 
-2.9762 -0.5207 -0.0957  0.4753  3.8095 

Random effects:
 Groups    Name          Variance Std.Dev. Corr 
 player_id (Intercept)   546.303  23.37         
           ageCentered20   5.809   2.41    -0.69
 Residual                136.871  11.70         
Number of obs: 3843, groups:  player_id, 1086

Fixed effects:
                         Estimate Std. Error         df t value Pr(>|t|)    
(Intercept)              14.67660    1.57189 1531.91283   9.337  < 2e-16 ***
contractYearyes          -3.06203    0.54737 3034.32724  -5.594 2.42e-08 ***
ageCentered20             2.46053    0.53060 2230.72821   4.637 3.74e-06 ***
ageCentered20Quadratic   -0.43117    0.02688 1514.70198 -16.041  < 2e-16 ***
years_of_experience       3.34048    0.40668 1329.57876   8.214 5.02e-16 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Correlation of Fixed Effects:
            (Intr) cntrcY agCn20 agC20Q
contrctYrys  0.124                     
ageCentrd20 -0.844 -0.152              
agCntrd20Qd  0.659  0.072 -0.648       
yrs_f_xprnc  0.336  0.033 -0.663 -0.066
Code
MuMIn::r.squaredGLMM(mixedModelAge_receivingYards)
           R2m       R2c
[1,] 0.1116187 0.7445546
Code
emmeans::emmeans(mixedModelAge_receivingYards, "contractYear")
 contractYear emmean    SE   df lower.CL upper.CL
 no             24.0 0.622 1199     22.8     25.2
 yes            20.9 0.713 1900     19.5     22.3

Degrees-of-freedom method: kenward-roger 
Confidence level used: 0.95 
Code
mixedModel_epaReceiving <- lmerTest::lmer(
  receiving_epa ~ contractYear + (1 | player_id),
  data = player_statsContractsWRTE_seasonal,
  control = lmerControl(optimizer = "bobyqa")
)

summary(mixedModel_epaReceiving)
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: 12590.8

Scaled residuals: 
    Min      1Q  Median      3Q     Max 
-5.5737 -0.5713 -0.0390  0.5357  3.8971 

Random effects:
 Groups    Name        Variance Std.Dev.
 player_id (Intercept) 0.555    0.745   
 Residual              1.302    1.141   
Number of obs: 3770, groups:  player_id, 1070

Fixed effects:
                  Estimate Std. Error         df t value Pr(>|t|)    
(Intercept)        0.65807    0.03346 1439.73658  19.669  < 2e-16 ***
contractYearyes   -0.15989    0.04531 3584.95730  -3.529 0.000422 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Correlation of Fixed Effects:
            (Intr)
contrctYrys -0.357
Code
MuMIn::r.squaredGLMM(mixedModel_epaReceiving)
             R2m      R2c
[1,] 0.002833608 0.300893
Code
emmeans::emmeans(mixedModel_epaReceiving, "contractYear")
 contractYear emmean     SE   df lower.CL upper.CL
 no            0.658 0.0335 1267    0.592    0.724
 yes           0.498 0.0457 2456    0.409    0.588

Degrees-of-freedom method: kenward-roger 
Confidence level used: 0.95 
Code
mixedModelAge_epaReceiving <- lmerTest::lmer(
  receiving_epa ~ contractYear + ageCentered20 + ageCentered20Quadratic + years_of_experience + (1 + ageCentered20 | player_id),
  data = player_statsContractsWRTE_seasonal,
  control = lmerControl(optimizer = "bobyqa")
)

summary(mixedModelAge_epaReceiving)
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: 12574

Scaled residuals: 
    Min      1Q  Median      3Q     Max 
-5.4222 -0.5657 -0.0320  0.5251  3.8600 

Random effects:
 Groups    Name          Variance Std.Dev. Corr 
 player_id (Intercept)   0.937883 0.9684        
           ageCentered20 0.005505 0.0742   -0.66
 Residual                1.248330 1.1173        
Number of obs: 3769, groups:  player_id, 1069

Fixed effects:
                         Estimate Std. Error         df t value Pr(>|t|)    
(Intercept)             3.515e-01  1.040e-01  1.118e+03   3.379 0.000753 ***
contractYearyes        -1.599e-01  4.797e-02  3.492e+03  -3.333 0.000868 ***
ageCentered20           7.715e-02  3.397e-02  1.209e+03   2.271 0.023338 *  
ageCentered20Quadratic -9.347e-03  1.880e-03  4.735e+02  -4.972 9.29e-07 ***
years_of_experience     6.136e-02  2.302e-02  1.144e+03   2.665 0.007799 ** 
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Correlation of Fixed Effects:
            (Intr) cntrcY agCn20 agC20Q
contrctYrys  0.145                     
ageCentrd20 -0.871 -0.206              
agCntrd20Qd  0.758  0.119 -0.742       
yrs_f_xprnc  0.203  0.044 -0.524 -0.121
Code
MuMIn::r.squaredGLMM(mixedModelAge_epaReceiving)
            R2m       R2c
[1,] 0.01276357 0.3377774
Code
emmeans::emmeans(mixedModelAge_epaReceiving, "contractYear")
 contractYear emmean     SE   df lower.CL upper.CL
 no            0.656 0.0346 1214    0.588    0.724
 yes           0.496 0.0468 2502    0.404    0.588

Degrees-of-freedom method: kenward-roger 
Confidence level used: 0.95 
Code
mixedModel_fantasyPtsReceiving <- lmerTest::lmer(
  fantasyPoints ~ contractYear + (1 | player_id),
  data = player_statsContractsWRTE_seasonal,
  control = lmerControl(optimizer = "bobyqa")
)

summary(mixedModel_fantasyPtsReceiving)
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: 42563.2

Scaled residuals: 
    Min      1Q  Median      3Q     Max 
-3.2497 -0.5267 -0.1437  0.4778  4.5954 

Random effects:
 Groups    Name        Variance Std.Dev.
 player_id (Intercept) 2935     54.18   
 Residual              2473     49.73   
Number of obs: 3845, groups:  player_id, 1087

Fixed effects:
                Estimate Std. Error       df t value Pr(>|t|)    
(Intercept)       76.217      1.978 1333.859  38.533  < 2e-16 ***
contractYearyes  -14.118      2.033 3340.343  -6.945 4.52e-12 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Correlation of Fixed Effects:
            (Intr)
contrctYrys -0.257
Code
MuMIn::r.squaredGLMM(mixedModel_fantasyPtsReceiving)
             R2m       R2c
[1,] 0.007544551 0.5461689
Code
emmeans::emmeans(mixedModel_fantasyPtsReceiving, "contractYear")
 contractYear emmean   SE   df lower.CL upper.CL
 no             76.2 1.98 1207     72.3     80.1
 yes            62.1 2.44 2201     57.3     66.9

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: 42197

Scaled residuals: 
    Min      1Q  Median      3Q     Max 
-3.0668 -0.4966 -0.1283  0.4563  4.9583 

Random effects:
 Groups    Name          Variance Std.Dev. Corr 
 player_id (Intercept)   5865.96  76.590        
           ageCentered20   60.78   7.796   -0.71
 Residual                1960.23  44.275        
Number of obs: 3843, groups:  player_id, 1086

Fixed effects:
                        Estimate Std. Error        df t value Pr(>|t|)    
(Intercept)              43.3018     5.5012 1472.0304   7.871 6.75e-15 ***
contractYearyes         -10.4200     2.0348 3167.5419  -5.121 3.22e-07 ***
ageCentered20             7.3181     1.8400 2151.2044   3.977 7.20e-05 ***
ageCentered20Quadratic   -1.3780     0.0964 1291.7568 -14.294  < 2e-16 ***
years_of_experience      11.3259     1.3642 1283.5592   8.302 2.57e-16 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Correlation of Fixed Effects:
            (Intr) cntrcY agCn20 agC20Q
contrctYrys  0.130                     
ageCentrd20 -0.852 -0.164              
agCntrd20Qd  0.684  0.082 -0.675       
yrs_f_xprnc  0.306  0.035 -0.629 -0.076
Code
MuMIn::r.squaredGLMM(mixedModelAge_fantasyPtsReceiving)
           R2m       R2c
[1,] 0.1033737 0.6793945
Code
emmeans::emmeans(mixedModelAge_fantasyPtsReceiving, "contractYear")
 contractYear emmean   SE   df lower.CL upper.CL
 no             72.5 2.07 1204     68.5     76.6
 yes            62.1 2.45 2057     57.3     66.9

Degrees-of-freedom method: kenward-roger 
Confidence level used: 0.95 

18.2.4 QB/RB/WR/TE

Code
# Placeholder for model predicting fantasy points
# Include player position as a covariate

18.3 Conclusion

18.4 Session Info

Code
sessionInfo()
R version 4.5.0 (2025-04-11)
Platform: x86_64-pc-linux-gnu
Running under: Ubuntu 24.04.2 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.0.4       readr_2.1.5       tidyr_1.3.1       tibble_3.2.1     
 [9] ggplot2_3.5.2     tidyverse_2.0.0   emmeans_1.11.0    MuMIn_1.48.11    
[13] lmerTest_3.1-3    lme4_1.1-37       Matrix_1.7-3      nflreadr_1.4.1   
[17] petersenlab_1.1.3

loaded via a namespace (and not attached):
 [1] tidyselect_1.2.1    psych_2.5.3         viridisLite_0.4.2  
 [4] farver_2.1.2        fastmap_1.2.0       TH.data_1.1-3      
 [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.0     
[16] rlang_1.1.6         Hmisc_5.2-3         tools_4.5.0        
[19] yaml_2.3.10         data.table_1.17.0   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.0          stats4_4.5.0        lavaan_0.6-19      
[34] xtable_1.8-4        colorspace_2.1-1    scales_1.4.0       
[37] MASS_7.3-65         cli_3.6.5           mvtnorm_1.3-3      
[40] rmarkdown_2.29      reformulas_0.4.0    generics_0.1.3     
[43] rstudioapi_0.17.1   tzdb_0.5.0          reshape2_1.4.4     
[46] minqa_1.2.8         DBI_1.2.3           cachem_1.1.0       
[49] splines_4.5.0       parallel_4.5.0      base64enc_0.1-3    
[52] mitools_2.4         vctrs_0.6.5         sandwich_3.1-1     
[55] boot_1.3-31         jsonlite_2.0.0      hms_1.1.3          
[58] pbkrtest_0.5.3      Formula_1.2-5       htmlTable_2.4.3    
[61] glue_1.8.0          nloptr_2.2.1        codetools_0.2-20   
[64] stringi_1.8.7       gtable_0.3.6        quadprog_1.5-8     
[67] pillar_1.10.2       htmltools_0.5.8.1   R6_2.6.1           
[70] Rdpack_2.6.4        mix_1.0-13          evaluate_1.0.3     
[73] pbivnorm_0.6.0      lattice_0.22-6      rbibutils_2.3      
[76] backports_1.5.0     broom_1.0.8         memoise_2.0.1      
[79] Rcpp_1.0.14         coda_0.19-4.1       gridExtra_2.3      
[82] nlme_3.1-168        checkmate_2.3.2     xfun_0.52          
[85] zoo_1.8-14          pkgconfig_2.0.3    

Feedback

Please consider providing feedback about this textbook, so that I can make it as helpful as possible. You can provide feedback at the following link: https://forms.gle/LsnVKwqmS1VuxWD18

Email Notification

The online version of this book will remain open access. If you want to know when the print version of the book is for sale, enter your email below so I can let you know.