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.2451 -0.5403  0.0767  0.5700  3.2399 

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.6156     0.8526  237.4894  52.328   <2e-16 ***
contractYearyes   -0.1715     1.1601 1008.9925  -0.148    0.883    
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Correlation of Fixed Effects:
            (Intr)
contrctYrys -0.239
Code
MuMIn::r.squaredGLMM(mixedModel_qbr)
              R2m       R2c
[1,] 1.478002e-05 0.3571011
Code
emmeans::emmeans(mixedModel_qbr, "contractYear")
 contractYear emmean    SE  df lower.CL upper.CL
 no             44.6 0.853 272     42.9     46.3
 yes            44.4 1.270 774     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.367 -0.513  0.084  0.549  3.266 

Random effects:
 Groups    Name          Variance Std.Dev. Corr 
 player_id (Intercept)   126.9109 11.2655       
           ageCentered20   0.3855  0.6209  -0.32
 Residual                191.2250 13.8284       
Number of obs: 1119, groups:  player_id, 258

Fixed effects:
                         Estimate Std. Error         df t value Pr(>|t|)    
(Intercept)              39.41240    2.25893  194.07184  17.447  < 2e-16 ***
contractYearyes           0.01999    1.20153 1000.46464   0.017  0.98673    
ageCentered20             1.52956    0.64905  288.74817   2.357  0.01911 *  
ageCentered20Quadratic   -0.07463    0.02254  106.04139  -3.311  0.00127 ** 
years_of_experience      -0.19545    0.53974  323.76549  -0.362  0.71750    
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Correlation of Fixed Effects:
            (Intr) cntrcY agCn20 agC20Q
contrctYrys  0.059                     
ageCentrd20 -0.790 -0.071              
agCntrd20Qd  0.736  0.049 -0.616       
yrs_f_xprnc  0.238 -0.023 -0.693 -0.094
Code
MuMIn::r.squaredGLMM(mixedModelAge_qbr)
           R2m       R2c
[1,] 0.0124524 0.4032476
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.3 1.300 718     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.7312 -0.4973  0.0857  0.5453  4.2750 

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.1282 223.7371  -6.226 2.33e-09 ***
contractYearyes  -0.1170     0.1705 993.6872  -0.686    0.493    
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Correlation of Fixed Effects:
            (Intr)
contrctYrys -0.233
Code
MuMIn::r.squaredGLMM(mixedModel_ptsAdded)
              R2m       R2c
[1,] 0.0003108757 0.3759314
Code
emmeans::emmeans(mixedModel_ptsAdded, "contractYear")
 contractYear emmean    SE  df lower.CL upper.CL
 no           -0.798 0.128 272    -1.05   -0.546
 yes          -0.915 0.188 763    -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.8882 -0.4961  0.0894  0.5301  4.2771 

Random effects:
 Groups    Name          Variance Std.Dev. Corr 
 player_id (Intercept)   3.57383  1.8905        
           ageCentered20 0.01126  0.1061   -0.52
 Residual                4.09959  2.0247        
Number of obs: 1119, groups:  player_id, 258

Fixed effects:
                         Estimate Std. Error         df t value Pr(>|t|)    
(Intercept)             -1.579520   0.343782 185.015762  -4.595 8.01e-06 ***
contractYearyes         -0.103404   0.176353 984.230548  -0.586  0.55778    
ageCentered20            0.197941   0.097544 286.031947   2.029  0.04336 *  
ageCentered20Quadratic  -0.010740   0.003374 106.514781  -3.183  0.00191 ** 
years_of_experience      0.009662   0.080376 306.571919   0.120  0.90440    
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Correlation of Fixed Effects:
            (Intr) cntrcY agCn20 agC20Q
contrctYrys  0.059                     
ageCentrd20 -0.790 -0.072              
agCntrd20Qd  0.731  0.054 -0.624       
yrs_f_xprnc  0.244 -0.023 -0.696 -0.081
Code
MuMIn::r.squaredGLMM(mixedModelAge_ptsAdded)
            R2m       R2c
[1,] 0.01001632 0.4159062
Code
emmeans::emmeans(mixedModelAge_ptsAdded, "contractYear")
 contractYear emmean    SE  df lower.CL upper.CL
 no           -0.823 0.134 254    -1.09   -0.559
 yes          -0.927 0.192 714    -1.30   -0.550

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

Scaled residuals: 
    Min      1Q  Median      3Q     Max 
-3.0638 -0.5098  0.0388  0.5482  4.4079 

Random effects:
 Groups    Name        Variance Std.Dev.
 player_id (Intercept) 2.51     1.584   
 Residual              2.98     1.726   
Number of obs: 1127, groups:  player_id, 262

Fixed effects:
                Estimate Std. Error       df t value Pr(>|t|)    
(Intercept)       1.1290     0.1198 242.6476   9.426  < 2e-16 ***
contractYearyes   0.3946     0.1436 977.3789   2.747  0.00612 ** 
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Correlation of Fixed Effects:
            (Intr)
contrctYrys -0.208
Code
MuMIn::r.squaredGLMM(mixedModel_epaPass)
             R2m       R2c
[1,] 0.004393483 0.4595603
Code
emmeans::emmeans(mixedModel_epaPass, "contractYear")
 contractYear emmean    SE  df lower.CL upper.CL
 no             1.13 0.120 272    0.893     1.36
 yes            1.52 0.167 709    1.196     1.85

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.3

Scaled residuals: 
    Min      1Q  Median      3Q     Max 
-3.1995 -0.4944  0.0481  0.5399  4.3277 

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

Fixed effects:
                         Estimate Std. Error         df t value Pr(>|t|)  
(Intercept)             3.318e-01  2.812e-01  9.569e+02   1.180   0.2383  
contractYearyes         2.359e-01  1.490e-01  9.964e+02   1.583   0.1137  
ageCentered20           1.374e-01  8.191e-02  7.390e+02   1.677   0.0939 .
ageCentered20Quadratic -4.524e-03  2.587e-03  1.057e+03  -1.749   0.0806 .
years_of_experience     1.751e-02  7.151e-02  4.131e+02   0.245   0.8067  
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Correlation of Fixed Effects:
            (Intr) cntrcY agCn20 agC20Q
contrctYrys  0.066                     
ageCentrd20 -0.770 -0.068              
agCntrd20Qd  0.704  0.054 -0.562       
yrs_f_xprnc  0.278 -0.028 -0.737 -0.108
Code
MuMIn::r.squaredGLMM(mixedModelAge_epaPass)
            R2m       R2c
[1,] 0.01881249 0.4760832
Code
emmeans::emmeans(mixedModelAge_epaPass, "contractYear")
 contractYear emmean    SE  df lower.CL upper.CL
 no             1.21 0.123 271    0.965     1.45
 yes            1.44 0.169 694    1.112     1.78

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

Scaled residuals: 
    Min      1Q  Median      3Q     Max 
-3.7938 -0.5628 -0.0854  0.6325  2.7421 

Random effects:
 Groups    Name        Variance Std.Dev.
 player_id (Intercept) 6298     79.36   
 Residual              5477     74.01   
Number of obs: 1127, groups:  player_id, 262

Fixed effects:
                Estimate Std. Error      df t value Pr(>|t|)    
(Intercept)      111.342      5.749 299.090  19.367  < 2e-16 ***
contractYearyes  -29.589      6.205 989.601  -4.769 2.13e-06 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Correlation of Fixed Effects:
            (Intr)
contrctYrys -0.185
Code
MuMIn::r.squaredGLMM(mixedModel_fantasyPtsPass)
            R2m       R2c
[1,] 0.01143801 0.5401623
Code
emmeans::emmeans(mixedModel_fantasyPtsPass, "contractYear")
 contractYear emmean   SE  df lower.CL upper.CL
 no            111.3 5.75 271    100.0    122.7
 yes            81.8 7.64 650     66.7     96.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: 13181.5

Scaled residuals: 
    Min      1Q  Median      3Q     Max 
-3.9481 -0.5697 -0.0770  0.6226  2.6078 

Random effects:
 Groups    Name        Variance Std.Dev.
 player_id (Intercept) 6574     81.08   
 Residual              5333     73.03   
Number of obs: 1119, groups:  player_id, 258

Fixed effects:
                         Estimate Std. Error         df t value Pr(>|t|)    
(Intercept)             138.77595   12.60858  971.08217  11.006  < 2e-16 ***
contractYearyes         -23.08409    6.42163  997.45929  -3.595 0.000341 ***
ageCentered20            -8.83704    3.72487  818.10907  -2.372 0.017901 *  
ageCentered20Quadratic   -0.09183    0.11208 1044.34333  -0.819 0.412796    
years_of_experience       8.44017    3.34466  523.36645   2.523 0.011915 *  
---
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.065              
agCntrd20Qd  0.675  0.051 -0.526       
yrs_f_xprnc  0.304 -0.026 -0.764 -0.111
Code
MuMIn::r.squaredGLMM(mixedModelAge_fantasyPtsPass)
            R2m       R2c
[1,] 0.03658633 0.5685138
Code
emmeans::emmeans(mixedModelAge_fantasyPtsPass, "contractYear")
 contractYear emmean   SE  df lower.CL upper.CL
 no            110.3 5.96 271     98.5      122
 yes            87.2 7.76 627     71.9      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.9762 -0.4002  0.0051  0.4031 14.8072 

Random effects:
 Groups    Name        Variance Std.Dev.
 player_id (Intercept) 0.5084   0.713   
 Residual              1.8718   1.368   
Number of obs: 1744, groups:  player_id, 531

Fixed effects:
                 Estimate Std. Error        df t value Pr(>|t|)    
(Intercept)     3.913e+00  5.135e-02 5.086e+02  76.202   <2e-16 ***
contractYearyes 8.829e-03  8.155e-02 1.677e+03   0.108    0.914    
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Correlation of Fixed Effects:
            (Intr)
contrctYrys -0.374
Code
MuMIn::r.squaredGLMM(mixedModel_ypc)
             R2m       R2c
[1,] 6.33599e-06 0.2135924
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.0786 1257     3.77     4.08

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.4

Scaled residuals: 
    Min      1Q  Median      3Q     Max 
-7.7936 -0.3853 -0.0070  0.3843 14.2145 

Random effects:
 Groups    Name          Variance Std.Dev. Corr 
 player_id (Intercept)   0.39621  0.6295        
           ageCentered20 0.01043  0.1021   -0.28
 Residual                1.79738  1.3407        
Number of obs: 1742, groups:  player_id, 529

Fixed effects:
                         Estimate Std. Error         df t value Pr(>|t|)    
(Intercept)             4.223e+00  1.705e-01  7.173e+02  24.772   <2e-16 ***
contractYearyes         1.093e-01  8.800e-02  1.602e+03   1.243    0.214    
ageCentered20          -5.148e-02  5.996e-02  8.149e+02  -0.859    0.391    
ageCentered20Quadratic -2.817e-03  4.225e-03  4.257e+02  -0.667    0.505    
years_of_experience     1.444e-02  3.780e-02  4.721e+02   0.382    0.703    
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Correlation of Fixed Effects:
            (Intr) cntrcY agCn20 agC20Q
contrctYrys  0.151                     
ageCentrd20 -0.891 -0.173              
agCntrd20Qd  0.795  0.127 -0.800       
yrs_f_xprnc  0.057 -0.066 -0.355 -0.198
Code
MuMIn::r.squaredGLMM(mixedModelAge_ypc)
            R2m       R2c
[1,] 0.01717228 0.2674471
Code
emmeans::emmeans(mixedModelAge_ypc, "contractYear")
 contractYear emmean     SE   df lower.CL upper.CL
 no             3.86 0.0547  555     3.76     3.97
 yes            3.97 0.0822 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.4

Scaled residuals: 
    Min      1Q  Median      3Q     Max 
-4.5544 -0.5021  0.0793  0.5799  3.4236 

Random effects:
 Groups    Name        Variance Std.Dev.
 player_id (Intercept) 0.09945  0.3154  
 Residual              0.97437  0.9871  
Number of obs: 1744, groups:  player_id, 531

Fixed effects:
                  Estimate Std. Error         df t value Pr(>|t|)    
(Intercept)       -0.64551    0.03172  652.34985 -20.349   <2e-16 ***
contractYearyes    0.03646    0.05649 1741.99500   0.645    0.519    
---
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.0002394901 0.09283116
Code
emmeans::emmeans(mixedModel_epaRush, "contractYear")
 contractYear emmean     SE   df lower.CL upper.CL
 no           -0.646 0.0317  650   -0.708   -0.583
 yes          -0.609 0.0512 1162   -0.710   -0.509

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.3

Scaled residuals: 
    Min      1Q  Median      3Q     Max 
-4.6136 -0.4966  0.0707  0.5823  3.4224 

Random effects:
 Groups    Name          Variance Std.Dev. Corr 
 player_id (Intercept)   0.173096 0.41605       
           ageCentered20 0.002046 0.04523  -0.67
 Residual                0.958271 0.97891       
Number of obs: 1742, groups:  player_id, 529

Fixed effects:
                         Estimate Std. Error         df t value Pr(>|t|)    
(Intercept)            -6.602e-01  1.179e-01  4.018e+02  -5.597 4.04e-08 ***
contractYearyes         7.124e-02  5.994e-02  1.545e+03   1.189  0.23475    
ageCentered20           5.052e-02  3.974e-02  4.083e+02   1.271  0.20434    
ageCentered20Quadratic -1.233e-03  2.753e-03  2.098e+02  -0.448  0.65469    
years_of_experience    -5.986e-02  2.252e-02  4.869e+02  -2.658  0.00812 ** 
---
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.197              
agCntrd20Qd  0.818  0.154 -0.838       
yrs_f_xprnc  0.030 -0.053 -0.307 -0.191
Code
MuMIn::r.squaredGLMM(mixedModelAge_epaRush)
             R2m       R2c
[1,] 0.008179426 0.1115709
Code
emmeans::emmeans(mixedModelAge_epaRush, "contractYear")
 contractYear emmean     SE   df lower.CL upper.CL
 no           -0.658 0.0326  581   -0.722   -0.594
 yes          -0.587 0.0532 1159   -0.691   -0.482

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

Scaled residuals: 
    Min      1Q  Median      3Q     Max 
-3.1340 -0.5057 -0.1729  0.4156  3.8988 

Random effects:
 Groups    Name        Variance Std.Dev.
 player_id (Intercept) 3546     59.55   
 Residual              3042     55.16   
Number of obs: 1844, groups:  player_id, 548

Fixed effects:
                Estimate Std. Error       df t value Pr(>|t|)    
(Intercept)       81.495      3.055  654.865  26.672  < 2e-16 ***
contractYearyes  -13.576      3.466 1589.483  -3.917 9.36e-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.00528056 0.5406345
Code
emmeans::emmeans(mixedModel_fantasyPtsRush, "contractYear")
 contractYear emmean   SE   df lower.CL upper.CL
 no             81.5 3.06  597     75.5     87.5
 yes            67.9 4.05 1246     60.0     75.9

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.4

Scaled residuals: 
    Min      1Q  Median      3Q     Max 
-3.4371 -0.4920 -0.1588  0.4013  3.6457 

Random effects:
 Groups    Name          Variance Std.Dev. Corr 
 player_id (Intercept)   6841.8   82.715        
           ageCentered20   54.6    7.389   -0.75
 Residual                2641.7   51.398        
Number of obs: 1842, groups:  player_id, 546

Fixed effects:
                        Estimate Std. Error        df t value Pr(>|t|)    
(Intercept)              70.0016     8.8203  729.4479   7.936 7.85e-15 ***
contractYearyes          -9.3663     3.6074 1564.7556  -2.596  0.00951 ** 
ageCentered20             1.5209     3.0282  968.8867   0.502  0.61560    
ageCentered20Quadratic   -1.1212     0.1823  514.8215  -6.149 1.57e-09 ***
years_of_experience      12.0528     2.0883  659.2541   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.170                     
ageCentrd20 -0.868 -0.171              
agCntrd20Qd  0.716  0.160 -0.740       
yrs_f_xprnc  0.264 -0.065 -0.551 -0.086
Code
MuMIn::r.squaredGLMM(mixedModelAge_fantasyPtsRush)
            R2m       R2c
[1,] 0.06210342 0.6125754
Code
emmeans::emmeans(mixedModelAge_fantasyPtsRush, "contractYear")
 contractYear emmean   SE   df lower.CL upper.CL
 no             80.7 3.08  599     74.6     86.7
 yes            71.3 4.04 1227     63.4     79.2

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.7

Scaled residuals: 
    Min      1Q  Median      3Q     Max 
-4.8528 -0.5278 -0.1103  0.5063  4.5623 

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.1251     0.5908 1296.1433  42.528  < 2e-16 ***
contractYearyes   -4.0728     0.5567 3257.3106  -7.316  3.2e-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.00733895 0.6083696
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.1 0.711 2075     19.7     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.7

Scaled residuals: 
    Min      1Q  Median      3Q     Max 
-2.9759 -0.5212 -0.0962  0.4752  3.8093 

Random effects:
 Groups    Name          Variance Std.Dev. Corr 
 player_id (Intercept)   546.051  23.368        
           ageCentered20   5.812   2.411   -0.69
 Residual                136.886  11.700        
Number of obs: 3843, groups:  player_id, 1086

Fixed effects:
                         Estimate Std. Error         df t value Pr(>|t|)    
(Intercept)              14.70081    1.57144 1529.88027   9.355  < 2e-16 ***
contractYearyes          -3.03800    0.54699 3035.02757  -5.554 3.03e-08 ***
ageCentered20             2.45054    0.53041 2226.85462   4.620 4.05e-06 ***
ageCentered20Quadratic   -0.43105    0.02688 1513.89053 -16.036  < 2e-16 ***
years_of_experience       3.34628    0.40668 1329.37210   8.228 4.48e-16 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Correlation of Fixed Effects:
            (Intr) cntrcY agCn20 agC20Q
contrctYrys  0.122                     
ageCentrd20 -0.844 -0.150              
agCntrd20Qd  0.659  0.071 -0.648       
yrs_f_xprnc  0.336  0.030 -0.663 -0.067
Code
MuMIn::r.squaredGLMM(mixedModelAge_receivingYards)
           R2m      R2c
[1,] 0.1117034 0.744591
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 1898     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.3

Scaled residuals: 
    Min      1Q  Median      3Q     Max 
-5.5745 -0.5715 -0.0389  0.5376  3.8959 

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

Fixed effects:
                  Estimate Std. Error         df t value Pr(>|t|)    
(Intercept)        0.65910    0.03347 1440.32757  19.692  < 2e-16 ***
contractYearyes   -0.16319    0.04527 3585.01245  -3.605 0.000316 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Correlation of Fixed Effects:
            (Intr)
contrctYrys -0.358
Code
MuMIn::r.squaredGLMM(mixedModel_epaReceiving)
             R2m     R2c
[1,] 0.002954858 0.30102
Code
emmeans::emmeans(mixedModel_epaReceiving, "contractYear")
 contractYear emmean     SE   df lower.CL upper.CL
 no            0.659 0.0335 1268    0.593    0.725
 yes           0.496 0.0457 2454    0.406    0.585

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

Scaled residuals: 
    Min      1Q  Median      3Q     Max 
-5.4226 -0.5652 -0.0324  0.5258  3.8586 

Random effects:
 Groups    Name          Variance Std.Dev. Corr 
 player_id (Intercept)   0.9379   0.96844       
           ageCentered20 0.0055   0.07416  -0.66
 Residual                1.2482   1.11724       
Number of obs: 3769, groups:  player_id, 1069

Fixed effects:
                         Estimate Std. Error         df t value Pr(>|t|)    
(Intercept)             3.514e-01  1.040e-01  1.117e+03   3.379 0.000752 ***
contractYearyes        -1.635e-01  4.791e-02  3.496e+03  -3.413 0.000651 ***
ageCentered20           7.729e-02  3.395e-02  1.207e+03   2.277 0.022982 *  
ageCentered20Quadratic -9.348e-03  1.879e-03  4.726e+02  -4.974 9.18e-07 ***
years_of_experience     6.149e-02  2.301e-02  1.144e+03   2.672 0.007654 ** 
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Correlation of Fixed Effects:
            (Intr) cntrcY agCn20 agC20Q
contrctYrys  0.143                     
ageCentrd20 -0.871 -0.203              
agCntrd20Qd  0.758  0.117 -0.741       
yrs_f_xprnc  0.202  0.041 -0.524 -0.121
Code
MuMIn::r.squaredGLMM(mixedModelAge_epaReceiving)
            R2m       R2c
[1,] 0.01285765 0.3377664
Code
emmeans::emmeans(mixedModelAge_epaReceiving, "contractYear")
 contractYear emmean     SE   df lower.CL upper.CL
 no            0.657 0.0346 1214    0.589    0.725
 yes           0.494 0.0467 2498    0.402    0.585

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

Scaled residuals: 
    Min      1Q  Median      3Q     Max 
-3.2503 -0.5270 -0.1438  0.4785  4.5956 

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.244      1.978 1334.194  38.543  < 2e-16 ***
contractYearyes  -14.199      2.032 3340.122  -6.989 3.32e-12 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Correlation of Fixed Effects:
            (Intr)
contrctYrys -0.258
Code
MuMIn::r.squaredGLMM(mixedModel_fantasyPtsReceiving)
             R2m       R2c
[1,] 0.007634808 0.5462717
Code
emmeans::emmeans(mixedModel_fantasyPtsReceiving, "contractYear")
 contractYear emmean   SE   df lower.CL upper.CL
 no             76.2 1.98 1207     72.4     80.1
 yes            62.0 2.44 2199     57.3     66.8

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

Scaled residuals: 
    Min      1Q  Median      3Q     Max 
-3.0668 -0.4967 -0.1292  0.4563  4.9582 

Random effects:
 Groups    Name          Variance Std.Dev. Corr 
 player_id (Intercept)   5863.26  76.572        
           ageCentered20   60.76   7.795   -0.71
 Residual                1960.17  44.274        
Number of obs: 3843, groups:  player_id, 1086

Fixed effects:
                         Estimate Std. Error         df t value Pr(>|t|)    
(Intercept)              43.34064    5.49900 1469.85276   7.882 6.25e-15 ***
contractYearyes         -10.48463    2.03309 3169.40708  -5.157 2.66e-07 ***
ageCentered20             7.30197    1.83894 2147.02160   3.971 7.40e-05 ***
ageCentered20Quadratic   -1.37759    0.09638 1290.30852 -14.293  < 2e-16 ***
years_of_experience      11.34115    1.36400 1283.33031   8.315 2.32e-16 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Correlation of Fixed Effects:
            (Intr) cntrcY agCn20 agC20Q
contrctYrys  0.128                     
ageCentrd20 -0.852 -0.161              
agCntrd20Qd  0.684  0.080 -0.675       
yrs_f_xprnc  0.306  0.032 -0.629 -0.076
Code
MuMIn::r.squaredGLMM(mixedModelAge_fantasyPtsReceiving)
           R2m       R2c
[1,] 0.1034356 0.6793804
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 2055     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.3.0     
 [9] ggplot2_3.5.2     tidyverse_2.0.0   emmeans_1.11.1    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.5

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.4   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.1    generics_0.1.4     
[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.4      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.