Example Blog Post: Are Air Yards or Receiving Yards a Better Predictor of Future Fantasy Points?

template
fantasy football
Author

Insert Author’s Name Here

Published

July 14, 2025

This is an example blog post. Note: The post image was obtained from here.

1 Preamble

1.1 Load Libraries

Code
library("petersenlab") #located here: https://github.com/DevPsyLab/petersenlab
library("psych")
library("DescTools")
library("effectsize")
library("broom")
library("car")
library("caret")
library("lme4")
library("lmerTest")
library("GGally")
library("tidyverse")

2 AI Disclosure

AI Disclosure: AI was not used.

Link to AI Transcript: not applicable

3 Abstract

Background: Due to natural variability of players’ fantasy points from week to week, it is possible that a player’s opportunity (i.e., volume) may be a stronger influence of future performance than previous performance. However, this possibility has not been tested.
Method: In the present study, I compared predictors to determine which is a stronger predictor of future performance among wide receivers: previous performance or previous opportunity. For the predictors, I examined air yards as the index of a wide receiver’s opportunity and receiving yards as the index of player performance. For the criterion, I examined wide receivers’ fantasy points in the subsequent game.
Results: I found that previous performance (i.e., receiving yards) was a stronger predictor of fantasy performance (i.e., fantasy points) than was previous opportunity (i.e., air yards). Nevertheless, both previous receiving yards and air yards accounted for unique variance in future fantasy points.
Discussion: In conclusion, both previous performance and opportunity are important predictors of fantasy performance and should be considered.

4 Background

In football, like many other domains, a key predictor of future performance is past performance. Wide receivers who gain many receiving yards in a given week or season are more likely than other players to gain many receiving yards and score many points in future weeks or seasons. However, previous performance is not the only important predictor of future performance.

Early in the fantasy football season, there is considerable uncertainty regarding how players will perform. It is common for players to regress to the mean (Stewart, 2013), where players who do not meet expectations in Week 1 or 2 may rebound in subsequent weeks; and players who outperform their expectations may be a blip on the radar and revert to their expected performance. After several weeks into the season, players’ situations tend to become better understood and thus their performance may become more predictable (i.e., their expected mean may change from preseason expectations for the player).

Thus, it is important to consider factors in addition to previous performance that predict player performance. Key influences of player performance, in addition to player ability, include opportunity, usage, and volume. For a wide receiver, opportunity may encompass many aspects including how many snaps they receive, how many routes they run, how many targets they receive, and so on. Another key aspect of opportunity is the depth of the targets they receive—because greater target depth provides greater opportunity for them to obtain receiving yards and fantasy points. A key index of the length of the targets that a player receives is called “air yards” (Hermsmeyer, 2018).

Air yards on a given play are defined as the yardage distance the ball travels in air, from the line of scrimmage, to the point the receiver catches or does not catch the ball (Congelio, 2023). Air yard distance is tracked in yards from the line of scrimmage, not the true distance the ball travels in air (Hermsmeyer, 2018). Air yards are depicted visually in Figure 1. Air yards involve targeted yards—whether or not the pass was completed—and do not include yards after the catch. For a given play in which there is a completed pass, the passing or receiving yards on that play is equal to the sum of the air yards and yards after the catch on that play. Receiving air yards over the course of a game are calculated as the sum of air yards in that game for all passes targeting the player. Receiving air yards thus represent an important index of a wide receiver’s opportunity, volume, or usage (Hermsmeyer, 2018). Hereafter, I refer to receiving air yards as simply “air yards”.

Fantasy managers often must decide whether to trust a player’s previous production or to consider opportunity metrics such as air yards. Understanding which better predicts future fantasy performance can improve weekly start/sit decisions and waiver pickups. One of the goals in fantasy football is to predict which players will do well before they “blow up” and have a big game. Relying on receiving yards, touchdowns, and fantasy points may not be the optimal prognostic indicator of future performance. Although analyzing a wide receiver’s receiving yards or fantasy points tells us about the player’s outcome, it does not indicate how they arrived at that outcome—i.e., how the team used the player, not just what the they caught. It is possible, for instance, that players who get air yards (even if they do not always catch the passes) are more likely to score fantasy points next week than those who just had a good box score (i.e., many receiving yards) last week. Moreover, among underperforming players, considering their air yards may be useful for identifying the ones who are more likely to bounce back in subsequent weeks due to regression to the mean.

Visual Representation of Air Yards. From Congelio (2023).
Figure 1: Visual Representation of Air Yards. From Congelio (2023).

However, it is not known whether previous performance or opportunity is a stronger predictor of future performance.

4.1 The Present Study

In the present study, I examined whether previous performance or previous opportunity is a stronger predictor of future performance among wide receivers. For the predictors, I operationalized a wide receiver’s performance using their receiving yards, obtained from the nflreadr package (Ho & Carl, 2024). I operationalized a wide receiver’s opportunity using their air yards, obtained from the nflreadr package (Ho & Carl, 2024). For the criterion, I examined wide receivers’ fantasy points in the subsequent game, calculated by Petersen (2025) using the ffanalytics package (Andersen, Petersen, & Tungate, 2025). Thus, I examined the wide receiver’s fantasy points and air yards in a given week in predicting their fantasy points in the subsequent week. Although fantasy points are the ultimate outcome, I examined air yards (opportunity) and receiving yards (performance) as a predictor to isolate the predictive utility of yardage-related receiving opportunity relative to yardage-related receiving production. To my knowledge, this is the first study to examine whether previous performance or opportunity is a stronger predictor of future fantasy performance.

I hypothesized that, due to natural variability from week-to-week that, in part, reflects randomness and is consistent with the statistical principle of regression to the mean, opportunity would be a stronger influence of subsequent performance than previous performance. If the hypothesis is true, I predict that air yards will be a stronger predictor, compared to actual receiving yards, in predicting future fantasy points. Testing this possibility is important for determining what factors may be used to best identify players who, despite not gaining many receiving yards in a given week, may do well in future weeks.

5 Method

To test the hypothesis, I examined whether a wide receiver’s air yards in a given week was a better predictor than their actual receiving yards of the player’s fantasy points the following week.

5.1 Data

Below, I load the data file used for this post. The data file includes weekly player statistics, including air yards, receiving yards, and fantasy points. I obtained the data file from the Open Science Framework repository for the textbook, “Fantasy Football Analytics: Statistics, Prediction, and Empiricism Using R” (Petersen, 2025).

Code
load(file = "../../data/player_stats_weekly.RData")

First, I subset the data to Wide Receivers:

Code
analysisData <- player_stats_weekly %>% 
  filter(position_group == "WR")

The weekly data include all seasons since 1999 (i.e., currently 1999–2024):

Code
analysisData %>% 
  arrange(season) %>% 
  select(season) %>% 
  pull() %>% 
  unique()
 [1] 1999 2000 2001 2002 2003 2004 2005 2006 2007 2008 2009 2010 2011 2012 2013
[16] 2014 2015 2016 2017 2018 2019 2020 2021 2022 2023 2024

Second, to prepare the data for analysis, I created a lagged variable for fantasy points so that fantasy points in a subsequent year (e.g., 2024) are in the same row as player performance from the prior year (e.g., 2023). Creation of a lagged variable allowed using previous performance to predict future fantasy points.

The original data set includes only those rows for weeks in which a given player played. For instance, if a player played weeks 9 and 11 but missed week 10 due to injury, they would have rows for only week 9 and 11. However, this is a problem if we were to lag a variable (fantasy points) up a row because, in this instance, it would be lagging the variable two weeks (from week 11 to week 9), rather than one week. To ensure that the lagged variable lags only 1 week (and not more weeks), I added missing rows for the missing weeks for given player-season combinations, to ensure each player-season combination had all 18 weeks represented.

Code
analysisData <- analysisData %>%
  group_by(player_id, season) %>%
  tidyr::complete(week = 1:18) %>%
  ungroup()

Then, I created the lagged variable for fantasy points. I lagged fantasy points within season, so week 1 data did not get lagged to the final week of the prior season.

Code
analysisData <- analysisData %>% 
  arrange(player_id, season, week) %>% 
  group_by(player_id, season) %>% 
  mutate(
    fantasyPoints_lag = lead(fantasyPoints)
  ) %>% 
  ungroup()

Below, I verify that the lagging worked as expected:

Code
analysisData %>% 
  select(player_id, player_display_name, season, week, fantasyPoints, fantasyPoints_lag)

Here are descriptive statistics for the variables that are the focus of this article:

Code
modelVars <- c("receiving_air_yards","receiving_yards","fantasyPoints","fantasyPoints_lag")

analysisData %>% 
  dplyr::select(all_of(modelVars)) %>% 
  dplyr::summarise(across(
      everything(),
      .fns = list(
        n = ~ length(na.omit(.)),
        missingness = ~ mean(is.na(.)) * 100,
        M = ~ mean(., na.rm = TRUE),
        SD = ~ sd(., na.rm = TRUE),
        min = ~ min(., na.rm = TRUE),
        max = ~ max(., na.rm = TRUE),
        q10 = ~ quantile(., .10, na.rm = TRUE), # 10th quantile
        q90 = ~ quantile(., .90, na.rm = TRUE), # 90th quantile
        range = ~ max(., na.rm = TRUE) - min(., na.rm = TRUE),
        IQR = ~ IQR(., na.rm = TRUE),
        MAD = ~ mad(., na.rm = TRUE),
        CV = ~ sd(., na.rm = TRUE) / mean(., na.rm = TRUE),
        median = ~ median(., na.rm = TRUE),
        pseudomedian = ~ DescTools::HodgesLehmann(., na.rm = TRUE),
        mode = ~ petersenlab::Mode(., multipleModes = "mean"),
        skewness = ~ psych::skew(., na.rm = TRUE),
        kurtosis = ~ psych::kurtosi(., na.rm = TRUE)),
      .names = "{.col}.{.fn}")) %>%
    tidyr::pivot_longer(
      cols = everything(),
      names_to = c("variable","index"),
      names_sep = "\\.") %>% 
    tidyr::pivot_wider(
      names_from = index,
      values_from = value)

The lagged variable of fantasy points has fewer observations than the original fantasy points variable because, as mentioned earlier, I lagged data within seasons; consequently, week 1 data were not lagged to the prior season and thus were not retained in the lagged variable.

Here is a correlation matrix of the relevant variables:

Code
petersenlab::cor.table(analysisData[,modelVars])

Figure 2 depicts a correlation matrix plot of the relevant variables.

Code
GGally::ggcorr(
  analysisData[,modelVars],
  label = TRUE,
  label_round = 2)
Correlation Matrix Plot.
Figure 2: Correlation Matrix Plot.

5.2 Statistical Analysis

I used multiple regression to test the hypothesis. Specifically, I compared the standardized regression coefficients for the two predictors of interest: air yards versus receiving yards.

6 Results

6.1 Main Analyses

Figure 3 depicts the association between air yards in a given game and their fantasy points the following week among wide receivers.

Code
ggplot2::ggplot(
  data = analysisData,
  aes(
    x = receiving_air_yards,
    y = fantasyPoints_lag)) +
  geom_point(alpha = 0.15) +
  geom_smooth() +
  coord_cartesian(
    xlim = c(0,NA),
    ylim = c(0,NA),
    expand = FALSE) +
  labs(
    x = "Air Yards (Game)",
    y = "Fantasy Points in the Following Week",
    title = "Fantasy Points by the Previous Week's Air Yards",
    subtitle = "(Among Wide Receivers)"
  ) +
  theme_classic()
Association Between Air Yards and the Player's Fantasy Points in the Following Week.
Figure 3: Association Between Air Yards and the Player’s Fantasy Points in the Following Week.

Figure 4 depicts the association between receiving yards in a given game and their fantasy points the following week among wide receivers.

Code
ggplot2::ggplot(
  data = analysisData,
  aes(
    x = receiving_yards,
    y = fantasyPoints_lag)) +
  geom_point(alpha = 0.15) +
  geom_smooth() +
  coord_cartesian(
    xlim = c(0,NA),
    ylim = c(0,NA),
    expand = FALSE) +
  labs(
    x = "Receiving Yards (Game)",
    y = "Fantasy Points in the Following Week",
    title = "Fantasy Points by the Previous Week Receiving Yards",
    subtitle = "(Among Wide Receivers)"
  ) +
  theme_classic()
Association Between Receiving Yards and the Player's Fantasy Points in the Following Week.
Figure 4: Association Between Receiving Yards and the Player’s Fantasy Points in the Following Week.

Below I fit the unstandardized regression model:

Code
regressionModel <- lm(
  formula = fantasyPoints_lag ~ receiving_air_yards + receiving_yards,
  data = analysisData
)

Below, I fit the standardized regression model (by standardizing all of the terms):

Code
regressionModelStandardized <- lm(
  formula = scale(fantasyPoints_lag) ~ scale(receiving_air_yards) + scale(receiving_yards),
  data = analysisData
)

Here are the model results:

Code
summary(regressionModel)

Call:
lm(formula = fantasyPoints_lag ~ receiving_air_yards + receiving_yards, 
    data = analysisData)

Residuals:
    Min      1Q  Median      3Q     Max 
-26.625  -5.185  -2.057   3.649  52.936 

Coefficients:
                     Estimate Std. Error t value Pr(>|t|)    
(Intercept)         5.1852278  0.0516265  100.44   <2e-16 ***
receiving_air_yards 0.0133909  0.0008666   15.45   <2e-16 ***
receiving_yards     0.0765063  0.0010604   72.15   <2e-16 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Residual standard error: 7.354 on 42771 degrees of freedom
  (54228 observations deleted due to missingness)
Multiple R-squared:  0.167, Adjusted R-squared:  0.167 
F-statistic:  4288 on 2 and 42771 DF,  p-value: < 2.2e-16

Here are the standardized regression coefficients:

Code
print(effectsize::standardize_parameters(regressionModel, method = "refit"), digits = 2)
# Standardization method: refit

Parameter           | Std. Coef. |        95% CI
------------------------------------------------
(Intercept)         |   3.71e-17 | [-0.01, 0.01]
receiving air yards |       0.08 | [ 0.07, 0.09]
receiving yards     |       0.36 | [ 0.35, 0.37]

The standardized regression coefficients are depicted in Figure 5.

Code
regressionModelStandardized_tidy <- broom::tidy(
  regressionModelStandardized,
  conf.int = TRUE)

ggplot(
  regressionModelStandardized_tidy,
  aes(
    x = term,
    y = estimate)) +
  geom_point() +
  geom_errorbar(
    aes(
      ymin = conf.low,
      ymax = conf.high),
    width = 0.2) +
  coord_flip(ylim = c(0, NA)) +
  labs(
    title = "Standardized Regression Coefficients with 95% CI",
    x = "Predictor",
    y = "Standardized Coefficient Estimate") +
  theme_minimal()
Standardized Regression Coefficients with 95% Confidence Interval.
Figure 5: Standardized Regression Coefficients with 95% Confidence Interval.

The standardized regression coefficient for receiving yards was larger than for air yards. As shown in Figure 5, their 95% confidence intervals did not overlap, indicating that the standardized regression coefficient for receiving yards was larger than for air yards. The standardized regression coefficient for air yards in predicting fantasy points was \(\beta = 0.08\). The standardized regression coefficient for receiving yards in predicting fantasy points was \(\beta = 0.36\). Thus, receiving yards had a medium effect size, whereas air yards had a small effect size.

6.2 Sensitivity Analyses

We considered assumptions of multiple regression: 1) linear association, 2) homoscedasticity, 3) uncorrelated residuals, and 4) normally distributed residuals. To evaluate the assumption of a linear association between the predictor variables and outcome variable, we examined the shape of the associations of air yards and receiving yards with fantasy points using line graphs. The associations of air yards and receiving yards with fantasy points were mostly linear, as depicted in Figures 3 and 4.

To evaluate the assumption of homoscedasticity, we examined residual and spread-level plots.

Residual plots is in Figure 6.

Code
car::residualPlots(
  regressionModel,
  id = TRUE)
                    Test stat Pr(>|Test stat|)    
receiving_air_yards   -14.982        < 2.2e-16 ***
receiving_yards       -32.962        < 2.2e-16 ***
Tukey test            -35.378        < 2.2e-16 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Residual Plots.
Figure 6: Residual Plots.

A spread-level plot is in Figure 7.

Code
car::spreadLevelPlot(
  regressionModel,
  id = TRUE)

Suggested power transformation:  0.5426738 
Spread-Level Plot.
Figure 7: Spread-Level Plot.

The residuals appeared to increase as a function of the fitted values. To handle this, we conducted a sensitivity analysis by transforming the outcome variable to be more normally distributed using a Yeo-Johnson transformation.

Code
yjTransformed <- caret::preProcess(
  analysisData["fantasyPoints_lag"],
  method = c("YeoJohnson"))

yjTransformed
Created from 52945 samples and 1 variables

Pre-processing:
  - ignored (0)
  - Yeo-Johnson transformation (1)

Lambda estimates for Yeo-Johnson transformation:
0.18
Code
analysisData$fantasyPoints_lag_transformed <- predict(
  yjTransformed,
  newdata = analysisData["fantasyPoints_lag"])$fantasyPoints_lag

A histogram of raw fantasy points is in Figure 8.

Code
ggplot2::ggplot(
  data = analysisData,
  mapping = aes(
    x = fantasyPoints_lag)
) +
  geom_histogram(
    color = "#000000",
    fill = "#0099F8"
  ) +
  labs(
    x = "Fantasy Points in a Week",
    y = "Count",
    title = "Histogram of Fantasy Points (Week)",
    subtitle = "(Among Wide Receivers)"
  ) +
  theme_classic() +
  theme(axis.title.y = element_text(angle = 0, vjust = 0.5))
`stat_bin()` using `bins = 30`. Pick better value with `binwidth`.
Warning: Removed 44057 rows containing non-finite outside the scale range
(`stat_bin()`).
Histogram of Fantasy Points.
Figure 8: Histogram of Fantasy Points.

A histogram of the transformed variable is in Figure 9.

Code
ggplot2::ggplot(
  data = analysisData,
  mapping = aes(
    x = fantasyPoints_lag_transformed)
) +
  geom_histogram(
    color = "#000000",
    fill = "#0099F8"
  ) +
  labs(
    x = "Fantasy Points (Transformed) in a Week",
    y = "Count",
    title = "Histogram of Fantasy Points (Transformed)",
    subtitle = "(Among Wide Receivers)"
  ) +
  theme_classic() +
  theme(axis.title.y = element_text(angle = 0, vjust = 0.5))
`stat_bin()` using `bins = 30`. Pick better value with `binwidth`.
Warning: Removed 44057 rows containing non-finite outside the scale range
(`stat_bin()`).
Histogram of Fantasy Points (Transformed).
Figure 9: Histogram of Fantasy Points (Transformed).

The results of the regression model are below:

Code
regressionModelStandardized_transformed <- lm(
  formula = scale(fantasyPoints_lag_transformed) ~ scale(receiving_air_yards) + scale(receiving_yards),
  data = analysisData
)

summary(regressionModelStandardized_transformed)

Call:
lm(formula = scale(fantasyPoints_lag_transformed) ~ scale(receiving_air_yards) + 
    scale(receiving_yards), data = analysisData)

Residuals:
    Min      1Q  Median      3Q     Max 
-4.2474 -0.6658  0.0210  0.6598  2.8172 

Coefficients:
                           Estimate Std. Error t value Pr(>|t|)    
(Intercept)                0.053162   0.004308   12.34   <2e-16 ***
scale(receiving_air_yards) 0.083806   0.004755   17.62   <2e-16 ***
scale(receiving_yards)     0.381033   0.004806   79.28   <2e-16 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Residual standard error: 0.8881 on 42771 degrees of freedom
  (54228 observations deleted due to missingness)
Multiple R-squared:  0.1963,    Adjusted R-squared:  0.1963 
F-statistic:  5224 on 2 and 42771 DF,  p-value: < 2.2e-16

The pattern of findings did not differ substantially when transforming the outcome variable to be more normally distributed.

Because the data were nested within players (i.e., longitudinal) and thus violated the assumption of uncorrelated residuals, we also conducted a sensitivity analysis using a mixed model to account for the nested data.

Code
mixedModelStandardized <- lmerTest::lmer(
  scale(fantasyPoints_lag_transformed) ~ scale(receiving_air_yards) + scale(receiving_yards) + (1 | player_id),
  data = analysisData,
  REML = FALSE,
  control = lmerControl(optimizer = "bobyqa")
)

summary(mixedModelStandardized)
Linear mixed model fit by maximum likelihood . t-tests use Satterthwaite's
  method [lmerModLmerTest]
Formula: scale(fantasyPoints_lag_transformed) ~ scale(receiving_air_yards) +  
    scale(receiving_yards) + (1 | player_id)
   Data: analysisData
Control: lmerControl(optimizer = "bobyqa")

      AIC       BIC    logLik -2*log(L)  df.resid 
 103536.4  103579.7  -51763.2  103526.4     42769 

Scaled residuals: 
    Min      1Q  Median      3Q     Max 
-4.7444 -0.6697  0.0106  0.7105  3.2985 

Random effects:
 Groups    Name        Variance Std.Dev.
 player_id (Intercept) 0.2559   0.5059  
 Residual              0.6177   0.7859  
Number of obs: 42774, groups:  player_id, 1238

Fixed effects:
                             Estimate Std. Error         df t value Pr(>|t|)
(Intercept)                -2.194e-01  1.599e-02  1.123e+03 -13.720  < 2e-16
scale(receiving_air_yards)  3.645e-02  5.448e-03  4.275e+04   6.691 2.25e-11
scale(receiving_yards)      1.590e-01  4.964e-03  4.263e+04  32.037  < 2e-16
                              
(Intercept)                ***
scale(receiving_air_yards) ***
scale(receiving_yards)     ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Correlation of Fixed Effects:
            (Intr) sc(__)
scl(rcvn__)  0.017       
scl(rcvng_)  0.049 -0.446

The pattern of findings did not differ substantially when using a mixed model and the transformed outcome variable.

To evaluate the assumption of normally distributed residuals, we examined a quantile–quantile (QQ) plot, in Figure 10.

Code
car::qqPlot(
  regressionModel,
  main = "QQ Plot",
  id = TRUE)
[1] 12169 92151
Quantile–Quantile (QQ) Plot.
Figure 10: Quantile–Quantile (QQ) Plot.

The residuals showed some deviations from normality. However, the residuals from the model with a transformed outcome variable showed some improvements in terms of deviations from normality, as depicted in Figure 11.

Code
car::qqPlot(
  regressionModelStandardized_transformed,
  main = "QQ Plot (Transformed)",
  id = TRUE)
[1] 21853 47581
Quantile–Quantile (QQ) Plot (Transformed).
Figure 11: Quantile–Quantile (QQ) Plot (Transformed).

In sum, the pattern of findings held after conducting sensitivity analyses to account for potential violations of assumptions of multiple regression.

7 Discussion

In this article, I examined whether previous performance or opportunity is a stronger predictor of future fantasy performance among wide receivers. For the predictors, I operationalized the wide receiver’s performance as their receiving yards. I operationalized a wide receiver’s opportunity as their air yards, an index of the depth of targets the player received. For the criterion of fantasy performance, I examined wide receivers’ fantasy points in the subsequent game. I hypothesized that opportunity would be a stronger influence of subsequent performance than previous performance and that, as a result, air yards will be a stronger predictor, compared to actual receiving yards, in predicting future fantasy points among wide receivers. Contrary to my hypothesis, however, I found that receiving yards was a stronger predictor than air yards in predicting wide receivers’ fantasy points the following week. Receiving yards had a medium effect size; air yards had a small effect size.

Nevertheless, both previous fantasy points and air yards accounted for unique variance in future fantasy points. The findings held when conducting sensitivity analyses, including transforming the outcome variable to be more normally distributed and using a mixed model to account for the longitudinal nature of the data. Thus, findings suggest that, although previous performance is a stronger predictor than previous opportunity in predicting future fantasy performance among wide receivers, both previous performance and opportunity are important predictors of fantasy performance that should be considered. A possible alternative interpretation of the findings is that previous performance reflects other causal processes (such as ability and opportunity) and that previous performance does not influence future performance per se.

7.1 Strengths and Limitations

The study had several strengths. First, I examined data from all seasons since 1999, thus providing strong statistical power to detect differences in the strength of associations. Second, I conducted several sensitivity analyses, providing additional rigor in the methods and additional confidence in the findings.

The study also had limitations. First, air yards and other statistical indexes depend on many factors such as the game script and, in a given game, may not be representative of a player’s role in the offense. Second, a player’s inclusion of a given week in the analysis required the player to play in the given game and the following week. Player availability could depend, in part, due to usage; for instance, receivers who run more routes, receive more targets, and obtain more air/receiving yards could be more likely to miss a subsequent game due to injury and thus could be missing systematically from my analysis. This possibility would be important for future work to consider.

7.2 Conclusion

In conclusion, previous performance is a stronger predictor than previous opportunity in predicting future fantasy performance among wide receivers. However, both previous performance and opportunity are important predictors of fantasy performance and should be considered when predicting wide receiver fantasy performance. Among underperforming players, air yards—as an index of opportunity—may be a useful way of identifying those who may bounce back in subsequent weeks.

8 References

Andersen, D., Petersen, I. T., & Tungate, A. (2025). ffanalytics: Scrape data for fantasy football. https://github.com/FantasyFootballAnalytics/ffanalytics

Congelio, B. J. (2023). Introduction to NFL analytics with R. CRC Press. https://doi.org/10.1201/9781003364320

Ho, T., & Carl, S. (2024). nflreadr: Download nflverse data. https://doi.org/10.32614/CRAN.package.nflreadr

Petersen, I. T. (2025). Fantasy football analytics: Statistics, prediction, and empiricism using R. University of Iowa Libraries. https://isaactpetersen.github.io/Fantasy-Football-Analytics-Textbook

9 Session Info

Code
sessionInfo()
R version 4.5.1 (2025-06-13)
Platform: x86_64-pc-linux-gnu
Running under: Ubuntu 24.04.2 LTS

Matrix products: default
BLAS:   /usr/lib/x86_64-linux-gnu/openblas-pthread/libblas.so.3 
LAPACK: /usr/lib/x86_64-linux-gnu/openblas-pthread/libopenblasp-r0.3.26.so;  LAPACK version 3.12.0

locale:
 [1] LC_CTYPE=C.UTF-8       LC_NUMERIC=C           LC_TIME=C.UTF-8       
 [4] LC_COLLATE=C.UTF-8     LC_MONETARY=C.UTF-8    LC_MESSAGES=C.UTF-8   
 [7] LC_PAPER=C.UTF-8       LC_NAME=C              LC_ADDRESS=C          
[10] LC_TELEPHONE=C         LC_MEASUREMENT=C.UTF-8 LC_IDENTIFICATION=C   

time zone: UTC
tzcode source: system (glibc)

attached base packages:
[1] stats     graphics  grDevices utils     datasets  methods   base     

other attached packages:
 [1] lubridate_1.9.4   forcats_1.0.0     stringr_1.5.1     dplyr_1.1.4      
 [5] purrr_1.1.0       readr_2.1.5       tidyr_1.3.1       tibble_3.3.0     
 [9] tidyverse_2.0.0   GGally_2.3.0      lmerTest_3.1-3    lme4_1.1-37      
[13] Matrix_1.7-3      caret_7.0-1       lattice_0.22-7    ggplot2_3.5.2    
[17] car_3.1-3         carData_3.0-5     broom_1.0.9       effectsize_1.0.1 
[21] DescTools_0.99.60 psych_2.5.6       petersenlab_1.1.7

loaded via a namespace (and not attached):
  [1] RColorBrewer_1.1-3   rstudioapi_0.17.1    jsonlite_2.0.0      
  [4] datawizard_1.2.0     magrittr_2.0.3       TH.data_1.1-3       
  [7] estimability_1.5.1   farver_2.1.2         nloptr_2.2.1        
 [10] rmarkdown_2.29       fs_1.6.6             vctrs_0.6.5         
 [13] minqa_1.2.8          base64enc_0.1-3      htmltools_0.5.8.1   
 [16] haven_2.5.5          cellranger_1.1.0     Formula_1.2-5       
 [19] pROC_1.19.0.1        parallelly_1.45.1    htmlwidgets_1.6.4   
 [22] plyr_1.8.9           sandwich_3.1-1       emmeans_1.11.2      
 [25] rootSolve_1.8.2.4    zoo_1.8-14           lifecycle_1.0.4     
 [28] iterators_1.0.14     pkgconfig_2.0.3      R6_2.6.1            
 [31] fastmap_1.2.0        rbibutils_2.3        future_1.67.0       
 [34] numDeriv_2016.8-1.1  digest_0.6.37        Exact_3.3           
 [37] colorspace_2.1-1     Hmisc_5.2-3          labeling_0.4.3      
 [40] timechange_0.3.0     mgcv_1.9-3           httr_1.4.7          
 [43] abind_1.4-8          compiler_4.5.1       proxy_0.4-27        
 [46] withr_3.0.2          S7_0.2.0             htmlTable_2.4.3     
 [49] backports_1.5.0      DBI_1.2.3            ggstats_0.10.0      
 [52] MASS_7.3-65          lava_1.8.1           gld_2.6.7           
 [55] ModelMetrics_1.2.2.2 tools_4.5.1          pbivnorm_0.6.0      
 [58] foreign_0.8-90       future.apply_1.20.0  nnet_7.3-20         
 [61] glue_1.8.0           quadprog_1.5-8       nlme_3.1-168        
 [64] grid_4.5.1           checkmate_2.3.2      cluster_2.1.8.1     
 [67] reshape2_1.4.4       generics_0.1.4       recipes_1.3.1       
 [70] gtable_0.3.6         tzdb_0.5.0           class_7.3-23        
 [73] data.table_1.17.8    lmom_3.2             hms_1.1.3           
 [76] foreach_1.5.2        pillar_1.11.0        mitools_2.4         
 [79] splines_4.5.1        survival_3.8-3       tidyselect_1.2.1    
 [82] mix_1.0-13           knitr_1.50           reformulas_0.4.1    
 [85] gridExtra_2.3        stats4_4.5.1         xfun_0.52           
 [88] expm_1.0-0           hardhat_1.4.1        timeDate_4041.110   
 [91] stringi_1.8.7        yaml_2.3.10          boot_1.3-31         
 [94] evaluate_1.0.4       codetools_0.2-20     cli_3.6.5           
 [97] rpart_4.1.24         xtable_1.8-4         parameters_0.27.0   
[100] Rdpack_2.6.4         lavaan_0.6-19        Rcpp_1.1.0          
[103] readxl_1.4.5         globals_0.18.0       coda_0.19-4.1       
[106] parallel_4.5.1       gower_1.0.2          bayestestR_0.16.1   
[109] listenv_0.9.1        viridisLite_0.4.2    mvtnorm_1.3-3       
[112] ipred_0.9-15         scales_1.4.0         prodlim_2025.04.28  
[115] e1071_1.7-16         insight_1.3.1        rlang_1.1.6         
[118] multcomp_1.4-28      mnormt_2.1.1