R
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Data Structures
-
Section 4: Data Manipulation
-
Section 5: Visualization and Statistics
-
Section 6: Advanced R
-
Section 7: Practical Projects
-
Section 8: Interview Practice
-
Section 9: More Practice Exercises
-
Section 10: Shiny Apps in Depth
-
Section 11: More Data Wrangling
-
Section 12: Tidyverse Deep Dive
-
Section 13: Statistical Modeling Deep Dive
-
Section 14: Machine Learning in R
-
Section 15: R Visualization Deep Dive
-
Section 16: R Package Development Deep Dive
-
Section 17: R for Reproducible Research
-
Section 18: R and Databases
-
Section 19: R Performance Optimization
-
Section 20: Bioinformatics and Specialized R
-
Section 21: More Shiny Practice
-
Section 22: More Practice Exercises
-
Section 23: R Data Structures Deep Dive
-
Section 24: More Interview and Review
-
Section 25: R for Business Analytics
-
Section 26: R Text Mining and NLP
-
Section 27: R Spatial Data Analysis
-
Section 28: R Deep Learning
-
Section 29: Advanced Statistical Techniques
-
Section 30: R Object Systems Deep Dive
-
Section 31: R Environments and Metaprogramming
-
Section 32: R for Finance
-
Section 33: R for Clinical and Health Data
-
Section 34: More Shiny Advanced Practice
-
Section 35: R Data Cleaning Deep Dive
-
Section 36: R Reporting Automation
-
Section 37: More Practical Projects Round 2
-
Section 38: R Ecosystem and Career
-
Section 39: More Visualization Practice
-
Section 40: R for Experimentation
-
Section 41: R for Genomics and Bioinformatics Deep Dive
-
Section 42: R for Marketing Analytics
-
Section 43: R Data Import/Export Deep Dive
-
Section 44: R String Processing Deep Dive
-
Section 45: R for Actuarial and Insurance Analytics
-
Section 46: R Testing and Quality Assurance Deep Dive
-
Section 47: R Version Control and Collaboration
-
Section 48: R Advanced Functional Programming
-
Section 49: R for Supply Chain and Operations
-
Section 50: More Practice Exercises Round 3
-
Section 51: R Dashboards and BI Integration
-
Section 52: R Data Governance and Ethics
-
Section 53: More Modeling Practice
-
Section 54: R Final Capstone Projects
-
Section 55: R for Sports Analytics
-
Section 56: More Interview Practice Round 2
-
Section 57: R Networking and APIs
-
Section 58: R for Environmental Science
-
Section 59: R Notebook and Documentation Practices
-
Section 60: More Data Wrangling Mastery
-
Section 61: R for A/B Testing at Scale
-
Section 62: R Package Ecosystem Deep Dive
222: Fantasy Sports Projection Models
When people talk about "projection models" in fantasy sports, they often make it sound like they're running a hedge fund's quantitative desk. In reality, most successful models are just a series of informed guesses backed by weighted averages and regression to the mean. We aren't trying to predict the future with 100% accuracy—that's impossible—we're just trying to be slightly less wrong than our league mates.
For this lesson, we're going to build a simple projection tool for NFL Wide Receivers. Our goal is to estimate "Expected Points" based on two key metrics: Targets per Game and Points per Target. I prefer this over just looking at total points because targets are a much more stable predictor of future success than actual scoring.
Setting up the player slate
I'll start by creating a small data frame. In a real scenario, you'd be pulling this from an API or a CSV, but let's keep our focus on the logic. I'm including a mix of high-volume stars and "boom-or-bust" players to see how the model handles them.
# Our sample player data
players <- data.frame(
name = c("Justin Jefferson", "Garrett Wilson", "Rashid Shaheed", "Zay Flowers"),
targets_per_game = c( 10.2, 9.1, 4.5, 6.2),
pts_per_target = c( 1.4, 1.1, 1.8, 1.2),
games_played = c(12, 11, 10, 13)
)
The first pass at the projection
The basic logic is simple: Expected Points = Targets per Game * Points per Target. I'll use a vectorized operation here because it's what R is built for. No need for loops when we can just multiply the columns.
# Simple projection
players$proj_points <- players$targets_per_game * players$pts_per_target
print(players)
At first glance, this looks fine. But if you look at Rashid Shaheed in our list, he has a very high points-per-target (1.8). If we trust this number blindly, we're assuming he'll keep scoring an unsustainable amount of points every time he's targeted. This is where I usually mess up my first draft of a model: I trust the "efficiency" metric too much without considering the sample size.
Correcting for the "Efficiency Trap"
Here's where I made my mistake: I ignored the fact that high efficiency on low volume is usually a fluke (or "luck"). To fix this, I'm going to implement a basic "regress to the mean" adjustment. I'll define a league-average points-per-target (let's say 1.1) and blend the player's actual efficiency with that average based on how many games they've played.
I'll use a weight of 0.7 for the player's own data and 0.3 for the league average. This "shrinks" the outliers back toward the middle, making the projection more conservative and realistic.
league_avg_ppt <- 1.1
# Correcting the projection using a weighted average
players$adjusted_ppt <- (players$pts_per_target * 0.7) + (league_avg_ppt * 0.3)
players$final_proj <- players$targets_per_game * players$adjusted_ppt
# Compare the raw projection vs the adjusted one
players[, c("name", "proj_points", "final_proj")]
See the difference? The "boom" players get pulled down slightly, and the under-performers get a small bump. It's a simple tweak, but it prevents you from overvaluing a player who had two lucky long touchdowns in a three-game span.
Turning the logic into a projection engine
Since we'll likely want to run this for multiple positions or different weightings, I'll wrap this into a function. This makes the code dry and allows us to experiment with the "shrinkage" factor without rewriting the math every time.
project_fantasy_points <- function(targets, efficiency, avg_eff = 1.1, weight = 0.7) {
adj_eff <- (efficiency * weight) + (avg_eff * (1 - weight))
return(targets * adj_eff)
}
# Now I can apply it cleanly across the data frame
players$final_proj <- project_fantasy_points(
players$targets_per_game,
players$pts_per_target
)
Now we have a repeatable process. If you find that the league average changes as the season progresses, you just update the avg_eff argument rather than hunting through your code for every instance of "1.1".
📋 Practical Task
Build a Dual-Threat QB Projection Model
Using the logic from the lesson, create a projection model for Quarterbacks. QBs are trickier because they score points from both passing and rushing.
Your task:
- Create a data frame of 3-5 QBs with the following columns:
name,pass_atts_per_game,pts_per_pass_att,rush_atts_per_game, andpts_per_rush_att. - Write a function called
project_qb_pointsthat takes these four metrics as arguments. - Inside the function, apply a "regress to the mean" adjustment to the
pts_per_pass_attusing a league average of 0.12 and a weight of 0.8. - Return the sum of the adjusted passing projection and the raw rushing projection (assume rushing efficiency is stable for this exercise).
- Apply this function to your data frame to generate a
final_projectioncolumn.
There are no comments for now.