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.

Opening an issue or submitting a pull request on GitHub: https://github.com/isaactpetersen/Fantasy-Football-Analytics-Textbook

Hypothesis Adding 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 (archived here) and 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: 8905.7

Scaled residuals: 
    Min      1Q  Median      3Q     Max 
-3.2658 -0.5446  0.0914  0.5732  3.2578 

Random effects:
 Groups    Name        Variance Std.Dev.
 player_id (Intercept) 111.6    10.56   
 Residual              198.6    14.09   
Number of obs: 1063, groups:  player_id, 253

Fixed effects:
                Estimate Std. Error       df t value Pr(>|t|)    
(Intercept)      44.2363     0.8738 231.4295  50.624   <2e-16 ***
contractYearyes   0.2432     1.2010 950.6905   0.202     0.84    
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Correlation of Fixed Effects:
            (Intr)
contrctYrys -0.241
Code
MuMIn::r.squaredGLMM(mixedModel_qbr)
              R2m      R2c
[1,] 2.915379e-05 0.359728
Code
emmeans::emmeans(mixedModel_qbr, "contractYear")
 contractYear emmean    SE  df lower.CL upper.CL
 no             44.2 0.874 262     42.5       46
 yes            44.5 1.300 752     41.9       47

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

Scaled residuals: 
    Min      1Q  Median      3Q     Max 
-3.3838 -0.5220  0.0928  0.5506  3.2853 

Random effects:
 Groups    Name          Variance Std.Dev. Corr 
 player_id (Intercept)   135.206  11.628        
           ageCentered20   0.433   0.658   -0.41
 Residual                191.543  13.840        
Number of obs: 1055, groups:  player_id, 249

Fixed effects:
                        Estimate Std. Error        df t value Pr(>|t|)    
(Intercept)             39.47167    2.29478 174.07664  17.201  < 2e-16 ***
contractYearyes          0.31817    1.23969 946.72121   0.257  0.79750    
ageCentered20            0.85795    0.64998 265.02249   1.320  0.18799    
ageCentered20Quadratic  -0.07527    0.02354  98.16828  -3.197  0.00187 ** 
years_of_experience      0.61694    0.54694 289.48076   1.128  0.26026    
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Correlation of Fixed Effects:
            (Intr) cntrcY agCn20 agC20Q
contrctYrys  0.055                     
ageCentrd20 -0.734 -0.059              
agCntrd20Qd  0.764  0.055 -0.628       
yrs_f_xprnc  0.100 -0.043 -0.662 -0.120
Code
MuMIn::r.squaredGLMM(mixedModelAge_qbr)
            R2m       R2c
[1,] 0.01316462 0.3966867
Code
emmeans::emmeans(mixedModelAge_qbr, "contractYear")
 contractYear emmean    SE  df lower.CL upper.CL
 no             44.1 0.916 240     42.3     45.9
 yes            44.4 1.330 706     41.8     47.0

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

Scaled residuals: 
    Min      1Q  Median      3Q     Max 
-4.7107 -0.4882  0.0804  0.5366  4.4282 

Random effects:
 Groups    Name        Variance Std.Dev.
 player_id (Intercept) 2.570    1.603   
 Residual              4.332    2.081   
Number of obs: 1063, groups:  player_id, 253

Fixed effects:
                 Estimate Std. Error        df t value Pr(>|t|)    
(Intercept)      -0.85472    0.13129 219.85741  -6.510 5.02e-10 ***
contractYearyes  -0.06893    0.17768 939.65392  -0.388    0.698    
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Correlation of Fixed Effects:
            (Intr)
contrctYrys -0.237
Code
MuMIn::r.squaredGLMM(mixedModel_ptsAdded)
              R2m       R2c
[1,] 0.0001052534 0.3723735
Code
emmeans::emmeans(mixedModel_ptsAdded, "contractYear")
 contractYear emmean    SE  df lower.CL upper.CL
 no           -0.855 0.131 262    -1.11   -0.596
 yes          -0.924 0.194 745    -1.31   -0.542

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

Scaled residuals: 
    Min      1Q  Median      3Q     Max 
-4.8624 -0.4856  0.0839  0.5194  4.4052 

Random effects:
 Groups    Name          Variance Std.Dev. Corr 
 player_id (Intercept)   3.54809  1.8836        
           ageCentered20 0.01153  0.1074   -0.55
 Residual                4.18409  2.0455        
Number of obs: 1055, groups:  player_id, 249

Fixed effects:
                         Estimate Std. Error         df t value Pr(>|t|)    
(Intercept)             -1.597976   0.347877 168.940692  -4.594 8.49e-06 ***
contractYearyes         -0.076689   0.183346 936.388386  -0.418  0.67584    
ageCentered20            0.100485   0.097484 270.759302   1.031  0.30356    
ageCentered20Quadratic  -0.011106   0.003522 100.614226  -3.153  0.00213 ** 
years_of_experience      0.133743   0.081042 281.434965   1.650  0.10000    
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Correlation of Fixed Effects:
            (Intr) cntrcY agCn20 agC20Q
contrctYrys  0.055                     
ageCentrd20 -0.737 -0.060              
agCntrd20Qd  0.761  0.059 -0.638       
yrs_f_xprnc  0.108 -0.044 -0.663 -0.107
Code
MuMIn::r.squaredGLMM(mixedModelAge_ptsAdded)
           R2m       R2c
[1,] 0.0139585 0.4014263
Code
emmeans::emmeans(mixedModelAge_ptsAdded, "contractYear")
 contractYear emmean    SE  df lower.CL upper.CL
 no           -0.846 0.136 242    -1.11   -0.579
 yes          -0.923 0.197 708    -1.31   -0.536

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

Scaled residuals: 
    Min      1Q  Median      3Q     Max 
-3.0315 -0.5088  0.0398  0.5664  4.3662 

Random effects:
 Groups    Name        Variance Std.Dev.
 player_id (Intercept) 2.454    1.566   
 Residual              3.049    1.746   
Number of obs: 1063, groups:  player_id, 253

Fixed effects:
                Estimate Std. Error       df t value Pr(>|t|)    
(Intercept)       1.0733     0.1218 239.6969   8.810 2.58e-16 ***
contractYearyes   0.4241     0.1504 928.0954   2.821   0.0049 ** 
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Correlation of Fixed Effects:
            (Intr)
contrctYrys -0.214
Code
MuMIn::r.squaredGLMM(mixedModel_epaPass)
             R2m       R2c
[1,] 0.004973305 0.4486446
Code
emmeans::emmeans(mixedModel_epaPass, "contractYear")
 contractYear emmean    SE  df lower.CL upper.CL
 no             1.07 0.122 263    0.833     1.31
 yes            1.50 0.172 699    1.159     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: 4496.2

Scaled residuals: 
    Min      1Q  Median      3Q     Max 
-3.1285 -0.5002  0.0415  0.5381  4.2903 

Random effects:
 Groups    Name        Variance Std.Dev.
 player_id (Intercept) 2.426    1.557   
 Residual              2.993    1.730   
Number of obs: 1055, groups:  player_id, 249

Fixed effects:
                         Estimate Std. Error         df t value Pr(>|t|)   
(Intercept)             3.315e-01  2.817e-01  9.490e+02   1.177  0.23962   
contractYearyes         2.468e-01  1.552e-01  9.498e+02   1.590  0.11219   
ageCentered20           2.496e-03  8.169e-02  6.844e+02   0.031  0.97563   
ageCentered20Quadratic -5.555e-03  2.704e-03  1.010e+03  -2.054  0.04022 * 
years_of_experience     1.935e-01  7.206e-02  3.935e+02   2.685  0.00756 **
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Correlation of Fixed Effects:
            (Intr) cntrcY agCn20 agC20Q
contrctYrys  0.063                     
ageCentrd20 -0.711 -0.055              
agCntrd20Qd  0.740  0.057 -0.574       
yrs_f_xprnc  0.141 -0.048 -0.709 -0.133
Code
MuMIn::r.squaredGLMM(mixedModelAge_epaPass)
            R2m       R2c
[1,] 0.03051935 0.4645375
Code
emmeans::emmeans(mixedModelAge_epaPass, "contractYear")
 contractYear emmean    SE  df lower.CL upper.CL
 no             1.19 0.124 260    0.942     1.43
 yes            1.43 0.173 687    1.094     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: 12460.3

Scaled residuals: 
    Min      1Q  Median      3Q     Max 
-3.8564 -0.5600 -0.0851  0.6047  2.8720 

Random effects:
 Groups    Name        Variance Std.Dev.
 player_id (Intercept) 5791     76.1    
 Residual              5041     71.0    
Number of obs: 1063, groups:  player_id, 253

Fixed effects:
                Estimate Std. Error      df t value Pr(>|t|)    
(Intercept)      103.696      5.628 293.908  18.424  < 2e-16 ***
contractYearyes  -25.186      6.171 935.684  -4.081 4.86e-05 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Correlation of Fixed Effects:
            (Intr)
contrctYrys -0.189
Code
MuMIn::r.squaredGLMM(mixedModel_fantasyPtsPass)
             R2m       R2c
[1,] 0.008874502 0.5387338
Code
emmeans::emmeans(mixedModel_fantasyPtsPass, "contractYear")
 contractYear emmean   SE  df lower.CL upper.CL
 no            103.7 5.63 262     92.6    114.8
 yes            78.5 7.53 635     63.7     93.3

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

Scaled residuals: 
    Min      1Q  Median      3Q     Max 
-3.8942 -0.5618 -0.0743  0.6125  2.7478 

Random effects:
 Groups    Name        Variance Std.Dev.
 player_id (Intercept) 5764     75.92   
 Residual              4923     70.17   
Number of obs: 1055, groups:  player_id, 249

Fixed effects:
                       Estimate Std. Error       df t value Pr(>|t|)    
(Intercept)            126.8448    12.0072 955.8120  10.564  < 2e-16 ***
contractYearyes        -20.8005     6.3731 948.2146  -3.264 0.001139 ** 
ageCentered20          -11.7659     3.5535 760.2272  -3.311 0.000973 ***
ageCentered20Quadratic  -0.1167     0.1117 994.4649  -1.044 0.296722    
years_of_experience     12.8752     3.2246 500.9729   3.993 7.51e-05 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Correlation of Fixed Effects:
            (Intr) cntrcY agCn20 agC20Q
contrctYrys  0.065                     
ageCentrd20 -0.693 -0.052              
agCntrd20Qd  0.713  0.054 -0.536       
yrs_f_xprnc  0.172 -0.045 -0.741 -0.134
Code
MuMIn::r.squaredGLMM(mixedModelAge_fantasyPtsPass)
            R2m       R2c
[1,] 0.05246165 0.5634855
Code
emmeans::emmeans(mixedModelAge_fantasyPtsPass, "contractYear")
 contractYear emmean   SE  df lower.CL upper.CL
 no            104.3 5.74 262     93.0    115.6
 yes            83.5 7.56 621     68.7     98.4

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

Scaled residuals: 
    Min      1Q  Median      3Q     Max 
-7.9077 -0.3904  0.0127  0.3862 14.7586 

Random effects:
 Groups    Name        Variance Std.Dev.
 player_id (Intercept) 0.4971   0.7051  
 Residual              1.9117   1.3826  
Number of obs: 1630, groups:  player_id, 509

Fixed effects:
                 Estimate Std. Error        df t value Pr(>|t|)    
(Intercept)     3.895e+00  5.279e-02 4.969e+02  73.779   <2e-16 ***
contractYearyes 2.525e-02  8.555e-02 1.568e+03   0.295    0.768    
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Correlation of Fixed Effects:
            (Intr)
contrctYrys -0.372
Code
MuMIn::r.squaredGLMM(mixedModel_ypc)
              R2m       R2c
[1,] 5.050989e-05 0.2064248
Code
emmeans::emmeans(mixedModel_ypc, "contractYear")
 contractYear emmean     SE   df lower.CL upper.CL
 no             3.89 0.0528  602     3.79     4.00
 yes            3.92 0.0822 1202     3.76     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: 5956.1

Scaled residuals: 
    Min      1Q  Median      3Q     Max 
-7.6944 -0.3728  0.0039  0.3912 14.1281 

Random effects:
 Groups    Name          Variance Std.Dev. Corr 
 player_id (Intercept)   0.32825  0.5729        
           ageCentered20 0.01158  0.1076   -0.30
 Residual                1.84189  1.3572        
Number of obs: 1628, groups:  player_id, 507

Fixed effects:
                         Estimate Std. Error         df t value Pr(>|t|)    
(Intercept)             4.222e+00  1.776e-01  6.717e+02  23.778   <2e-16 ***
contractYearyes         1.265e-01  9.244e-02  1.505e+03   1.368    0.171    
ageCentered20          -7.950e-02  6.140e-02  7.141e+02  -1.295    0.196    
ageCentered20Quadratic -3.626e-03  4.461e-03  3.764e+02  -0.813    0.417    
years_of_experience     5.539e-02  3.738e-02  4.306e+02   1.482    0.139    
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Correlation of Fixed Effects:
            (Intr) cntrcY agCn20 agC20Q
contrctYrys  0.157                     
ageCentrd20 -0.872 -0.173              
agCntrd20Qd  0.820  0.123 -0.824       
yrs_f_xprnc -0.070 -0.074 -0.292 -0.215
Code
MuMIn::r.squaredGLMM(mixedModelAge_ypc)
            R2m       R2c
[1,] 0.02173972 0.2602485
Code
emmeans::emmeans(mixedModelAge_ypc, "contractYear")
 contractYear emmean     SE   df lower.CL upper.CL
 no             3.85 0.0561  519     3.74     3.96
 yes            3.97 0.0860 1163     3.80     4.14

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

Scaled residuals: 
    Min      1Q  Median      3Q     Max 
-4.5842 -0.5074  0.0844  0.5913  3.4362 

Random effects:
 Groups    Name        Variance Std.Dev.
 player_id (Intercept) 0.1091   0.3303  
 Residual              0.9480   0.9737  
Number of obs: 1630, groups:  player_id, 509

Fixed effects:
                  Estimate Std. Error         df t value Pr(>|t|)    
(Intercept)       -0.65276    0.03262  612.67321 -20.011   <2e-16 ***
contractYearyes    0.04100    0.05822 1626.69621   0.704    0.481    
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Correlation of Fixed Effects:
            (Intr)
contrctYrys -0.429
Code
MuMIn::r.squaredGLMM(mixedModel_epaRush)
              R2m       R2c
[1,] 0.0003033165 0.1034567
Code
emmeans::emmeans(mixedModel_epaRush, "contractYear")
 contractYear emmean     SE   df lower.CL upper.CL
 no           -0.653 0.0326  617   -0.717   -0.589
 yes          -0.612 0.0532 1124   -0.716   -0.507

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

Scaled residuals: 
    Min      1Q  Median      3Q     Max 
-4.6207 -0.5022  0.0780  0.5854  3.4459 

Random effects:
 Groups    Name          Variance Std.Dev. Corr 
 player_id (Intercept)   0.228839 0.47837       
           ageCentered20 0.002858 0.05346  -0.73
 Residual                0.927374 0.96300       
Number of obs: 1628, groups:  player_id, 507

Fixed effects:
                         Estimate Std. Error         df t value Pr(>|t|)    
(Intercept)            -6.448e-01  1.230e-01  3.952e+02  -5.243 2.58e-07 ***
contractYearyes         7.553e-02  6.173e-02  1.440e+03   1.224   0.2213    
ageCentered20           4.237e-02  4.081e-02  3.764e+02   1.038   0.2999    
ageCentered20Quadratic -1.401e-03  2.871e-03  1.958e+02  -0.488   0.6261    
years_of_experience    -4.881e-02  2.240e-02  4.306e+02  -2.179   0.0298 *  
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Correlation of Fixed Effects:
            (Intr) cntrcY agCn20 agC20Q
contrctYrys  0.154                     
ageCentrd20 -0.886 -0.197              
agCntrd20Qd  0.833  0.153 -0.856       
yrs_f_xprnc -0.060 -0.059 -0.268 -0.193
Code
MuMIn::r.squaredGLMM(mixedModelAge_epaRush)
             R2m       R2c
[1,] 0.006806377 0.1273214
Code
emmeans::emmeans(mixedModelAge_epaRush, "contractYear")
 contractYear emmean     SE   df lower.CL upper.CL
 no           -0.664 0.0336  552   -0.730   -0.598
 yes          -0.589 0.0553 1111   -0.697   -0.480

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

Scaled residuals: 
    Min      1Q  Median      3Q     Max 
-3.0576 -0.5022 -0.1804  0.3944  3.8668 

Random effects:
 Groups    Name        Variance Std.Dev.
 player_id (Intercept) 2298     47.93   
 Residual              2109     45.92   
Number of obs: 1724, groups:  player_id, 525

Fixed effects:
                Estimate Std. Error       df t value Pr(>|t|)    
(Intercept)       64.887      2.543  630.937  25.513  < 2e-16 ***
contractYearyes  -11.289      2.998 1490.473  -3.765 0.000173 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Correlation of Fixed Effects:
            (Intr)
contrctYrys -0.238
Code
MuMIn::r.squaredGLMM(mixedModel_fantasyPtsRush)
             R2m       R2c
[1,] 0.005379707 0.5239906
Code
emmeans::emmeans(mixedModel_fantasyPtsRush, "contractYear")
 contractYear emmean   SE   df lower.CL upper.CL
 no             64.9 2.54  572     59.9     69.9
 yes            53.6 3.44 1223     46.8     60.4

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

Scaled residuals: 
    Min      1Q  Median      3Q     Max 
-3.2611 -0.4880 -0.1577  0.3980  3.6091 

Random effects:
 Groups    Name          Variance Std.Dev. Corr 
 player_id (Intercept)   4600.19  67.825        
           ageCentered20   39.61   6.294   -0.76
 Residual                1816.43  42.620        
Number of obs: 1722, groups:  player_id, 523

Fixed effects:
                        Estimate Std. Error        df t value Pr(>|t|)    
(Intercept)              57.4155     7.3822  669.8502   7.778 2.80e-14 ***
contractYearyes          -6.4763     3.1078 1459.9084  -2.084   0.0373 *  
ageCentered20            -0.6904     2.5240  872.0844  -0.274   0.7845    
ageCentered20Quadratic   -0.8427     0.1574  459.6570  -5.353 1.37e-07 ***
years_of_experience      10.7026     1.6790  619.9360   6.374 3.59e-10 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Correlation of Fixed Effects:
            (Intr) cntrcY agCn20 agC20Q
contrctYrys  0.176                     
ageCentrd20 -0.848 -0.171              
agCntrd20Qd  0.748  0.156 -0.770       
yrs_f_xprnc  0.155 -0.075 -0.505 -0.090
Code
MuMIn::r.squaredGLMM(mixedModelAge_fantasyPtsRush)
            R2m      R2c
[1,] 0.07533867 0.604956
Code
emmeans::emmeans(mixedModelAge_fantasyPtsRush, "contractYear")
 contractYear emmean   SE   df lower.CL upper.CL
 no             63.9 2.56  573     58.8     68.9
 yes            57.4 3.42 1200     50.7     64.1

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

Scaled residuals: 
    Min      1Q  Median      3Q     Max 
-4.8618 -0.5297 -0.1068  0.5113  4.5629 

Random effects:
 Groups    Name        Variance Std.Dev.
 player_id (Intercept) 280.0    16.73   
 Residual              184.1    13.57   
Number of obs: 3560, groups:  player_id, 1034

Fixed effects:
                 Estimate Std. Error        df t value Pr(>|t|)    
(Intercept)       25.1047     0.6073 1237.9557  41.336  < 2e-16 ***
contractYearyes   -3.4995     0.5868 3007.2060  -5.964 2.75e-09 ***
---
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_receivingYards)
             R2m       R2c
[1,] 0.005322244 0.6053999
Code
emmeans::emmeans(mixedModel_receivingYards, "contractYear")
 contractYear emmean    SE   df lower.CL upper.CL
 no             25.1 0.607 1129     23.9     26.3
 yes            21.6 0.740 2021     20.2     23.1

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

Scaled residuals: 
    Min      1Q  Median      3Q     Max 
-2.9222 -0.5213 -0.0916  0.4772  3.8044 

Random effects:
 Groups    Name          Variance Std.Dev. Corr 
 player_id (Intercept)   532.259  23.071        
           ageCentered20   6.139   2.478   -0.70
 Residual                137.910  11.744        
Number of obs: 3558, groups:  player_id, 1033

Fixed effects:
                         Estimate Std. Error         df t value Pr(>|t|)    
(Intercept)              13.42269    1.57295 1376.84753   8.533  < 2e-16 ***
contractYearyes          -2.83756    0.57200 2796.87598  -4.961 7.44e-07 ***
ageCentered20             2.11751    0.53966 2028.63105   3.924 9.01e-05 ***
ageCentered20Quadratic   -0.43924    0.02844 1383.02319 -15.445  < 2e-16 ***
years_of_experience       3.96901    0.41384 1221.21592   9.591  < 2e-16 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Correlation of Fixed Effects:
            (Intr) cntrcY agCn20 agC20Q
contrctYrys  0.121                     
ageCentrd20 -0.804 -0.149              
agCntrd20Qd  0.696  0.079 -0.663       
yrs_f_xprnc  0.211  0.021 -0.633 -0.082
Code
MuMIn::r.squaredGLMM(mixedModelAge_receivingYards)
           R2m       R2c
[1,] 0.1265075 0.7433555
Code
emmeans::emmeans(mixedModelAge_receivingYards, "contractYear")
 contractYear emmean    SE   df lower.CL upper.CL
 no             24.2 0.633 1134     23.0     25.5
 yes            21.4 0.734 1855     19.9     22.8

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

Scaled residuals: 
    Min      1Q  Median      3Q     Max 
-5.5671 -0.5715 -0.0364  0.5264  3.8987 

Random effects:
 Groups    Name        Variance Std.Dev.
 player_id (Intercept) 0.5536   0.7441  
 Residual              1.3060   1.1428  
Number of obs: 3490, groups:  player_id, 1018

Fixed effects:
                  Estimate Std. Error         df t value Pr(>|t|)    
(Intercept)        0.65163    0.03448 1371.93211  18.901  < 2e-16 ***
contractYearyes   -0.14793    0.04753 3317.24675  -3.112  0.00187 ** 
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Correlation of Fixed Effects:
            (Intr)
contrctYrys -0.356
Code
MuMIn::r.squaredGLMM(mixedModel_epaReceiving)
            R2m      R2c
[1,] 0.00238466 0.299384
Code
emmeans::emmeans(mixedModel_epaReceiving, "contractYear")
 contractYear emmean     SE   df lower.CL upper.CL
 no            0.652 0.0345 1199    0.584    0.719
 yes           0.504 0.0478 2355    0.410    0.597

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

Scaled residuals: 
    Min      1Q  Median      3Q     Max 
-5.4465 -0.5584 -0.0289  0.5261  3.8701 

Random effects:
 Groups    Name          Variance Std.Dev. Corr 
 player_id (Intercept)   0.873149 0.9344        
           ageCentered20 0.005505 0.0742   -0.62
 Residual                1.256079 1.1207        
Number of obs: 3489, groups:  player_id, 1017

Fixed effects:
                         Estimate Std. Error         df t value Pr(>|t|)    
(Intercept)             3.152e-01  1.055e-01  1.022e+03   2.988  0.00287 ** 
contractYearyes        -1.565e-01  4.996e-02  3.238e+03  -3.133  0.00174 ** 
ageCentered20           5.594e-02  3.443e-02  1.064e+03   1.625  0.10451    
ageCentered20Quadratic -9.972e-03  1.967e-03  4.232e+02  -5.070 5.95e-07 ***
years_of_experience     9.926e-02  2.348e-02  1.052e+03   4.228 2.57e-05 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Correlation of Fixed Effects:
            (Intr) cntrcY agCn20 agC20Q
contrctYrys  0.134                     
ageCentrd20 -0.836 -0.197              
agCntrd20Qd  0.785  0.120 -0.754       
yrs_f_xprnc  0.074  0.029 -0.486 -0.142
Code
MuMIn::r.squaredGLMM(mixedModelAge_epaReceiving)
            R2m      R2c
[1,] 0.01823977 0.334487
Code
emmeans::emmeans(mixedModelAge_epaReceiving, "contractYear")
 contractYear emmean     SE   df lower.CL upper.CL
 no            0.656 0.0354 1133    0.587    0.726
 yes           0.500 0.0486 2385    0.404    0.595

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

Scaled residuals: 
    Min      1Q  Median      3Q     Max 
-3.2907 -0.5245 -0.1494  0.4380  4.6460 

Random effects:
 Groups    Name        Variance Std.Dev.
 player_id (Intercept) 1247     35.31   
 Residual              1140     33.77   
Number of obs: 3560, groups:  player_id, 1034

Fixed effects:
                Estimate Std. Error       df t value Pr(>|t|)    
(Intercept)       49.380      1.342 1291.947   36.81  < 2e-16 ***
contractYearyes   -8.623      1.444 3112.753   -5.97 2.64e-09 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Correlation of Fixed Effects:
            (Intr)
contrctYrys -0.263
Code
MuMIn::r.squaredGLMM(mixedModel_fantasyPtsReceiving)
             R2m       R2c
[1,] 0.006276973 0.5253879
Code
emmeans::emmeans(mixedModel_fantasyPtsReceiving, "contractYear")
 contractYear emmean   SE   df lower.CL upper.CL
 no             49.4 1.34 1151     46.7     52.0
 yes            40.8 1.69 2167     37.4     44.1

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

Scaled residuals: 
    Min      1Q  Median      3Q     Max 
-3.1178 -0.4991 -0.1295  0.4295  4.9895 

Random effects:
 Groups    Name          Variance Std.Dev. Corr 
 player_id (Intercept)   2501.70  50.017        
           ageCentered20   26.88   5.185   -0.72
 Residual                 904.91  30.082        
Number of obs: 3558, groups:  player_id, 1033

Fixed effects:
                         Estimate Std. Error         df t value Pr(>|t|)    
(Intercept)              27.43781    3.69149 1339.44865   7.433 1.89e-13 ***
contractYearyes          -6.37326    1.43120 2951.87067  -4.453 8.78e-06 ***
ageCentered20             3.29076    1.24364 1919.16753   2.646  0.00821 ** 
ageCentered20Quadratic   -0.88027    0.06781 1135.74348 -12.981  < 2e-16 ***
years_of_experience       8.87280    0.91446 1172.95041   9.703  < 2e-16 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Correlation of Fixed Effects:
            (Intr) cntrcY agCn20 agC20Q
contrctYrys  0.127                     
ageCentrd20 -0.817 -0.163              
agCntrd20Qd  0.722  0.091 -0.697       
yrs_f_xprnc  0.178  0.024 -0.592 -0.091
Code
MuMIn::r.squaredGLMM(mixedModelAge_fantasyPtsReceiving)
           R2m       R2c
[1,] 0.1150593 0.6622461
Code
emmeans::emmeans(mixedModelAge_fantasyPtsReceiving, "contractYear")
 contractYear emmean   SE   df lower.CL upper.CL
 no             47.5 1.39 1137     44.7     50.2
 yes            41.1 1.68 2040     37.8     44.4

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.4.2 (2024-10-31)
Platform: x86_64-pc-linux-gnu
Running under: Ubuntu 24.04.1 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.2       readr_2.1.5       tidyr_1.3.1       tibble_3.2.1     
 [9] ggplot2_3.5.1     tidyverse_2.0.0   emmeans_1.10.6    MuMIn_1.48.4     
[13] lmerTest_3.1-3    lme4_1.1-35.5     Matrix_1.7-1      nflreadr_1.4.1   
[17] petersenlab_1.1.0

loaded via a namespace (and not attached):
 [1] tidyselect_1.2.1    psych_2.4.12        viridisLite_0.4.2  
 [4] fastmap_1.2.0       digest_0.6.37       rpart_4.1.23       
 [7] timechange_0.3.0    estimability_1.5.1  lifecycle_1.0.4    
[10] cluster_2.1.6       magrittr_2.0.3      compiler_4.4.2     
[13] rlang_1.1.4         Hmisc_5.2-1         tools_4.4.2        
[16] yaml_2.3.10         data.table_1.16.4   knitr_1.49         
[19] htmlwidgets_1.6.4   mnormt_2.1.1        plyr_1.8.9         
[22] RColorBrewer_1.1-3  foreign_0.8-87      withr_3.0.2        
[25] numDeriv_2016.8-1.1 nnet_7.3-19         grid_4.4.2         
[28] stats4_4.4.2        lavaan_0.6-19       xtable_1.8-4       
[31] colorspace_2.1-1    scales_1.3.0        MASS_7.3-61        
[34] cli_3.6.3           mvtnorm_1.3-2       rmarkdown_2.29     
[37] generics_0.1.3      rstudioapi_0.17.1   tzdb_0.4.0         
[40] reshape2_1.4.4      minqa_1.2.8         DBI_1.2.3          
[43] cachem_1.1.0        splines_4.4.2       parallel_4.4.2     
[46] base64enc_0.1-3     mitools_2.4         vctrs_0.6.5        
[49] boot_1.3-31         jsonlite_1.8.9      hms_1.1.3          
[52] pbkrtest_0.5.3      Formula_1.2-5       htmlTable_2.4.3    
[55] glue_1.8.0          nloptr_2.1.1        stringi_1.8.4      
[58] gtable_0.3.6        quadprog_1.5-8      munsell_0.5.1      
[61] pillar_1.10.0       htmltools_0.5.8.1   R6_2.5.1           
[64] mix_1.0-13          evaluate_1.0.1      pbivnorm_0.6.0     
[67] lattice_0.22-6      backports_1.5.0     broom_1.0.7        
[70] memoise_2.0.1       Rcpp_1.0.13-1       coda_0.19-4.1      
[73] gridExtra_2.3       nlme_3.1-166        checkmate_2.3.2    
[76] xfun_0.49           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.