Code Examples

Copy-paste ready R and Python code for NFL analytics. From data loading to machine learning models.

122 Examples
R & Python Support: All examples include both R and Python versions. Click the tabs to switch between languages. Use the copy button to copy code to clipboard.

Fantasy Football

Fantasy football analysis, projections, and trade evaluations

Fantasy Points Calculation
Calculate fantasy points for all skill position players.
Intermediate
library(nflfastR)
library(tidyverse)

pbp <- load_pbp(2023)

# PPR Fantasy Points calculation
# Passing: 0.04 per yard, 4 per TD, -2 per INT
# Rushing: 0.1 per yard, 6 per TD
# Receiving: 0.1 per yard, 6 per TD, 1 per reception

# QB Fantasy Points
qb_fantasy <- pbp %>%
  filter(!is.na(passer_player_id)) %>%
  group_by(passer_player_id, passer_player_name) %>%
  summarize(
    pass_yards = sum(passing_yards, na.rm = TRUE),
    pass_tds = sum(pass_touchdown, na.rm = TRUE),
    ints = sum(interception),
    fantasy_pts = pass_yards * 0.04 + pass_tds * 4 - ints * 2,
    .groups = "drop"
  ) %>%
  arrange(desc(fantasy_pts))

# RB/WR/TE Fantasy Points (PPR)
skill_fantasy <- pbp %>%
  filter(!is.na(rusher_player_id) | !is.na(receiver_player_id)) %>%
  mutate(
    player_id = coalesce(rusher_player_id, receiver_player_id),
    player_name = coalesce(rusher_player_name, receiver_player_name)
  ) %>%
  group_by(player_id, player_name) %>%
  summarize(
    rush_yards = sum(rushing_yards, na.rm = TRUE),
    rush_tds = sum(rush_touchdown, na.rm = TRUE),
    receptions = sum(complete_pass, na.rm = TRUE),
    rec_yards = sum(receiving_yards, na.rm = TRUE),
    rec_tds = sum(pass_touchdown[!is.na(receiver_player_id)], na.rm = TRUE),
    fantasy_pts = rush_yards * 0.1 + rush_tds * 6 +
                  receptions * 1 + rec_yards * 0.1 + rec_tds * 6,
    .groups = "drop"
  ) %>%
  filter(fantasy_pts > 50) %>%
  arrange(desc(fantasy_pts))

print(skill_fantasy %>% head(30))
import nfl_data_py as nfl
import pandas as pd

pbp = nfl.import_pbp_data([2023])

# QB Fantasy Points
qb_plays = pbp[pbp["passer_player_id"].notna()]
qb_fantasy = (qb_plays.groupby(["passer_player_id", "passer_player_name"])
    .agg(
        pass_yards=("passing_yards", "sum"),
        pass_tds=("pass_touchdown", "sum"),
        ints=("interception", "sum")
    )
    .reset_index())
qb_fantasy["fantasy_pts"] = (qb_fantasy["pass_yards"] * 0.04 +
                              qb_fantasy["pass_tds"] * 4 -
                              qb_fantasy["ints"] * 2)
qb_fantasy = qb_fantasy.sort_values("fantasy_pts", ascending=False)

# Skill position fantasy points (simplified)
rush_plays = pbp[pbp["rusher_player_id"].notna()]
rush_pts = (rush_plays.groupby(["rusher_player_id", "rusher_player_name"])
    .agg(
        rush_yards=("rushing_yards", "sum"),
        rush_tds=("rush_touchdown", "sum")
    )
    .reset_index())
rush_pts.columns = ["player_id", "player_name", "rush_yards", "rush_tds"]

rec_plays = pbp[pbp["receiver_player_id"].notna()]
rec_pts = (rec_plays.groupby(["receiver_player_id", "receiver_player_name"])
    .agg(
        receptions=("complete_pass", "sum"),
        rec_yards=("receiving_yards", "sum"),
        rec_tds=("pass_touchdown", "sum")
    )
    .reset_index())
rec_pts.columns = ["player_id", "player_name", "receptions", "rec_yards", "rec_tds"]

# Combine
skill_fantasy = rush_pts.merge(rec_pts, on=["player_id", "player_name"], how="outer").fillna(0)
skill_fantasy["fantasy_pts"] = (skill_fantasy["rush_yards"] * 0.1 +
                                 skill_fantasy["rush_tds"] * 6 +
                                 skill_fantasy["receptions"] * 1 +
                                 skill_fantasy["rec_yards"] * 0.1 +
                                 skill_fantasy["rec_tds"] * 6)
skill_fantasy = skill_fantasy[skill_fantasy["fantasy_pts"] > 50].sort_values("fantasy_pts", ascending=False)

print(skill_fantasy.head(30))
Packages: nflfastR tidyverse nfl_data_py pandas
Target Share Analysis
Analyze target distribution within offenses for fantasy value.
Intermediate
library(nflfastR)
library(tidyverse)

pbp <- load_pbp(2023)

# Calculate target share by team
target_share <- pbp %>%
  filter(play_type == "pass", !is.na(receiver_player_id)) %>%
  group_by(posteam) %>%
  mutate(team_targets = n()) %>%
  group_by(posteam, receiver_player_id, receiver_player_name, team_targets) %>%
  summarize(
    targets = n(),
    air_yards = sum(air_yards, na.rm = TRUE),
    receptions = sum(complete_pass),
    yards = sum(yards_gained, na.rm = TRUE),
    .groups = "drop"
  ) %>%
  mutate(
    target_share = targets / team_targets,
    air_yard_share = air_yards / sum(air_yards),
    wopr = target_share * 1.5 + air_yard_share * 0.7  # Weighted Opportunity Rating
  ) %>%
  arrange(desc(wopr))

# Top target share players
print(target_share %>%
        select(receiver_player_name, posteam, target_share,
               air_yard_share, wopr) %>%
        head(25))
import nfl_data_py as nfl
import pandas as pd

pbp = nfl.import_pbp_data([2023])

# Filter to pass plays with receiver
pass_plays = pbp[(pbp["play_type"] == "pass") &
                 (pbp["receiver_player_id"].notna())]

# Team totals
team_targets = pass_plays.groupby("posteam").agg(
    team_targets=("play_id", "count"),
    team_air_yards=("air_yards", "sum")
).reset_index()

# Player stats
player_stats = (pass_plays.groupby(
    ["posteam", "receiver_player_id", "receiver_player_name"])
    .agg(
        targets=("play_id", "count"),
        air_yards=("air_yards", "sum"),
        receptions=("complete_pass", "sum"),
        yards=("yards_gained", "sum")
    )
    .reset_index())

# Calculate shares
target_share = player_stats.merge(team_targets, on="posteam")
target_share["target_share"] = target_share["targets"] / target_share["team_targets"]
target_share["air_yard_share"] = target_share["air_yards"] / target_share["team_air_yards"]
target_share["wopr"] = target_share["target_share"] * 1.5 + target_share["air_yard_share"] * 0.7

result = target_share.nlargest(25, "wopr")[
    ["receiver_player_name", "posteam", "target_share", "air_yard_share", "wopr"]
]
print(result)
Packages: nflfastR tidyverse nfl_data_py pandas
Snap Count Analysis
Analyze snap counts and playing time share for fantasy value.
Intermediate
library(nflfastR)
library(tidyverse)

# Load participation data
participation <- load_participation(2023)
rosters <- fast_scraper_roster(2023)

# Calculate snap counts
snap_counts <- participation %>%
  separate_rows(offense_players, sep = ";") %>%
  filter(offense_players != "") %>%
  group_by(nflverse_game_id, offense_players) %>%
  summarize(snaps = n(), .groups = "drop") %>%
  rename(gsis_id = offense_players)

# Join with roster for names
snap_counts <- snap_counts %>%
  left_join(rosters %>% select(gsis_id, full_name, position, team),
            by = "gsis_id")

# Calculate season totals
season_snaps <- snap_counts %>%
  group_by(gsis_id, full_name, position, team) %>%
  summarize(
    games = n(),
    total_snaps = sum(snaps),
    avg_snaps = mean(snaps),
    .groups = "drop"
  ) %>%
  filter(position %in% c("RB", "WR", "TE")) %>%
  arrange(desc(total_snaps))

print(season_snaps %>% head(30))
import nfl_data_py as nfl
import pandas as pd

# Load participation data
participation = nfl.import_snap_counts([2023])
rosters = nfl.import_rosters([2023])

# Aggregate by player
season_snaps = (participation.groupby("pfr_player_id")
    .agg(
        games=("week", "nunique"),
        total_off_snaps=("offense_snaps", "sum"),
        avg_off_snaps=("offense_snaps", "mean"),
        total_def_snaps=("defense_snaps", "sum")
    )
    .reset_index())

# Join with roster data
roster_subset = rosters[["pfr_id", "player_name", "position", "team"]].drop_duplicates()
season_snaps = season_snaps.merge(roster_subset, left_on="pfr_player_id",
                                   right_on="pfr_id", how="left")

# Filter skill positions
skill_snaps = season_snaps[season_snaps["position"].isin(["RB", "WR", "TE"])]
skill_snaps = skill_snaps.sort_values("total_off_snaps", ascending=False)

print("Top Snap Count Leaders (Skill Positions):")
print(skill_snaps.head(30))
Packages: nflfastR tidyverse nfl_data_py pandas
Touchdown Regression Candidates
Identify players due for positive or negative TD regression.
Intermediate
library(nflfastR)
library(tidyverse)

pbp <- load_pbp(2023)

# Calculate TD rate vs expected
td_analysis <- pbp %>%
  filter(play_type == "pass", !is.na(receiver_player_id)) %>%
  group_by(receiver_player_id, receiver_player_name) %>%
  summarize(
    targets = n(),
    rz_targets = sum(yardline_100 <= 20, na.rm = TRUE),
    tds = sum(pass_touchdown),
    expected_td_rate = 0.10 * (rz_targets / targets),  # Simplified expected
    actual_td_rate = tds / targets,
    .groups = "drop"
  ) %>%
  filter(targets >= 50) %>%
  mutate(
    td_diff = actual_td_rate - expected_td_rate,
    regression_direction = case_when(
      td_diff > 0.03 ~ "Negative",
      td_diff < -0.03 ~ "Positive",
      TRUE ~ "Stable"
    )
  )

# Candidates for positive regression (underperformed)
positive_regression <- td_analysis %>%
  filter(regression_direction == "Positive") %>%
  arrange(td_diff)

print("Positive Regression Candidates (Due for more TDs):")
print(positive_regression %>% select(receiver_player_name, targets, tds, td_diff))

# Candidates for negative regression (overperformed)
negative_regression <- td_analysis %>%
  filter(regression_direction == "Negative") %>%
  arrange(desc(td_diff))

print("\nNegative Regression Candidates (TD rate unsustainable):")
print(negative_regression %>% select(receiver_player_name, targets, tds, td_diff))
import nfl_data_py as nfl
import pandas as pd

pbp = nfl.import_pbp_data([2023])

# Calculate TD rate analysis
pass_plays = pbp[(pbp["play_type"] == "pass") & (pbp["receiver_player_id"].notna())]

td_analysis = (pass_plays.groupby(["receiver_player_id", "receiver_player_name"])
    .agg(
        targets=("play_id", "count"),
        rz_targets=("yardline_100", lambda x: (x <= 20).sum()),
        tds=("pass_touchdown", "sum")
    )
    .reset_index())

td_analysis = td_analysis[td_analysis["targets"] >= 50]
td_analysis["rz_rate"] = td_analysis["rz_targets"] / td_analysis["targets"]
td_analysis["expected_td_rate"] = 0.10 * td_analysis["rz_rate"]
td_analysis["actual_td_rate"] = td_analysis["tds"] / td_analysis["targets"]
td_analysis["td_diff"] = td_analysis["actual_td_rate"] - td_analysis["expected_td_rate"]

# Positive regression candidates
positive = td_analysis[td_analysis["td_diff"] < -0.03].sort_values("td_diff")
print("Positive Regression Candidates (Due for more TDs):")
print(positive[["receiver_player_name", "targets", "tds", "td_diff"]].head(10))

# Negative regression candidates
negative = td_analysis[td_analysis["td_diff"] > 0.03].sort_values("td_diff", ascending=False)
print("\nNegative Regression Candidates (TD rate unsustainable):")
print(negative[["receiver_player_name", "targets", "tds", "td_diff"]].head(10))
Packages: nflfastR tidyverse nfl_data_py pandas
Red Zone Target Leaders
Identify top red zone target getters for TD upside.
Beginner
library(nflfastR)
library(tidyverse)

pbp <- load_pbp(2023)

# Red zone targets analysis
rz_targets <- pbp %>%
  filter(play_type == "pass", yardline_100 <= 20, !is.na(receiver_player_id)) %>%
  group_by(receiver_player_id, receiver_player_name, posteam) %>%
  summarize(
    rz_targets = n(),
    rz_receptions = sum(complete_pass),
    rz_tds = sum(pass_touchdown),
    avg_depth = mean(air_yards, na.rm = TRUE),
    .groups = "drop"
  ) %>%
  mutate(
    rz_catch_rate = rz_receptions / rz_targets * 100,
    rz_td_rate = rz_tds / rz_targets * 100
  ) %>%
  arrange(desc(rz_targets))

print("Top Red Zone Target Leaders:")
print(rz_targets %>% head(25))

# Inside 10 yard line
goalline <- pbp %>%
  filter(play_type == "pass", yardline_100 <= 10, !is.na(receiver_player_id)) %>%
  group_by(receiver_player_name) %>%
  summarize(
    gl_targets = n(),
    gl_tds = sum(pass_touchdown),
    .groups = "drop"
  ) %>%
  arrange(desc(gl_targets))

print("\nGoal Line Target Leaders (Inside 10):")
print(goalline %>% head(15))
import nfl_data_py as nfl
import pandas as pd

pbp = nfl.import_pbp_data([2023])

# Red zone targets
rz_plays = pbp[(pbp["play_type"] == "pass") &
               (pbp["yardline_100"] <= 20) &
               (pbp["receiver_player_id"].notna())]

rz_targets = (rz_plays.groupby(["receiver_player_id", "receiver_player_name", "posteam"])
    .agg(
        rz_targets=("play_id", "count"),
        rz_receptions=("complete_pass", "sum"),
        rz_tds=("pass_touchdown", "sum"),
        avg_depth=("air_yards", "mean")
    )
    .reset_index())

rz_targets["rz_catch_rate"] = rz_targets["rz_receptions"] / rz_targets["rz_targets"] * 100
rz_targets["rz_td_rate"] = rz_targets["rz_tds"] / rz_targets["rz_targets"] * 100

rz_targets = rz_targets.sort_values("rz_targets", ascending=False)

print("Top Red Zone Target Leaders:")
print(rz_targets.head(25))

# Goal line targets
gl_plays = pbp[(pbp["play_type"] == "pass") &
               (pbp["yardline_100"] <= 10) &
               (pbp["receiver_player_id"].notna())]

goalline = (gl_plays.groupby("receiver_player_name")
    .agg(gl_targets=("play_id", "count"), gl_tds=("pass_touchdown", "sum"))
    .reset_index()
    .sort_values("gl_targets", ascending=False))

print("\nGoal Line Target Leaders:")
print(goalline.head(15))
Packages: nflfastR tidyverse nfl_data_py pandas
DFS Ownership vs Value Analysis
Find undervalued players in daily fantasy lineups.
Advanced
library(nflfastR)
library(tidyverse)

pbp <- load_pbp(2023)

# Calculate fantasy points per game
fantasy_ppg <- pbp %>%
  filter(!is.na(receiver_player_id) | !is.na(rusher_player_id)) %>%
  mutate(
    player_id = coalesce(receiver_player_id, rusher_player_id),
    player_name = coalesce(receiver_player_name, rusher_player_name)
  ) %>%
  group_by(player_id, player_name, week) %>%
  summarize(
    rush_yards = sum(rushing_yards, na.rm = TRUE),
    rush_tds = sum(rush_touchdown, na.rm = TRUE),
    receptions = sum(complete_pass, na.rm = TRUE),
    rec_yards = sum(receiving_yards, na.rm = TRUE),
    rec_tds = sum(touchdown[!is.na(receiver_player_id)], na.rm = TRUE),
    fantasy_pts = rush_yards * 0.1 + rush_tds * 6 + receptions * 1 +
                  rec_yards * 0.1 + rec_tds * 6,
    .groups = "drop"
  )

# Calculate consistency metrics
consistency <- fantasy_ppg %>%
  group_by(player_id, player_name) %>%
  summarize(
    games = n(),
    avg_pts = mean(fantasy_pts),
    std_pts = sd(fantasy_pts),
    floor = quantile(fantasy_pts, 0.1),
    ceiling = quantile(fantasy_pts, 0.9),
    boom_rate = mean(fantasy_pts > 15) * 100,  # "Boom" games
    bust_rate = mean(fantasy_pts < 5) * 100,   # "Bust" games
    .groups = "drop"
  ) %>%
  filter(games >= 8) %>%
  mutate(
    consistency_score = avg_pts / (std_pts + 1),  # Higher is better
    upside_score = ceiling / avg_pts  # Higher ceiling relative to average
  ) %>%
  arrange(desc(consistency_score))

print("Most Consistent Fantasy Performers:")
print(consistency %>% select(player_name, avg_pts, boom_rate, bust_rate, consistency_score) %>% head(20))
import nfl_data_py as nfl
import pandas as pd
import numpy as np

pbp = nfl.import_pbp_data([2023])

# Calculate weekly fantasy points
skill_plays = pbp[(pbp["receiver_player_id"].notna()) | (pbp["rusher_player_id"].notna())].copy()
skill_plays["player_id"] = skill_plays["receiver_player_id"].fillna(skill_plays["rusher_player_id"])
skill_plays["player_name"] = skill_plays["receiver_player_name"].fillna(skill_plays["rusher_player_name"])

weekly = (skill_plays.groupby(["player_id", "player_name", "week"])
    .agg(
        rush_yards=("rushing_yards", "sum"),
        rush_tds=("rush_touchdown", "sum"),
        receptions=("complete_pass", "sum"),
        rec_yards=("receiving_yards", "sum"),
        rec_tds=("pass_touchdown", "sum")
    )
    .reset_index())

weekly["fantasy_pts"] = (weekly["rush_yards"] * 0.1 + weekly["rush_tds"] * 6 +
                          weekly["receptions"] * 1 + weekly["rec_yards"] * 0.1 +
                          weekly["rec_tds"] * 6)

# Consistency metrics
consistency = (weekly.groupby(["player_id", "player_name"])
    .agg(
        games=("fantasy_pts", "count"),
        avg_pts=("fantasy_pts", "mean"),
        std_pts=("fantasy_pts", "std"),
        floor=("fantasy_pts", lambda x: x.quantile(0.1)),
        ceiling=("fantasy_pts", lambda x: x.quantile(0.9)),
        boom_rate=("fantasy_pts", lambda x: (x > 15).mean() * 100),
        bust_rate=("fantasy_pts", lambda x: (x < 5).mean() * 100)
    )
    .reset_index())

consistency = consistency[consistency["games"] >= 8]
consistency["consistency_score"] = consistency["avg_pts"] / (consistency["std_pts"] + 1)

consistency = consistency.sort_values("consistency_score", ascending=False)
print("Most Consistent Fantasy Performers:")
print(consistency[["player_name", "avg_pts", "boom_rate", "bust_rate", "consistency_score"]].head(20))
Packages: nflfastR tidyverse nfl_data_py pandas numpy
Rest of Season Rankings
Project remaining season value based on opportunities.
Advanced
library(nflfastR)
library(tidyverse)

pbp <- load_pbp(2023)
rosters <- fast_scraper_roster(2023)

# Get opportunity metrics
opportunities <- pbp %>%
  filter(!is.na(receiver_player_id) | !is.na(rusher_player_id)) %>%
  mutate(
    player_id = coalesce(receiver_player_id, rusher_player_id),
    player_name = coalesce(receiver_player_name, rusher_player_name),
    is_target = !is.na(receiver_player_id),
    is_carry = !is.na(rusher_player_id) & play_type == "run"
  ) %>%
  group_by(player_id, player_name, posteam) %>%
  summarize(
    games = n_distinct(game_id),
    targets = sum(is_target),
    carries = sum(is_carry),
    touches = targets + carries,
    fantasy_pts = sum(rushing_yards, na.rm = TRUE) * 0.1 +
                  sum(rush_touchdown, na.rm = TRUE) * 6 +
                  sum(complete_pass, na.rm = TRUE) * 1 +
                  sum(receiving_yards, na.rm = TRUE) * 0.1 +
                  sum(pass_touchdown[is_target], na.rm = TRUE) * 6,
    .groups = "drop"
  ) %>%
  mutate(
    touches_per_game = touches / games,
    pts_per_touch = fantasy_pts / touches,
    pts_per_game = fantasy_pts / games,
    # Project remaining games (assuming 17-game season)
    games_remaining = 17 - games,
    projected_ros_pts = pts_per_game * games_remaining
  ) %>%
  filter(games >= 5, touches >= 30) %>%
  arrange(desc(projected_ros_pts))

print("Rest of Season Fantasy Rankings:")
print(opportunities %>%
        select(player_name, games, pts_per_game, games_remaining, projected_ros_pts) %>%
        head(30))
import nfl_data_py as nfl
import pandas as pd

pbp = nfl.import_pbp_data([2023])

# Get opportunity metrics
skill_plays = pbp[(pbp["receiver_player_id"].notna()) | (pbp["rusher_player_id"].notna())].copy()
skill_plays["player_id"] = skill_plays["receiver_player_id"].fillna(skill_plays["rusher_player_id"])
skill_plays["player_name"] = skill_plays["receiver_player_name"].fillna(skill_plays["rusher_player_name"])
skill_plays["is_target"] = skill_plays["receiver_player_id"].notna()
skill_plays["is_carry"] = (skill_plays["rusher_player_id"].notna()) & (skill_plays["play_type"] == "run")

opportunities = (skill_plays.groupby(["player_id", "player_name", "posteam"])
    .agg(
        games=("game_id", "nunique"),
        targets=("is_target", "sum"),
        carries=("is_carry", "sum"),
        rush_yards=("rushing_yards", "sum"),
        rush_tds=("rush_touchdown", "sum"),
        receptions=("complete_pass", "sum"),
        rec_yards=("receiving_yards", "sum"),
        rec_tds=("pass_touchdown", "sum")
    )
    .reset_index())

opportunities["touches"] = opportunities["targets"] + opportunities["carries"]
opportunities["fantasy_pts"] = (opportunities["rush_yards"] * 0.1 +
                                 opportunities["rush_tds"] * 6 +
                                 opportunities["receptions"] * 1 +
                                 opportunities["rec_yards"] * 0.1 +
                                 opportunities["rec_tds"] * 6)
opportunities["pts_per_game"] = opportunities["fantasy_pts"] / opportunities["games"]
opportunities["games_remaining"] = 17 - opportunities["games"]
opportunities["projected_ros_pts"] = opportunities["pts_per_game"] * opportunities["games_remaining"]

opportunities = opportunities[(opportunities["games"] >= 5) & (opportunities["touches"] >= 30)]
opportunities = opportunities.sort_values("projected_ros_pts", ascending=False)

print("Rest of Season Fantasy Rankings:")
print(opportunities[["player_name", "games", "pts_per_game", "games_remaining", "projected_ros_pts"]].head(30))
Packages: nflfastR tidyverse nfl_data_py pandas
Trade Value Calculator
Calculate relative trade value for fantasy players.
Intermediate
library(nflfastR)
library(tidyverse)

pbp <- load_pbp(2023)

# Calculate player values
player_values <- pbp %>%
  filter(!is.na(receiver_player_id) | !is.na(rusher_player_id)) %>%
  mutate(
    player_id = coalesce(receiver_player_id, rusher_player_id),
    player_name = coalesce(receiver_player_name, rusher_player_name)
  ) %>%
  group_by(player_id, player_name, posteam) %>%
  summarize(
    games = n_distinct(game_id),
    fantasy_pts = sum(rushing_yards, na.rm = TRUE) * 0.1 +
                  sum(rush_touchdown, na.rm = TRUE) * 6 +
                  sum(complete_pass, na.rm = TRUE) * 1 +
                  sum(receiving_yards, na.rm = TRUE) * 0.1 +
                  sum(pass_touchdown[!is.na(receiver_player_id)], na.rm = TRUE) * 6,
    .groups = "drop"
  ) %>%
  mutate(ppg = fantasy_pts / games) %>%
  filter(games >= 5)

# Create trade value scale (1-100)
player_values <- player_values %>%
  mutate(
    # Base value on PPG percentile
    value_percentile = percent_rank(ppg),
    # Scale to 1-100
    trade_value = round(value_percentile * 100),
    # Tier assignment
    tier = case_when(
      trade_value >= 90 ~ "Elite (Tier 1)",
      trade_value >= 75 ~ "Star (Tier 2)",
      trade_value >= 60 ~ "Starter (Tier 3)",
      trade_value >= 40 ~ "Flex (Tier 4)",
      TRUE ~ "Bench (Tier 5)"
    )
  ) %>%
  arrange(desc(trade_value))

print("Fantasy Trade Values:")
print(player_values %>%
        select(player_name, ppg, trade_value, tier) %>%
        head(40))
import nfl_data_py as nfl
import pandas as pd
import numpy as np

pbp = nfl.import_pbp_data([2023])

# Calculate player values
skill_plays = pbp[(pbp["receiver_player_id"].notna()) | (pbp["rusher_player_id"].notna())].copy()
skill_plays["player_id"] = skill_plays["receiver_player_id"].fillna(skill_plays["rusher_player_id"])
skill_plays["player_name"] = skill_plays["receiver_player_name"].fillna(skill_plays["rusher_player_name"])

player_values = (skill_plays.groupby(["player_id", "player_name", "posteam"])
    .agg(
        games=("game_id", "nunique"),
        rush_yards=("rushing_yards", "sum"),
        rush_tds=("rush_touchdown", "sum"),
        receptions=("complete_pass", "sum"),
        rec_yards=("receiving_yards", "sum"),
        rec_tds=("pass_touchdown", "sum")
    )
    .reset_index())

player_values["fantasy_pts"] = (player_values["rush_yards"] * 0.1 +
                                 player_values["rush_tds"] * 6 +
                                 player_values["receptions"] * 1 +
                                 player_values["rec_yards"] * 0.1 +
                                 player_values["rec_tds"] * 6)
player_values["ppg"] = player_values["fantasy_pts"] / player_values["games"]

player_values = player_values[player_values["games"] >= 5]

# Create trade value scale
player_values["value_percentile"] = player_values["ppg"].rank(pct=True)
player_values["trade_value"] = (player_values["value_percentile"] * 100).round()

def get_tier(val):
    if val >= 90: return "Elite (Tier 1)"
    elif val >= 75: return "Star (Tier 2)"
    elif val >= 60: return "Starter (Tier 3)"
    elif val >= 40: return "Flex (Tier 4)"
    else: return "Bench (Tier 5)"

player_values["tier"] = player_values["trade_value"].apply(get_tier)
player_values = player_values.sort_values("trade_value", ascending=False)

print("Fantasy Trade Values:")
print(player_values[["player_name", "ppg", "trade_value", "tier"]].head(40))
Packages: nflfastR tidyverse nfl_data_py pandas numpy
Matchup Analysis
Evaluate player value based on defensive matchups.
Intermediate
library(nflfastR)
library(tidyverse)

pbp <- load_pbp(2023)
schedules <- load_schedules(2023)

# Calculate defense vs position stats
def_vs_rb <- pbp %>%
  filter(play_type == "run", !is.na(epa)) %>%
  group_by(defteam) %>%
  summarize(
    plays = n(),
    rush_epa_allowed = mean(epa),
    rush_ypc_allowed = mean(yards_gained),
    rush_td_allowed = sum(rush_touchdown),
    .groups = "drop"
  ) %>%
  mutate(
    rb_rank = rank(rush_epa_allowed)  # Lower EPA = better defense
  )

def_vs_wr <- pbp %>%
  filter(play_type == "pass", !is.na(epa)) %>%
  group_by(defteam) %>%
  summarize(
    plays = n(),
    pass_epa_allowed = mean(epa),
    ypa_allowed = mean(yards_gained),
    pass_td_allowed = sum(pass_touchdown),
    .groups = "drop"
  ) %>%
  mutate(
    wr_rank = rank(pass_epa_allowed)  # Lower EPA = better defense
  )

# Combine defensive rankings
def_rankings <- def_vs_rb %>%
  select(defteam, rush_epa_allowed, rb_rank) %>%
  left_join(def_vs_wr %>% select(defteam, pass_epa_allowed, wr_rank), by = "defteam") %>%
  mutate(
    rb_matchup = case_when(
      rb_rank <= 8 ~ "Tough",
      rb_rank <= 24 ~ "Average",
      TRUE ~ "Favorable"
    ),
    wr_matchup = case_when(
      wr_rank <= 8 ~ "Tough",
      wr_rank <= 24 ~ "Average",
      TRUE ~ "Favorable"
    )
  )

print("Defensive Matchup Rankings:")
print(def_rankings %>% arrange(rb_rank))
import nfl_data_py as nfl
import pandas as pd

pbp = nfl.import_pbp_data([2023])
schedules = nfl.import_schedules([2023])

# Defense vs RB
rushes = pbp[(pbp["play_type"] == "run") & (pbp["epa"].notna())]
def_vs_rb = (rushes.groupby("defteam")
    .agg(
        plays=("epa", "count"),
        rush_epa_allowed=("epa", "mean"),
        rush_ypc=("yards_gained", "mean"),
        rush_td=("rush_touchdown", "sum")
    )
    .reset_index())
def_vs_rb["rb_rank"] = def_vs_rb["rush_epa_allowed"].rank()

# Defense vs WR
passes = pbp[(pbp["play_type"] == "pass") & (pbp["epa"].notna())]
def_vs_wr = (passes.groupby("defteam")
    .agg(
        plays=("epa", "count"),
        pass_epa_allowed=("epa", "mean"),
        ypa=("yards_gained", "mean"),
        pass_td=("pass_touchdown", "sum")
    )
    .reset_index())
def_vs_wr["wr_rank"] = def_vs_wr["pass_epa_allowed"].rank()

# Combine
def_rankings = def_vs_rb[["defteam", "rush_epa_allowed", "rb_rank"]].merge(
    def_vs_wr[["defteam", "pass_epa_allowed", "wr_rank"]], on="defteam")

def rb_matchup(rank):
    if rank <= 8: return "Tough"
    elif rank <= 24: return "Average"
    else: return "Favorable"

def_rankings["rb_matchup"] = def_rankings["rb_rank"].apply(rb_matchup)
def_rankings["wr_matchup"] = def_rankings["wr_rank"].apply(rb_matchup)

print("Defensive Matchup Rankings:")
print(def_rankings.sort_values("rb_rank"))
Packages: nflfastR tidyverse nfl_data_py pandas
Waiver Wire Gems
Find under-the-radar players with emerging opportunity.
Intermediate
library(nflfastR)
library(tidyverse)

pbp <- load_pbp(2023)

# Find players with increasing opportunity
weekly_touches <- pbp %>%
  filter(!is.na(receiver_player_id) | !is.na(rusher_player_id)) %>%
  mutate(
    player_id = coalesce(receiver_player_id, rusher_player_id),
    player_name = coalesce(receiver_player_name, rusher_player_name),
    is_touch = TRUE
  ) %>%
  group_by(player_id, player_name, week, posteam) %>%
  summarize(
    touches = n(),
    fantasy_pts = sum(rushing_yards, na.rm = TRUE) * 0.1 +
                  sum(rush_touchdown, na.rm = TRUE) * 6 +
                  sum(complete_pass, na.rm = TRUE) * 1 +
                  sum(receiving_yards, na.rm = TRUE) * 0.1 +
                  sum(pass_touchdown[!is.na(receiver_player_id)], na.rm = TRUE) * 6,
    .groups = "drop"
  )

# Calculate trend (last 3 weeks vs first 3 weeks)
max_week <- max(weekly_touches$week)
trending <- weekly_touches %>%
  mutate(
    period = case_when(
      week >= max_week - 2 ~ "Recent",
      week <= 3 ~ "Early",
      TRUE ~ "Middle"
    )
  ) %>%
  filter(period %in% c("Recent", "Early")) %>%
  group_by(player_id, player_name, posteam, period) %>%
  summarize(
    avg_touches = mean(touches),
    avg_pts = mean(fantasy_pts),
    .groups = "drop"
  ) %>%
  pivot_wider(names_from = period, values_from = c(avg_touches, avg_pts)) %>%
  filter(!is.na(avg_touches_Recent), !is.na(avg_touches_Early)) %>%
  mutate(
    touch_trend = avg_touches_Recent - avg_touches_Early,
    pts_trend = avg_pts_Recent - avg_pts_Early
  ) %>%
  filter(avg_pts_Recent < 15) %>%  # Filter out established stars
  arrange(desc(touch_trend))

print("Waiver Wire Gems (Increasing Opportunity):")
print(trending %>%
        select(player_name, avg_touches_Early, avg_touches_Recent, touch_trend, pts_trend) %>%
        head(15))
import nfl_data_py as nfl
import pandas as pd

pbp = nfl.import_pbp_data([2023])

# Get weekly touches
skill_plays = pbp[(pbp["receiver_player_id"].notna()) | (pbp["rusher_player_id"].notna())].copy()
skill_plays["player_id"] = skill_plays["receiver_player_id"].fillna(skill_plays["rusher_player_id"])
skill_plays["player_name"] = skill_plays["receiver_player_name"].fillna(skill_plays["rusher_player_name"])

weekly = (skill_plays.groupby(["player_id", "player_name", "week", "posteam"])
    .agg(
        touches=("play_id", "count"),
        rush_yards=("rushing_yards", "sum"),
        rush_tds=("rush_touchdown", "sum"),
        receptions=("complete_pass", "sum"),
        rec_yards=("receiving_yards", "sum"),
        rec_tds=("pass_touchdown", "sum")
    )
    .reset_index())

weekly["fantasy_pts"] = (weekly["rush_yards"] * 0.1 + weekly["rush_tds"] * 6 +
                          weekly["receptions"] * 1 + weekly["rec_yards"] * 0.1 +
                          weekly["rec_tds"] * 6)

# Calculate trends
max_week = weekly["week"].max()
early = weekly[weekly["week"] <= 3].groupby(["player_id", "player_name"]).agg(
    avg_touches_early=("touches", "mean"),
    avg_pts_early=("fantasy_pts", "mean")
).reset_index()

recent = weekly[weekly["week"] >= max_week - 2].groupby(["player_id", "player_name"]).agg(
    avg_touches_recent=("touches", "mean"),
    avg_pts_recent=("fantasy_pts", "mean")
).reset_index()

trending = early.merge(recent, on=["player_id", "player_name"])
trending["touch_trend"] = trending["avg_touches_recent"] - trending["avg_touches_early"]
trending["pts_trend"] = trending["avg_pts_recent"] - trending["avg_pts_early"]

# Filter out established stars
trending = trending[trending["avg_pts_recent"] < 15]
trending = trending.sort_values("touch_trend", ascending=False)

print("Waiver Wire Gems (Increasing Opportunity):")
print(trending[["player_name", "avg_touches_early", "avg_touches_recent", "touch_trend", "pts_trend"]].head(15))
Packages: nflfastR tidyverse nfl_data_py pandas
Quick Package Reference
R Packages
  • nflfastR - Play-by-play data with EPA
  • nflplotR - NFL team logos & plotting
  • tidyverse - Data manipulation & visualization
  • ggplot2 - Advanced visualizations
Python Packages
  • nfl_data_py - NFL data (nflverse compatible)
  • pandas - Data manipulation
  • matplotlib - Visualizations
  • scikit-learn - Machine learning

Ready to Dive Deeper?

Learn the theory behind these techniques in our comprehensive tutorial series

Browse Tutorials