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_offense %>% 
  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),
    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) %>% 
  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
    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.9

Scaled residuals: 
    Min      1Q  Median      3Q     Max 
-3.2653 -0.5512  0.0912  0.5732  3.2574 

Random effects:
 Groups    Name        Variance Std.Dev.
 player_id (Intercept) 111.5    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.2361     0.8737 231.4158  50.628   <2e-16 ***
contractYearyes   0.2435     1.2011 950.7334   0.203    0.839    
---
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.923268e-05 0.3595778
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: 8848.2

Scaled residuals: 
    Min      1Q  Median      3Q     Max 
-3.3856 -0.5210  0.0932  0.5483  3.2851 

Random effects:
 Groups    Name          Variance Std.Dev. Corr 
 player_id (Intercept)   136.2765 11.6738       
           ageCentered20   0.4514  0.6718  -0.43
 Residual                191.2085 13.8278       
Number of obs: 1057, groups:  player_id, 250

Fixed effects:
                        Estimate Std. Error        df t value Pr(>|t|)    
(Intercept)             39.40393    2.29616 175.97837  17.161  < 2e-16 ***
contractYearyes          0.29562    1.23595 948.22429   0.239  0.81102    
ageCentered20            0.86786    0.64643 269.13650   1.343  0.18055    
ageCentered20Quadratic  -0.07586    0.02358 100.92420  -3.217  0.00174 ** 
years_of_experience      0.62472    0.54271 290.03262   1.151  0.25064    
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Correlation of Fixed Effects:
            (Intr) cntrcY agCn20 agC20Q
contrctYrys  0.056                     
ageCentrd20 -0.735 -0.062              
agCntrd20Qd  0.764  0.055 -0.631       
yrs_f_xprnc  0.096 -0.040 -0.657 -0.122
Code
MuMIn::r.squaredGLMM(mixedModelAge_qbr)
           R2m       R2c
[1,] 0.0132803 0.3959472
Code
emmeans::emmeans(mixedModelAge_qbr, "contractYear")
 contractYear emmean    SE  df lower.CL upper.CL
 no             44.1 0.913 241     42.3     45.9
 yes            44.4 1.320 708     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.7104 -0.4856  0.0805  0.5366  4.4281 

Random effects:
 Groups    Name        Variance Std.Dev.
 player_id (Intercept) 2.569    1.603   
 Residual              4.333    2.082   
Number of obs: 1063, groups:  player_id, 253

Fixed effects:
                 Estimate Std. Error        df t value Pr(>|t|)    
(Intercept)      -0.85473    0.13128 219.84986  -6.511 5.01e-10 ***
contractYearyes  -0.06891    0.17769 939.67476  -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.0001051826 0.3723006
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: 4833.1

Scaled residuals: 
    Min      1Q  Median      3Q     Max 
-4.8658 -0.4801  0.0835  0.5195  4.4083 

Random effects:
 Groups    Name          Variance Std.Dev. Corr 
 player_id (Intercept)   3.56554  1.8883        
           ageCentered20 0.01187  0.1089   -0.56
 Residual                4.17782  2.0440        
Number of obs: 1057, groups:  player_id, 250

Fixed effects:
                         Estimate Std. Error         df t value Pr(>|t|)    
(Intercept)             -1.607781   0.347938 169.870515  -4.621 7.53e-06 ***
contractYearyes         -0.080246   0.182799 937.931780  -0.439  0.66077    
ageCentered20            0.103772   0.096913 273.835497   1.071  0.28522    
ageCentered20Quadratic  -0.011186   0.003526 102.420446  -3.173  0.00199 ** 
years_of_experience      0.132677   0.080392 281.954383   1.650  0.09998 .  
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Correlation of Fixed Effects:
            (Intr) cntrcY agCn20 agC20Q
contrctYrys  0.056                     
ageCentrd20 -0.739 -0.063              
agCntrd20Qd  0.761  0.059 -0.641       
yrs_f_xprnc  0.104 -0.041 -0.658 -0.109
Code
MuMIn::r.squaredGLMM(mixedModelAge_ptsAdded)
            R2m       R2c
[1,] 0.01399559 0.4005138
Code
emmeans::emmeans(mixedModelAge_ptsAdded, "contractYear")
 contractYear emmean    SE  df lower.CL upper.CL
 no           -0.842 0.135 243    -1.11   -0.576
 yes          -0.923 0.196 710    -1.31   -0.538

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.6979   8.810 2.58e-16 ***
contractYearyes   0.4241     0.1504 928.0942   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.00497323 0.4486506
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: 4504.9

Scaled residuals: 
    Min      1Q  Median      3Q     Max 
-3.1322 -0.5027  0.0413  0.5370  4.2928 

Random effects:
 Groups    Name        Variance Std.Dev.
 player_id (Intercept) 2.417    1.555   
 Residual              2.994    1.730   
Number of obs: 1057, groups:  player_id, 250

Fixed effects:
                         Estimate Std. Error         df t value Pr(>|t|)   
(Intercept)             3.258e-01  2.815e-01  9.512e+02   1.157  0.24740   
contractYearyes         2.381e-01  1.549e-01  9.516e+02   1.537  0.12451   
ageCentered20           9.014e-03  8.120e-02  6.851e+02   0.111  0.91165   
ageCentered20Quadratic -5.585e-03  2.704e-03  1.012e+03  -2.065  0.03916 * 
years_of_experience     1.874e-01  7.157e-02  3.933e+02   2.618  0.00918 **
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Correlation of Fixed Effects:
            (Intr) cntrcY agCn20 agC20Q
contrctYrys  0.063                     
ageCentrd20 -0.712 -0.058              
agCntrd20Qd  0.741  0.057 -0.576       
yrs_f_xprnc  0.137 -0.045 -0.705 -0.136
Code
MuMIn::r.squaredGLMM(mixedModelAge_epaPass)
            R2m     R2c
[1,] 0.02982806 0.46316
Code
emmeans::emmeans(mixedModelAge_epaPass, "contractYear")
 contractYear emmean    SE  df lower.CL upper.CL
 no             1.19 0.124 262    0.947     1.43
 yes            1.43 0.172 689    1.091     1.77

Degrees-of-freedom method: kenward-roger 
Confidence level used: 0.95 
Code
# Placeholder for model predicting fantasy points

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

Scaled residuals: 
    Min      1Q  Median      3Q     Max 
-3.8771 -0.4656  0.0124  0.4849  6.4872 

Random effects:
 Groups    Name        Variance Std.Dev.
 player_id (Intercept) 0.3922   0.6262  
 Residual              1.1414   1.0684  
Number of obs: 1512, groups:  player_id, 482

Fixed effects:
                 Estimate Std. Error        df t value Pr(>|t|)    
(Intercept)     3.910e+00  4.481e-02 4.826e+02  87.271   <2e-16 ***
contractYearyes 5.126e-02  6.978e-02 1.421e+03   0.735    0.463    
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Correlation of Fixed Effects:
            (Intr)
contrctYrys -0.348
Code
MuMIn::r.squaredGLMM(mixedModel_ypc)
              R2m       R2c
[1,] 0.0003241364 0.2559497
Code
emmeans::emmeans(mixedModel_ypc, "contractYear")
 contractYear emmean     SE   df lower.CL upper.CL
 no             3.91 0.0448  556     3.82      4.0
 yes            3.96 0.0686 1151     3.83      4.1

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

Scaled residuals: 
    Min      1Q  Median      3Q     Max 
-4.0565 -0.4514 -0.0042  0.4678  6.2875 

Random effects:
 Groups    Name          Variance Std.Dev. Corr 
 player_id (Intercept)   1.12478  1.0606        
           ageCentered20 0.01468  0.1212   -0.78
 Residual                0.99234  0.9962        
Number of obs: 1511, groups:  player_id, 481

Fixed effects:
                         Estimate Std. Error         df t value Pr(>|t|)    
(Intercept)             4.274e+00  1.577e-01  5.092e+02  27.108  < 2e-16 ***
contractYearyes         1.877e-01  7.215e-02  1.307e+03   2.602  0.00938 ** 
ageCentered20          -1.001e-01  5.389e-02  5.980e+02  -1.857  0.06380 .  
ageCentered20Quadratic -1.654e-03  3.659e-03  3.355e+02  -0.452  0.65149    
years_of_experience     5.229e-02  3.309e-02  3.731e+02   1.580  0.11491    
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Correlation of Fixed Effects:
            (Intr) cntrcY agCn20 agC20Q
contrctYrys  0.161                     
ageCentrd20 -0.859 -0.177              
agCntrd20Qd  0.797  0.145 -0.812       
yrs_f_xprnc  0.025 -0.062 -0.395 -0.139
Code
MuMIn::r.squaredGLMM(mixedModelAge_ypc)
            R2m       R2c
[1,] 0.03118983 0.3813675
Code
emmeans::emmeans(mixedModelAge_ypc, "contractYear")
 contractYear emmean     SE   df lower.CL upper.CL
 no             3.85 0.0476  527     3.75     3.94
 yes            4.04 0.0706 1129     3.90     4.17

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

Scaled residuals: 
    Min      1Q  Median      3Q     Max 
-4.6912 -0.5189  0.0883  0.6114  3.4713 

Random effects:
 Groups    Name        Variance Std.Dev.
 player_id (Intercept) 0.1025   0.3202  
 Residual              0.9057   0.9517  
Number of obs: 1512, groups:  player_id, 482

Fixed effects:
                  Estimate Std. Error         df t value Pr(>|t|)    
(Intercept)       -0.65626    0.03296  576.28288 -19.912   <2e-16 ***
contractYearyes    0.04570    0.05926 1508.59478   0.771    0.441    
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Correlation of Fixed Effects:
            (Intr)
contrctYrys -0.428
Code
MuMIn::r.squaredGLMM(mixedModel_epaRush)
              R2m       R2c
[1,] 0.0003919375 0.1020542
Code
emmeans::emmeans(mixedModel_epaRush, "contractYear")
 contractYear emmean     SE   df lower.CL upper.CL
 no           -0.656 0.0330  574   -0.721   -0.591
 yes          -0.611 0.0542 1059   -0.717   -0.504

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

Scaled residuals: 
    Min      1Q  Median      3Q     Max 
-4.7511 -0.5077  0.0697  0.6018  3.1622 

Random effects:
 Groups    Name          Variance Std.Dev. Corr 
 player_id (Intercept)   0.316977 0.56301       
           ageCentered20 0.003557 0.05964  -0.85
 Residual                0.873051 0.93437       
Number of obs: 1511, groups:  player_id, 481

Fixed effects:
                         Estimate Std. Error         df t value Pr(>|t|)    
(Intercept)            -6.595e-01  1.249e-01  3.652e+02  -5.282  2.2e-07 ***
contractYearyes         7.934e-02  6.205e-02  1.328e+03   1.279  0.20123    
ageCentered20           5.740e-02  4.140e-02  3.535e+02   1.386  0.16656    
ageCentered20Quadratic -1.829e-03  2.853e-03  1.808e+02  -0.641  0.52234    
years_of_experience    -6.118e-02  2.287e-02  3.833e+02  -2.675  0.00779 ** 
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Correlation of Fixed Effects:
            (Intr) cntrcY agCn20 agC20Q
contrctYrys  0.150                     
ageCentrd20 -0.878 -0.197              
agCntrd20Qd  0.830  0.156 -0.853       
yrs_f_xprnc -0.046 -0.048 -0.301 -0.166
Code
MuMIn::r.squaredGLMM(mixedModelAge_epaRush)
            R2m      R2c
[1,] 0.01004318 0.139284
Code
emmeans::emmeans(mixedModelAge_epaRush, "contractYear")
 contractYear emmean     SE   df lower.CL upper.CL
 no           -0.667 0.0337  527   -0.733   -0.601
 yes          -0.588 0.0559 1051   -0.697   -0.478

Degrees-of-freedom method: kenward-roger 
Confidence level used: 0.95 
Code
# Placeholder for model predicting fantasy points

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

Scaled residuals: 
    Min      1Q  Median      3Q     Max 
-4.7774 -0.5657 -0.0860  0.5285  4.4960 

Random effects:
 Groups    Name        Variance Std.Dev.
 player_id (Intercept) 233.9    15.29   
 Residual              190.1    13.79   
Number of obs: 3181, groups:  player_id, 938

Fixed effects:
                 Estimate Std. Error        df t value Pr(>|t|)    
(Intercept)       29.2510     0.6018 1149.9717  48.605  < 2e-16 ***
contractYearyes   -3.7672     0.6298 2753.7386  -5.982 2.49e-09 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Correlation of Fixed Effects:
            (Intr)
contrctYrys -0.254
Code
MuMIn::r.squaredGLMM(mixedModel_receivingYards)
             R2m      R2c
[1,] 0.006673929 0.554657
Code
emmeans::emmeans(mixedModel_receivingYards, "contractYear")
 contractYear emmean    SE   df lower.CL upper.CL
 no             29.3 0.602 1035     28.1     30.4
 yes            25.5 0.752 1930     24.0     27.0

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

Scaled residuals: 
    Min      1Q  Median      3Q     Max 
-2.8562 -0.5484 -0.0835  0.4903  3.8045 

Random effects:
 Groups    Name          Variance Std.Dev. Corr 
 player_id (Intercept)   469.026  21.657        
           ageCentered20   5.791   2.406   -0.69
 Residual                143.432  11.976        
Number of obs: 3179, groups:  player_id, 937

Fixed effects:
                         Estimate Std. Error         df t value Pr(>|t|)    
(Intercept)              17.03925    1.61920 1201.85554  10.523  < 2e-16 ***
contractYearyes          -2.89879    0.61202 2537.07095  -4.736 2.30e-06 ***
ageCentered20             2.73922    0.56578 1782.22265   4.841 1.40e-06 ***
ageCentered20Quadratic   -0.43513    0.02993 1211.48663 -14.537  < 2e-16 ***
years_of_experience       3.05942    0.43371 1123.96294   7.054 3.03e-12 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Correlation of Fixed Effects:
            (Intr) cntrcY agCn20 agC20Q
contrctYrys  0.106                     
ageCentrd20 -0.794 -0.140              
agCntrd20Qd  0.710  0.071 -0.662       
yrs_f_xprnc  0.182  0.023 -0.633 -0.086
Code
MuMIn::r.squaredGLMM(mixedModelAge_receivingYards)
           R2m       R2c
[1,] 0.1199563 0.7144681
Code
emmeans::emmeans(mixedModelAge_receivingYards, "contractYear")
 contractYear emmean    SE   df lower.CL upper.CL
 no             27.9 0.642 1022     26.7     29.2
 yes            25.0 0.757 1741     23.5     26.5

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

Scaled residuals: 
    Min      1Q  Median      3Q     Max 
-4.1755 -0.5759 -0.0597  0.5249  3.9512 

Random effects:
 Groups    Name        Variance Std.Dev.
 player_id (Intercept) 0.5236   0.7236  
 Residual              1.2726   1.1281  
Number of obs: 3179, groups:  player_id, 938

Fixed effects:
                  Estimate Std. Error         df t value Pr(>|t|)    
(Intercept)        0.71504    0.03532 1272.25554  20.244   <2e-16 ***
contractYearyes   -0.11951    0.04929 3029.75001  -2.425   0.0154 *  
---
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.001594279 0.2926485
Code
emmeans::emmeans(mixedModel_epaReceiving, "contractYear")
 contractYear emmean     SE   df lower.CL upper.CL
 no            0.715 0.0353 1094    0.646    0.784
 yes           0.596 0.0494 2182    0.499    0.692

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

Scaled residuals: 
    Min      1Q  Median      3Q     Max 
-4.2197 -0.5696 -0.0529  0.5315  3.9167 

Random effects:
 Groups    Name          Variance Std.Dev. Corr 
 player_id (Intercept)   0.862888 0.9289        
           ageCentered20 0.006774 0.0823   -0.62
 Residual                1.209765 1.0999        
Number of obs: 3178, groups:  player_id, 937

Fixed effects:
                         Estimate Std. Error         df t value Pr(>|t|)    
(Intercept)             3.283e-01  1.082e-01  9.653e+02   3.034  0.00248 ** 
contractYearyes        -1.184e-01  5.147e-02  2.944e+03  -2.300  0.02155 *  
ageCentered20           8.990e-02  3.599e-02  1.087e+03   2.498  0.01265 *  
ageCentered20Quadratic -1.139e-02  2.039e-03  4.295e+02  -5.588  4.1e-08 ***
years_of_experience     7.517e-02  2.513e-02  1.015e+03   2.992  0.00284 ** 
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Correlation of Fixed Effects:
            (Intr) cntrcY agCn20 agC20Q
contrctYrys  0.115                     
ageCentrd20 -0.821 -0.177              
agCntrd20Qd  0.783  0.100 -0.739       
yrs_f_xprnc  0.068  0.030 -0.511 -0.136
Code
MuMIn::r.squaredGLMM(mixedModelAge_epaReceiving)
            R2m       R2c
[1,] 0.01846712 0.3424349
Code
emmeans::emmeans(mixedModelAge_epaReceiving, "contractYear")
 contractYear emmean     SE   df lower.CL upper.CL
 no            0.704 0.0367 1023    0.632    0.776
 yes           0.586 0.0504 2189    0.487    0.685

Degrees-of-freedom method: kenward-roger 
Confidence level used: 0.95 
Code
# Placeholder for model predicting fantasy points

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 22.04.5 LTS

Matrix products: default
BLAS:   /usr/lib/x86_64-linux-gnu/openblas-pthread/libblas.so.3 
LAPACK: /usr/lib/x86_64-linux-gnu/openblas-pthread/libopenblasp-r0.3.20.so;  LAPACK version 3.10.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.3   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.5    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.6.26      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-0         tools_4.4.2        
[16] utf8_1.2.4          yaml_2.3.10         data.table_1.16.2  
[19] knitr_1.49          htmlwidgets_1.6.4   mnormt_2.1.1       
[22] plyr_1.8.9          RColorBrewer_1.1-3  withr_3.0.2        
[25] foreign_0.8-87      numDeriv_2016.8-1.1 nnet_7.3-19        
[28] grid_4.4.2          stats4_4.4.2        fansi_1.0.6        
[31] lavaan_0.6-19       xtable_1.8-4        colorspace_2.1-1   
[34] scales_1.3.0        MASS_7.3-61         cli_3.6.3          
[37] mvtnorm_1.3-2       rmarkdown_2.29      generics_0.1.3     
[40] rstudioapi_0.17.1   tzdb_0.4.0          reshape2_1.4.4     
[43] minqa_1.2.8         DBI_1.2.3           cachem_1.1.0       
[46] splines_4.4.2       parallel_4.4.2      base64enc_0.1-3    
[49] mitools_2.4         vctrs_0.6.5         boot_1.3-31        
[52] jsonlite_1.8.9      hms_1.1.3           pbkrtest_0.5.3     
[55] Formula_1.2-5       htmlTable_2.4.3     glue_1.8.0         
[58] nloptr_2.1.1        stringi_1.8.4       gtable_0.3.6       
[61] quadprog_1.5-8      munsell_0.5.1       pillar_1.9.0       
[64] htmltools_0.5.8.1   R6_2.5.1            mix_1.0-12         
[67] evaluate_1.0.1      pbivnorm_0.6.0      lattice_0.22-6     
[70] backports_1.5.0     broom_1.0.7         memoise_2.0.1      
[73] Rcpp_1.0.13-1       coda_0.19-4.1       gridExtra_2.3      
[76] nlme_3.1-166        checkmate_2.3.2     xfun_0.49          
[79] 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.