NFL spread modeling starts with EPA/play and DVOA efficiency metrics. Predicted Spread = (Home EPA/play - Away EPA/play) x Plays + HFA. The NFL side market is brutally efficient — edges live in player props, teasers crossing key numbers (3 and 7), and early-week lines before sharp action arrives.

Why This Matters for Agents

The NFL generates more betting handle than any other sport in North America. An autonomous betting agent targeting NFL markets needs sport-specific models — generic approaches fail because NFL scoring is discrete (3s and 7s), the sample size is tiny (272 regular-season games per year), and the market is the sharpest in sports betting.

This is Layer 4 — Intelligence. The agent’s NFL module ingests team and player efficiency data, generates spread and total predictions, projects player props, and identifies correlation opportunities in same-game parlays. These predictions feed into the agent’s expected value framework and Kelly sizing module for position sizing. The efficiency data itself can be sourced via The Odds API for lines or scraped from public EPA/DVOA databases, then compared against live sportsbook prices tracked in the AgentBets Vig Index.

The Math

Team Efficiency Metrics

Three metrics form the foundation of any serious NFL model:

EPA/play (Expected Points Added per play): Measures the change in expected points on each play relative to the situation (down, distance, field position). A team averaging +0.15 EPA/play on offense adds 0.15 expected points per play above baseline. EPA captures both volume (a team that runs 65 plays sees more total EPA) and efficiency (a team that gains +0.20 EPA/play on fewer plays is more efficient per snap).

EPA(play) = EP_after - EP_before

where EP = expected points based on (down, distance, yard_line) lookup table

DVOA (Defense-adjusted Value Over Average): Football Outsiders’ metric comparing each play’s outcome to the league average for that situation, adjusted for opponent. Expressed as a percentage: +15% DVOA means a team is 15% better than league average. DVOA decomposes into offensive DVOA, defensive DVOA (lower is better for defenses), and special teams DVOA.

Success Rate: Binary metric — a play is “successful” if it gains at least 40% of needed yards on 1st down, 60% on 2nd down, or 100% on 3rd/4th down. Success rate correlates more strongly with future performance than yards per play because it is less influenced by explosive play variance.

Point Spread Modeling

The core spread prediction formula:

Predicted Margin = (Home_Off_EPA - Away_Def_EPA) x Expected_Plays/2
                 + (Home_Def_EPA - Away_Off_EPA) x Expected_Plays/2
                 + HFA

Simplified:
Predicted Margin = (Home_Net_EPA - Away_Net_EPA) x Expected_Plays/2 + HFA

where Net_EPA = Offensive EPA/play - Defensive EPA/play (from the team’s perspective, using opponent-adjusted values), Expected_Plays is the average total plays per game for the matchup (typically 120-140 combined), and HFA is home field advantage.

Home Field Advantage (HFA): The historical NFL HFA has declined from ~3 points pre-2020 to approximately 1.5-2.5 points in recent seasons. The COVID-era empty stadiums produced near-zero HFA, and the partial recovery suggests crowd noise accounts for roughly 1-1.5 points of the total effect. Altitude (Denver: +1.5 additional points), travel distance (cross-country: +0.5), and time zone disadvantage (+0.3 for West-to-East early kickoffs) create venue-specific adjustments.

HFA_adjusted = HFA_base + altitude_bonus + travel_penalty + timezone_penalty

NFL average HFA_base ≈ 2.0 points (2021-2025 seasons)
Denver altitude_bonus ≈ 1.5 points
Cross-country travel_penalty ≈ 0.5 points
Timezone disadvantage ≈ 0.3 points

Key Numbers

NFL scoring is discrete: touchdowns (6 + PAT = 7, or 6 + 2pt = 8), field goals (3), and safeties (2). This produces a non-uniform distribution of final margins. The key numbers, based on historical NFL data (2000-2025):

Margin    Frequency    Cumulative
  3        15.6%          —
  7         9.4%          —
  6         5.5%          —
 10         5.4%          —
  4         4.8%          —
 14         4.6%          —
  1         4.3%          —
  0         1.0%       (push)

Three and 7 dominate. An agent pricing NFL spreads must account for the probability mass concentrated at these margins. A spread of -2.5 versus -3 is not a half-point difference — it’s the difference between losing to every game that lands on exactly 3 (15.6% of games) versus pushing. This half-point is worth approximately 5-6 cents on a dollar bet.

Totals Modeling

Predicting the total combined score (Over/Under) uses pace and efficiency:

Predicted Total = Home_Off_Points_Per_Play x Expected_Plays/2
               + Away_Off_Points_Per_Play x Expected_Plays/2
               + Adjustments

A more precise formulation uses drive-level efficiency:

Predicted Total = (Home_Off_Drives x Home_Off_Pts_Per_Drive)
               + (Away_Off_Drives x Away_Off_Pts_Per_Drive)

where:
  Drives ≈ Expected_Plays / Avg_Plays_Per_Drive
  Pts_Per_Drive = f(EPA/play, RedZone_Efficiency, Turnover_Rate)

Game environment matters for totals: wind speed > 15 mph reduces passing efficiency by approximately 0.03 EPA/play; indoor games produce 1.5-2.0 more total points on average; games with temperatures below 32°F reduce totals by approximately 1.5 points.

Teaser Math

A teaser gives you additional points on the spread in exchange for parlaying multiple legs. The standard NFL teaser: 6 points, two teams, at -110 odds.

Break-even math for a two-team teaser at -110:

Payout on $110 bet = $210 ($110 stake + $100 profit)
Required probability = 110 / 210 = 52.38%
Since both legs must win: p_leg^2 = 0.5238
p_leg = sqrt(0.5238) = 0.7237

Each leg needs a 72.4% win rate to break even.

The Wong teaser strategy identifies the mathematically optimal teaser legs. A 6-point teaser is profitable when it crosses through the key numbers 3 and 7. The optimal windows:

Teaser Window Analysis (6-point teaser)

Original Spread → Teased Spread    Key Numbers Crossed    Historical Win %
  -1.0          →    +5.0            0, 3                   ~79%
  -1.5          →    +4.5            0, 3                   ~78%
  -2.0          →    +4.0            0, 3                   ~77%
  -2.5          →    +3.5            0, 3                   ~76%
  +1.5          →    +7.5            3, 7                   ~80%
  +2.0          →    +8.0            3, 7                   ~79%
  +2.5          →    +8.5            3, 7                   ~78%

Break-even per leg: 72.4%
Wong windows exceed break-even by 4-8 percentage points.

The correlation between legs in a same-game scenario can make teasers even more attractive: if you tease the underdog and the under in the same game, these legs are positively correlated (low-scoring games favor underdogs), violating the independence assumption sportsbooks use when pricing teasers.

Player Prop Modeling

Player prop projections decompose into three components:

Projected Stat = Opportunity_Volume x Per_Opportunity_Efficiency x Opponent_Adjustment

Passing Yards:
  Proj = Pass_Attempts x Yards_Per_Attempt x (Lg_Avg_DVOA / Opp_Pass_Def_DVOA)

Rushing Yards:
  Proj = Rush_Attempts x Yards_Per_Carry x (Lg_Avg_DVOA / Opp_Rush_Def_DVOA)

Receiving Yards:
  Proj = Targets x Yards_Per_Target x (Lg_Avg_DVOA / Opp_Pass_Def_DVOA)

where Opportunity_Volume is projected from the player’s snap share, team pace, and game script (a trailing team passes more, a leading team runs more), Per_Opportunity_Efficiency uses the player’s season-long or rolling average (regressed toward position mean early in season), and Opponent_Adjustment scales by the opponent’s defense DVOA for the relevant position group.

Game script adjustment is critical for props. The implied team total (derived from the spread and total) predicts game flow:

Home Implied Total = (Total + Spread) / 2  (using negative spread for favorites)
Away Implied Total = (Total - Spread) / 2

Example: Home -7, Total 48
  Home Implied Total = (48 + 7) / 2 = 27.5
  Away Implied Total = (48 - 7) / 2 = 20.5

A team with an implied total of 27.5 is expected to score ~4 touchdowns. Their QB will throw more, their RB may see game-script-adjusted volume, and their WRs benefit from positive game flow.

Same-Game Parlay Correlation

Sportsbooks price same-game parlay (SGP) legs as if they are independent. They are not. Key NFL correlations:

Correlation Matrix (approximate r values):

                    QB Pass Yds  Team Points  RB Rush Yds  WR Rec Yds
Team Points            +0.60         —          +0.35        +0.45
QB Pass Yds              —         +0.60        -0.15        +0.70
QB Pass TDs            +0.75       +0.70        -0.10        +0.40
RB Rush Yds            -0.15       +0.35          —          -0.10
Game Total             +0.40       +0.75        +0.20        +0.35
Team Wins              +0.20       +0.55        +0.30        +0.15

The exploitable combinations:

QB Passing Yards Over + Team to Win by 7+: r ≈ +0.40. Sportsbooks underestimate this correlation because blowouts often involve sustained passing (unlike the “run out the clock” narrative — teams actually pass more than expected in moderate blowout scenarios through 3 quarters).

WR Receiving Yards Over + QB Passing TDs Over: r ≈ +0.50. A QB having a high-TD game almost certainly throws for volume. The primary target benefits disproportionately.

RB Rush Yards Over + Team Win + Under: r ≈ +0.30. A low-scoring game the team wins implies ball control and rushing efficiency.

Worked Examples

Example 1: Week 8 Spread Prediction

Kansas City Chiefs at Buffalo Bills. Season EPA/play data (through Week 7):

Chiefs Offense EPA/play:  +0.12
Chiefs Defense EPA/play:  -0.08  (negative is good for defense)
Bills Offense EPA/play:   +0.18
Bills Defense EPA/play:   -0.05

Expected combined plays: 130
HFA for Buffalo: 2.0 (base) + 0.0 (no altitude/travel adjustment) = 2.0

The Bills are home. Predicted margin (from Buffalo’s perspective):

Bills_Net = Off(+0.18) - Def_opponent_faces(-0.08) → Bills off vs KC def
KC_Net = Off(+0.12) - Def_opponent_faces(-0.05) → KC off vs BUF def

Home margin = (Bills_Off_EPA - KC_Def_EPA) x 65 + (KC_Off_EPA - Bills_Def_EPA) x (-65) + HFA

= (0.18 - (-0.08)) x 65 - (0.12 - (-0.05)) x 65 + 2.0
= (0.26) x 65 - (0.17) x 65 + 2.0
= 16.9 - 11.05 + 2.0
= 7.85

Predicted line: Bills -7.85

If the market has Bills -6.5 at -110 on BetOnline, the model sees 1.35 points of value on the Bills. The agent checks if this exceeds the threshold for a positive EV bet given the market’s vig.

Example 2: Wong Teaser

The Odds API returns these lines:

Lions -2.5 (-110) at Bears
Packers +1.5 (-110) at Vikings

A 6-point teaser shifts these to:

Lions +3.5 (crosses through 0 and 3)
Packers +7.5 (crosses through 3 and 7)

Historical win rates for these windows: Lions leg ~76%, Packers leg ~80%.

Combined probability = 0.76 x 0.80 = 0.608
Break-even probability at -110 = 0.5238
Edge = 0.608 - 0.5238 = 0.0842 (8.4% edge)

EV per $110 bet = 0.608 x $100 - 0.392 x $110 = $60.80 - $43.12 = +$17.68
ROI = $17.68 / $110 = 16.1%

This is a strong Wong teaser. The agent at Bovada or BetOnline would flag this as a high-EV play.

Example 3: Player Prop — Patrick Mahomes Passing Yards

Mahomes season avg: 38.2 attempts/game, 7.4 yards/attempt
Opponent (Raiders) pass defense DVOA: +12.5% (25th ranked, bad)
League average pass defense DVOA: 0.0% (by definition)

Opponent adjustment = 1 + (Opp_DVOA / 100) = 1 + 0.125 = 1.125
Projected yards = 38.2 x 7.4 x 1.125 = 318.2 yards

Sportsbook line on BookMaker: 289.5 yards (-115 over / -105 under)
Model projection: 318.2 yards
Implied probability of over (assuming normal distribution, std dev ≈ 55 yards):
  z = (289.5 - 318.2) / 55 = -0.522
  P(over) = 1 - Φ(-0.522) = 0.699 (69.9%)

Break-even at -115: 115/215 = 53.5%
Edge: 69.9% - 53.5% = 16.4%

Implementation

import numpy as np
import pandas as pd
from scipy import stats
from dataclasses import dataclass


@dataclass
class TeamEfficiency:
    """Season-level team efficiency metrics."""
    team: str
    off_epa_per_play: float
    def_epa_per_play: float  # negative = good defense
    off_success_rate: float
    def_success_rate: float
    plays_per_game: float
    off_dvoa: float  # percentage, e.g., 15.0 = +15%
    def_dvoa: float  # percentage, negative = good
    pass_def_dvoa: float
    rush_def_dvoa: float


@dataclass
class SpreadPrediction:
    """Output of the spread model."""
    home_team: str
    away_team: str
    predicted_margin: float  # positive = home favored
    predicted_total: float
    home_implied_total: float
    away_implied_total: float
    confidence_interval_90: tuple[float, float]


def predict_spread(
    home: TeamEfficiency,
    away: TeamEfficiency,
    hfa: float = 2.0,
    altitude_bonus: float = 0.0,
    travel_penalty: float = 0.0,
    model_std: float = 13.5
) -> SpreadPrediction:
    """
    Predict NFL point spread from team efficiency metrics.

    Args:
        home: Home team efficiency data
        away: Away team efficiency data
        hfa: Base home field advantage in points
        altitude_bonus: Additional points for altitude (e.g., Denver +1.5)
        travel_penalty: Points for cross-country travel
        model_std: Standard deviation of prediction error (NFL ≈ 13.5 pts)

    Returns:
        SpreadPrediction with margin, total, and confidence interval
    """
    total_hfa = hfa + altitude_bonus + travel_penalty
    avg_plays = (home.plays_per_game + away.plays_per_game) / 2
    half_plays = avg_plays / 2

    # Home offense vs away defense
    home_off_component = (home.off_epa_per_play - away.def_epa_per_play) * half_plays
    # Away offense vs home defense
    away_off_component = (away.off_epa_per_play - home.def_epa_per_play) * half_plays

    predicted_margin = home_off_component - away_off_component + total_hfa

    # Total prediction using points per play
    home_points = (home.off_epa_per_play - away.def_epa_per_play + 0.37) * half_plays
    away_points = (away.off_epa_per_play - home.def_epa_per_play + 0.37) * half_plays
    predicted_total = max(home_points + away_points, 30.0)

    home_implied = (predicted_total + predicted_margin) / 2
    away_implied = (predicted_total - predicted_margin) / 2

    ci_90 = (
        predicted_margin - 1.645 * model_std,
        predicted_margin + 1.645 * model_std
    )

    return SpreadPrediction(
        home_team=home.team,
        away_team=away.team,
        predicted_margin=round(predicted_margin, 1),
        predicted_total=round(predicted_total, 1),
        home_implied_total=round(home_implied, 1),
        away_implied_total=round(away_implied, 1),
        confidence_interval_90=(round(ci_90[0], 1), round(ci_90[1], 1))
    )


def evaluate_teaser(
    spreads: list[float],
    teaser_points: float = 6.0,
    odds: int = -110,
    key_number_rates: dict[int, float] | None = None
) -> dict:
    """
    Evaluate a multi-team NFL teaser for profitability.

    Args:
        spreads: Original spreads for each leg (negative = favorite)
        teaser_points: Points added to each spread (standard: 6)
        odds: American odds for the teaser (standard: -110)
        key_number_rates: Historical frequency of margins at key numbers

    Returns:
        Dictionary with teaser analysis including EV and edge
    """
    if key_number_rates is None:
        key_number_rates = {
            0: 0.010, 1: 0.043, 2: 0.035, 3: 0.156,
            4: 0.048, 5: 0.030, 6: 0.055, 7: 0.094,
            8: 0.028, 9: 0.018, 10: 0.054, 13: 0.032, 14: 0.046
        }

    # Break-even calculation
    if odds < 0:
        break_even_prob = abs(odds) / (abs(odds) + 100)
    else:
        break_even_prob = 100 / (odds + 100)

    n_legs = len(spreads)
    per_leg_break_even = break_even_prob ** (1 / n_legs)

    leg_analysis = []
    for spread in spreads:
        teased = spread + teaser_points
        numbers_crossed = []
        for key in [0, 3, 7]:
            if spread < key <= teased or teased <= key < spread:
                numbers_crossed.append(key)

        probability_gained = sum(
            key_number_rates.get(abs(n), 0) for n in numbers_crossed
        )
        # Base win rate estimate: starts at 50% for pick'em, adjusts
        base_win_rate = 0.50 + (teased * 0.03)  # rough linear approx
        base_win_rate = min(max(base_win_rate, 0.50), 0.90)

        # Add probability mass from key numbers crossed
        estimated_win_rate = min(base_win_rate + probability_gained * 0.5, 0.95)

        leg_analysis.append({
            "original_spread": spread,
            "teased_spread": teased,
            "key_numbers_crossed": numbers_crossed,
            "estimated_win_rate": round(estimated_win_rate, 3),
            "exceeds_breakeven": estimated_win_rate > per_leg_break_even
        })

    combined_prob = np.prod([leg["estimated_win_rate"] for leg in leg_analysis])

    if odds < 0:
        profit_on_win = 100
        risk = abs(odds)
    else:
        profit_on_win = odds
        risk = 100

    ev = combined_prob * profit_on_win - (1 - combined_prob) * risk
    roi = ev / risk

    return {
        "legs": leg_analysis,
        "combined_probability": round(combined_prob, 4),
        "break_even_probability": round(break_even_prob, 4),
        "per_leg_break_even": round(per_leg_break_even, 4),
        "edge": round(combined_prob - break_even_prob, 4),
        "ev_per_100_risked": round(ev / risk * 100, 2),
        "is_positive_ev": combined_prob > break_even_prob
    }


def project_player_prop(
    season_attempts: float,
    yards_per_attempt: float,
    opp_dvoa: float,
    prop_line: float,
    prop_odds: int = -115,
    std_dev: float = 55.0,
    stat_type: str = "passing_yards"
) -> dict:
    """
    Project a player stat and evaluate the prop bet.

    Args:
        season_attempts: Player's average attempts per game
        yards_per_attempt: Player's yards per attempt
        opp_dvoa: Opponent defense DVOA for the relevant stat (positive = bad D)
        prop_line: Sportsbook over/under line
        prop_odds: American odds on the over
        std_dev: Standard deviation of the stat distribution
        stat_type: Type of stat for labeling

    Returns:
        Dictionary with projection, edge, and EV analysis
    """
    # Opponent adjustment: DVOA is % above average, so bad defense = positive
    opp_multiplier = 1 + (opp_dvoa / 100)
    projection = season_attempts * yards_per_attempt * opp_multiplier

    # Probability of going over the line (normal approximation)
    z_score = (prop_line - projection) / std_dev
    prob_over = 1 - stats.norm.cdf(z_score)
    prob_under = stats.norm.cdf(z_score)

    # Break-even probability
    if prop_odds < 0:
        break_even = abs(prop_odds) / (abs(prop_odds) + 100)
        profit_on_win = 100
        risk = abs(prop_odds)
    else:
        break_even = 100 / (prop_odds + 100)
        profit_on_win = prop_odds
        risk = 100

    edge_over = prob_over - break_even
    ev_over = prob_over * profit_on_win - (1 - prob_over) * risk

    return {
        "stat_type": stat_type,
        "projection": round(projection, 1),
        "prop_line": prop_line,
        "prob_over": round(prob_over, 3),
        "prob_under": round(prob_under, 3),
        "break_even": round(break_even, 3),
        "edge_over": round(edge_over, 3),
        "ev_per_100_risked_over": round(ev_over / risk * 100, 2),
        "recommendation": "OVER" if edge_over > 0.02 else ("UNDER" if edge_over < -0.05 else "PASS"),
        "z_score": round(z_score, 3)
    }


def sgp_correlation_ev(
    leg_probs: list[float],
    correlation_matrix: np.ndarray,
    sgp_odds: float,
    stake: float = 100.0
) -> dict:
    """
    Estimate same-game parlay EV accounting for correlation.

    Uses a Gaussian copula approximation to estimate the joint probability
    of all legs winning, given pairwise correlations.

    Args:
        leg_probs: Marginal win probability for each leg
        correlation_matrix: Pairwise correlation matrix (n x n)
        sgp_odds: Decimal odds offered by sportsbook (e.g., 3.5)
        stake: Bet amount

    Returns:
        Dictionary with independent vs correlated probability and EV
    """
    n = len(leg_probs)

    # Independent probability (what sportsbook assumes)
    independent_prob = np.prod(leg_probs)

    # Gaussian copula approximation for correlated joint probability
    # Convert marginal probs to normal quantiles
    normal_quantiles = [stats.norm.ppf(p) for p in leg_probs]

    # Simulate from multivariate normal with correlation structure
    np.random.seed(42)
    n_sims = 100_000
    samples = np.random.multivariate_normal(
        mean=np.zeros(n),
        cov=correlation_matrix,
        size=n_sims
    )

    # Convert back to uniform, then check if each leg wins
    uniform_samples = stats.norm.cdf(samples)
    wins = np.all(uniform_samples < np.array(leg_probs), axis=1)
    correlated_prob = np.mean(wins)

    # EV calculation
    payout = stake * sgp_odds
    ev_independent = independent_prob * payout - stake
    ev_correlated = correlated_prob * payout - stake

    return {
        "independent_probability": round(independent_prob, 4),
        "correlated_probability": round(correlated_prob, 4),
        "probability_uplift": round(correlated_prob - independent_prob, 4),
        "sgp_odds_decimal": sgp_odds,
        "implied_probability": round(1 / sgp_odds, 4),
        "ev_if_independent": round(ev_independent, 2),
        "ev_with_correlation": round(ev_correlated, 2),
        "edge_from_correlation": round(
            (correlated_prob - 1 / sgp_odds) * 100, 2
        ),
        "is_positive_ev": correlated_prob > (1 / sgp_odds)
    }


# --- Example usage ---
if __name__ == "__main__":
    # Spread prediction
    bills = TeamEfficiency(
        team="BUF", off_epa_per_play=0.18, def_epa_per_play=-0.05,
        off_success_rate=0.50, def_success_rate=0.42,
        plays_per_game=66, off_dvoa=22.5, def_dvoa=-8.3,
        pass_def_dvoa=-12.0, rush_def_dvoa=-4.5
    )
    chiefs = TeamEfficiency(
        team="KC", off_epa_per_play=0.12, def_epa_per_play=-0.08,
        off_success_rate=0.47, def_success_rate=0.40,
        plays_per_game=64, off_dvoa=18.0, def_dvoa=-12.0,
        pass_def_dvoa=-15.0, rush_def_dvoa=-9.0
    )

    pred = predict_spread(bills, chiefs, hfa=2.0)
    print(f"=== {pred.away_team} at {pred.home_team} ===")
    print(f"Predicted spread: {pred.home_team} {pred.predicted_margin:+.1f}")
    print(f"Predicted total: {pred.predicted_total}")
    print(f"90% CI: [{pred.confidence_interval_90[0]:+.1f}, {pred.confidence_interval_90[1]:+.1f}]")
    print()

    # Teaser evaluation
    teaser = evaluate_teaser([-2.5, 1.5], teaser_points=6.0, odds=-110)
    print("=== Wong Teaser ===")
    for leg in teaser["legs"]:
        print(f"  {leg['original_spread']:+.1f}{leg['teased_spread']:+.1f}  "
              f"Keys crossed: {leg['key_numbers_crossed']}  "
              f"Win rate: {leg['estimated_win_rate']:.1%}")
    print(f"  Combined: {teaser['combined_probability']:.1%}  "
          f"Edge: {teaser['edge']:+.1%}  "
          f"EV/100: ${teaser['ev_per_100_risked']:.2f}")
    print()

    # Player prop
    prop = project_player_prop(
        season_attempts=38.2, yards_per_attempt=7.4,
        opp_dvoa=12.5, prop_line=289.5, prop_odds=-115,
        std_dev=55.0, stat_type="passing_yards"
    )
    print(f"=== Player Prop: {prop['stat_type']} ===")
    print(f"  Projection: {prop['projection']} yds vs line {prop['prop_line']}")
    print(f"  P(over): {prop['prob_over']:.1%}  Edge: {prop['edge_over']:+.1%}")
    print(f"  Recommendation: {prop['recommendation']}")
    print()

    # SGP correlation
    corr_matrix = np.array([
        [1.0, 0.6],
        [0.6, 1.0]
    ])
    sgp = sgp_correlation_ev(
        leg_probs=[0.65, 0.60],
        correlation_matrix=corr_matrix,
        sgp_odds=2.80
    )
    print("=== Same-Game Parlay (2 legs, r=0.6) ===")
    print(f"  Independent prob: {sgp['independent_probability']:.1%}")
    print(f"  Correlated prob:  {sgp['correlated_probability']:.1%}")
    print(f"  EV (correlated):  ${sgp['ev_with_correlation']:.2f} per $100")
    print(f"  Positive EV: {sgp['is_positive_ev']}")

Limitations and Edge Cases

Small sample sizes destroy signal. The NFL regular season is 18 weeks. Through Week 4, EPA/play estimates have massive variance. An agent must regress team metrics toward the league mean early in the season. A reasonable schedule: 75% regression to mean in Weeks 1-3, 50% in Weeks 4-6, 25% in Weeks 7-10, 0% from Week 11 onward. Even at season’s end, 17 games is a tiny sample — one blowout can shift a team’s EPA/play by 0.02.

Injuries are the largest unmodeled variable. A starting QB injury changes a team’s EPA/play by 0.05-0.15 overnight. No efficiency model captures this without a real-time injury-adjusted layer. An agent must monitor injury reports (typically released Wednesday, Thursday, and Friday during the NFL week) and apply manual adjustments.

The side market is brutally efficient. NFL point spreads close within 1 point of the true probability over 97% of the time. The median sharp model generates 1-2% ROI on sides — barely above the vig. An agent targeting NFL sides needs sub-1% edge detection, low vig sportsbooks (check the Vig Index), and high volume to overcome variance.

Player props have wider edges but lower limits. Sportsbooks know their prop lines are softer, so they limit winning prop bettors aggressively. An agent running a profitable prop model on Bovada or MyBookie will face reduced limits within weeks. Diversifying across books (including sharp-friendly BookMaker) extends the window.

Teaser math assumes historical distributions hold. If the NFL’s scoring distribution shifts (rule changes, offensive evolution), the key number frequencies change. The 2-point conversion push in recent years slightly reduces the frequency of margin = 7. An agent should recalculate key number frequencies each season.

Correlation estimates are noisy. The r = 0.60 between QB passing yards and team points is a historical average. Individual QBs deviate: a run-heavy team’s QB might show r = 0.40, while a pass-first team’s QB shows r = 0.75. Use team-specific correlations when the sample size supports it (3+ seasons of the same QB-system combination).

FAQ

How do you build an NFL point spread model?

An NFL point spread model starts with team efficiency metrics — EPA/play, DVOA, and success rate — as inputs. The core formula is Predicted Spread = (Home Offensive EPA/play - Away Defensive EPA/play) x Expected Plays/2 - (Away Offensive EPA/play - Home Defensive EPA/play) x Expected Plays/2 + Home Field Advantage. HFA in the modern NFL is approximately 1.5-2.5 points. Regress efficiency inputs toward the mean early in the season when sample sizes are small.

What are the key numbers in NFL point spreads?

The key numbers are 3 and 7 because NFL games are decided by field goals (3 points) and touchdowns with extra points (7 points). Approximately 15.6% of NFL games land on a margin of exactly 3, and 9.4% land on exactly 7. These numbers make teasing through them mathematically valuable — a half-point around 3 is worth approximately 5-6 cents on the dollar.

How does teaser math work in NFL betting?

A standard 6-point NFL teaser shifts your spread by 6 points and requires all legs to win. A two-team teaser at -110 requires each leg to win at approximately 72.4% to break even (since both must hit: 0.7237^2 = 0.5238). The Wong teaser strategy targets spreads from -1 to -2.5 and +1.5 to +2.5, which when teased cross through both 3 and 7, capturing roughly 25% additional probability mass.

How do you model NFL player props mathematically?

Player prop modeling decomposes into three factors: opportunity volume (target share, snap percentage, rush attempts), per-opportunity efficiency (yards per target, yards per carry), and opponent adjustment (defense DVOA by position group). The projection formula is Projected Stat = Opportunity Volume x Efficiency x Opponent Multiplier. For passing yards: Projected = Attempts x Yards/Attempt x (1 + Opponent Pass DVOA / 100).

How does the Elo rating system connect to NFL modeling?

Elo ratings provide a power ranking foundation that captures long-term team strength. An Elo difference of 25 points corresponds to roughly 1 point of spread. Elo can supplement EPA-based models by providing stable baselines early in the season when efficiency sample sizes are small. See the Elo Ratings and Power Rankings guide for the full mathematical derivation and implementation.

What’s Next

This guide covers NFL-specific modeling. The techniques here connect to several other guides in the series:

  • Power rankings as model input: The Elo Ratings and Power Rankings guide provides the rating system that feeds into spread predictions as a complementary signal to EPA.
  • Regression techniques for NFL features: Regression Models for Sports Betting covers the linear, logistic, and ridge regression methods used to weight EPA, DVOA, and situational factors.
  • Soccer equivalent: The Expected Goals (xG) Betting Model applies analogous efficiency decomposition to football/soccer, using shot-level data instead of play-level EPA.
  • Sizing your NFL bets: Run your spread edge through the Kelly Criterion to determine optimal position sizes given the high variance of NFL outcomes.
  • Full agent pipeline: The Agent Betting Stack shows how the NFL intelligence module connects to wallet management, data ingestion, and execution layers.
  • Finding the best lines: Compare vig across sportsbooks for NFL markets at the AgentBets Vig Index — even 0.5% vig reduction compounds significantly over a full season.